Skip to main content

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",
)
OptionTypeDescription
serviceStringPayment Point name from panel.dpay.pl (required)
secret_hashStringSecret Hash used for checksum generation (required)
timeoutIntegerHTTP request timeout in seconds (default 30)
http_clientDPay::HTTP::ClientCustom HTTP client (tests, proxy)
base_urlsHashAPI host overrides (keys: :api_payments, :panel, :gateway)

The client exposes services as methods:

ServiceScope
dpay.paymentsPayment registration, transaction details
dpay.refundsRefunds and refund availability checks
dpay.banksPay-by-link bank list
dpay.blikBLIK OneClick and Recurring aliases
dpay.cardsServer-to-server card payments
dpay.payouts1: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

MethodDescription
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
The response body must be exactly OK

dpay.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.

info

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 codeRefusal reason
400Amount exceeds the available refund amount
401The payment channel does not support refunds
402The transaction has not been paid
406Insufficient balance to cover the refund
409A refund request has already been submitted
410The transaction has already been refunded
411Charge 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.

info

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:

ExceptionWhen
DPay::TransportErrorNetwork error - payment status unknown, use payments.details
DPay::AuthenticationError401 - invalid checksum or secret hash
DPay::InvalidRequestError400/422 - validation errors (field_errors)
DPay::AccessDeniedError403 - operation not permitted
DPay::NotFoundError404 - resource does not exist
DPay::RateLimitError429 - rate limited (retry_after)
DPay::PaymentRejectedErrorRegistration rejected (e.g. wrong BLIK code; transaction_id)
DPay::CardPaymentErrorCard payment rejection (error_code)
DPay::SignatureVerificationErrorInvalid IPN signature
DPay::ApiServerError5xx or a malformed API response
DPay::InvalidArgumentErrorInvalid 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"

More information