Ruby SDK
✅ Available - version 0.1.0
The official Ruby library for integrating with the dpay.pl API. It automates checksum generation, IPN signature verification and API calls: payment registration, transaction details, refunds, bank list, BLIK aliases, server-to-server card payments and 1:1 payouts. Amounts are represented by the Money object (minor units as int) and API responses are mapped to typed objects. Zero runtime dependencies, RBS type signatures included.
Requirements
- Ruby 3.1 or newer (tested up to Ruby 4.0)
- Zero runtime dependencies - the SDK uses only the standard library (
net/http,openssl,json) - Ships RBS type signatures (
sig/) for type checking (Steep) and editor autocompletion
Installation
gem install dpay
Or in your Gemfile:
gem "dpay"
The gem is published on RubyGems and the source code is available on GitHub.
Configuration
Create a DPay::Client instance using credentials from the dpay.pl Panel:
require "dpay"
dpay = DPay::Client.new(
service: "service_name",
secret_hash: "your_secret_hash",
)
| Option | Type | Description |
|---|---|---|
service | String | Payment Point name from panel.dpay.pl (required) |
secret_hash | String | Secret Hash used for checksum generation (required) |
timeout | Integer | HTTP request timeout in seconds (default 30) |
http_client | DPay::HTTP::Client | Custom HTTP client (tests, proxy) |
base_urls | Hash | API host overrides (keys: :api_payments, :panel, :gateway) |
The client exposes services as methods:
| Service | Scope |
|---|---|
dpay.payments | Payment registration, transaction details |
dpay.refunds | Refunds and refund availability checks |
dpay.banks | Pay-by-link bank list |
dpay.blik | BLIK OneClick and Recurring aliases |
dpay.cards | Server-to-server card payments |
dpay.payouts | 1:1 payout details |
Amounts - the Money object
Every amount in the SDK is a DPay::Money object that internally stores minor units (grosze) as int. The SDK takes care of the correct amount format for each endpoint (decimal amount vs minor units), so you never have to remember which endpoint expects which format.
DPay::Money.pln(1050) # 10.50 PLN
DPay::Money.of(500, "EUR") # 5.00 EUR
DPay::Money.from_decimal("10.50", "PLN")
money.minor # 1050 (minor units)
money.to_decimal # "10.50"
Money is immutable and comparable - two objects with the same amount and currency are equal.
Payment registration
Build the request with RegisterPaymentRequest - required fields go into create, optional ones via a chain of with_* setters. The checksum and the transactionType field are added automatically:
require "dpay"
dpay = DPay::Client.new(
service: "service_name",
secret_hash: "your_secret_hash",
)
payment = dpay.payments.register(
DPay::RegisterPaymentRequest.create(
DPay::Money.pln(1050),
DPay::TransactionType::TRANSFERS,
DPay::ReturnUrls.new(
"https://yourshop.com/success",
"https://yourshop.com/failure",
"https://yourshop.com/ipn",
),
)
.with_description("Order #1234")
.with_custom("order-1234")
.with_payer(DPay::Payer.create.with_email("customer@example.com")),
)
redirect_to payment.redirect_url if payment.redirect_url
# payment settled inline (e.g. BLIK Level 0)
mark_order_as_paid if payment.paid?
Transaction types (TransactionType): TRANSFERS, DCB_GATEWAY, CARD_AUTH, MB_WAY_DIRECT, BIZUM_DIRECT, BLIK_RECURRING, CARD_RECURRING.
Key RegisterPaymentRequest setters
| Method | Description |
|---|---|
with_description(String) | Transaction description visible to the customer |
with_custom(String) | Your own order identifier (returned in IPN) |
with_payer(Payer) | Customer e-mail and name |
with_channel(String) | Direct payment via a specific bank channel |
with_credit_card(bool) / with_paypal(bool) / with_paysafecard(bool) / with_installment(bool) / with_blik(bool) | Enable or disable methods on the gateway |
with_no_banks(bool) | Hide the bank list |
with_blik_code(code, user_agent, user_ip) | BLIK Level 0 payment (6-digit code) |
with_blik_alias(alias_value, user_agent, user_ip) | Pay with a BLIK OneClick alias |
with_register_blik_alias(BlikAliasRegistration) | Register a OneClick alias during payment |
with_card_recurring(CardRecurringRegistration) | Register a card recurring mandate |
with_card_recurring_alias(String) | Charge a stored card (MIT) |
with_payout(PayoutInstruction) | 1:1 payout instruction |
with_efaktura(InvoiceDetails) | eInvoice (KSeF) payment |
with_phone_number(phone, currency) | MB WAY |
Handling IPN
Verify IPN notifications with IpnVerifier.construct_event - an invalid signature raises SignatureVerificationError:
begin
event = DPay::IpnVerifier.construct_event(request.raw_post, "your_secret_hash")
rescue DPay::SignatureVerificationError
return render plain: "Invalid signature", status: :bad_request
end
mark_order_as_paid(event.id, event.amount) if event.transfer?
mark_order_as_captured(event.id, event.amount, event.capture_payment_id) if event.capture?
render plain: DPay::IpnEvent::ACK
OKdpay.pl considers an IPN delivered only when the response body is exactly OK (the DPay::IpnEvent::ACK constant). The HTTP status code is ignored. Make sure your framework does not append anything to the body.
Always compare event.amount (string, e.g. "10.00") with the order amount in your database and process each transaction only once (an IPN may be delivered multiple times).
Transaction details
transaction = dpay.payments.details("transaction-id")
transaction.status # "paid", "created", "processing", "expired", "captured"
transaction.paid? # true for paid and captured
transaction.value.to_decimal # "29.99"
transaction.refunded_amount # DPay::Money
transaction.available_refund_amount # DPay::Money
transaction.fully_refunded? # bool
transaction.refunds # Array<TransactionRefund>
Refunds
# full refund
refund = dpay.refunds.create("transaction-id")
# partial refund with a reason
refund = dpay.refunds.create("transaction-id", DPay::Money.pln(500), "complaint")
refund.accepted?
Before refunding you can check availability - the method returns a business result (it does not raise) even when the refund is not possible:
availability = dpay.refunds.check_availability("transaction-id", DPay::Money.pln(500))
unless availability.available?
availability.message # e.g. "Transaction has not been paid"
availability.http_status # stable reason code, see the table
end
| HTTP code | Refusal reason |
|---|---|
400 | Amount exceeds the available refund amount |
401 | The payment channel does not support refunds |
402 | The transaction has not been paid |
406 | Insufficient balance to cover the refund |
409 | A refund request has already been submitted |
410 | The transaction has already been refunded |
411 | Charge transactions cannot be refunded |
Banks
banks = dpay.banks.all # all dpay.pl banks
banks = dpay.banks.for_service # banks available for your service
banks.first.id
banks.first.name
BLIK - OneClick and Recurring aliases
blik_alias = dpay.blik.alias("DPAY.UID.123456.abc12345")
blik_alias.active?
blik_alias.apps
dpay.blik.unregister_alias("DPAY.UID.123456.abc12345")
status = dpay.blik.recurring_status("PAYID-...")
status.registration
Alias registration and alias payments happen through payment registration (with_blik_code + with_register_blik_alias, payment via with_blik_alias).
Server-to-server cards
Card data is encrypted with an RSA key fetched before every payment attempt:
public_key = dpay.cards.public_key
encrypted = DPay::CardEncryptor.new.encrypt(
DPay::CardData.new("4111111111111111", "123", "12/30"),
transaction_id,
public_key,
)
device_info = DPay::DeviceInfo.create(...) # payer browser data
result = dpay.cards.pay_otp(
transaction_id,
DPay::CardPaymentRequest.create(device_info)
.with_encrypted_card_data(encrypted)
.with_card_holder("John", "Smith")
.with_channel_id(31),
)
if result.success?
# payment captured
elsif result.three_ds_form?
html = result.three_ds_form_html # render in the payer's browser
elsif result.dcc_offer?
offer = result.dcc_offer # present the DCC offer to the payer
# send the decision by calling pay_otp again with .with_dcc_decision(DPay::DccDecision::ACCEPT)
end
Also available: pre_auth (pre-authorization), capture and cancel, google_pay and apple_pay. A payment rejection at HTTP 200 raises CardPaymentError with an error code.
RSA encryption uses the standard OpenSSL library, so S2S cards require no additional cryptography dependency.
S2S cards require PCI-DSS compliance on the merchant side. See the S2S cards documentation for the full flow.
1:1 payouts
details = dpay.payouts.details(12345)
details.processed?
details.net.to_decimal
details.receiver
Error handling
Every SDK exception can be caught through the DPay::Error marker:
| Exception | When |
|---|---|
DPay::TransportError | Network error - payment status unknown, use payments.details |
DPay::AuthenticationError | 401 - invalid checksum or secret hash |
DPay::InvalidRequestError | 400/422 - validation errors (field_errors) |
DPay::AccessDeniedError | 403 - operation not permitted |
DPay::NotFoundError | 404 - resource does not exist |
DPay::RateLimitError | 429 - rate limited (retry_after) |
DPay::PaymentRejectedError | Registration rejected (e.g. wrong BLIK code; transaction_id) |
DPay::CardPaymentError | Card payment rejection (error_code) |
DPay::SignatureVerificationError | Invalid IPN signature |
DPay::ApiServerError | 5xx or a malformed API response |
DPay::InvalidArgumentError | Invalid argument (also derives from ArgumentError) |
begin
payment = dpay.payments.register(request)
rescue DPay::InvalidRequestError => error
error.field_errors # Hash of fields and validation messages
rescue DPay::ApiError => error
error.http_status
error.error_code
rescue DPay::TransportError
# unknown whether the request arrived - verify via payments.details
end
Testing your integration
The SDK ships a test transport that queues responses and records the requests that were sent, so your tests need no network access:
require "dpay"
require "dpay/testing"
transport = DPay::Testing::MockHttpClient.new
transport.queue_json(200, { "transactionId" => "tx-1", "msg" => "https://secure.dpay.pl/pay/1" })
dpay = DPay::Client.new(service: "test", secret_hash: "test", http_client: transport)
payment = dpay.payments.register(request)
transport.last_request_body["value"] # "10.50"