Python SDK
✅ Available - version 0.1.0
The official Python 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. Available in both a synchronous and an asynchronous flavour.
Requirements
- Python 3.10 or newer (tested up to Python 3.14)
- The synchronous client has no runtime dependencies - it uses the standard library
- The asynchronous client requires
httpx(installed through the[async]extra)
Installation
pip install dpay-python-sdk
Asynchronous flavour:
pip install "dpay-python-sdk[async]"
You install dpay-python-sdk but you import dpay. Do not install the PyPI package named dpay - it is an unrelated project that claims the same top-level module and would shadow the dpay.pl SDK.
The package is published on PyPI and the source code is available on GitHub.
Configuration
Create a DPayClient instance using credentials from the dpay.pl Panel:
from dpay import DPayClient
dpay = DPayClient(
service="service_name",
secret_hash="your_secret_hash",
)
| Option | Type | Description |
|---|---|---|
service | str | Payment Point name from panel.dpay.pl (required) |
secret_hash | str | Secret Hash used for checksum generation (required) |
timeout | int | HTTP request timeout in seconds (default 30) |
http_client | HttpClient | Custom HTTP client (tests, proxy) |
base_urls | dict[str, str] | API host overrides (keys: api_payments, panel, gateway) |
The client exposes services as attributes:
| 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 |
Asynchronous client
AsyncDPayClient has an identical API - the same services, the same request and response objects - and differs only in that its methods are coroutines:
from dpay.aio import AsyncDPayClient
async with AsyncDPayClient(
service="service_name",
secret_hash="your_secret_hash",
) as dpay:
payment = await dpay.payments.register(request)
transaction = await dpay.payments.details(payment.transaction_id)
Outside an async with block, close the client yourself with await dpay.aclose().
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.
from dpay import Money
Money.pln(1050) # 10.50 PLN
Money.of(500, "EUR") # 5.00 EUR
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 with_*() setters. The checksum and the transactionType field are added automatically:
from dpay import (
DPayClient,
Money,
Payer,
RegisterPaymentRequest,
ReturnUrls,
TransactionType,
)
dpay = DPayClient(
service="service_name",
secret_hash="your_secret_hash",
)
payment = dpay.payments.register(
RegisterPaymentRequest.create(
Money.pln(1050),
TransactionType.TRANSFERS,
ReturnUrls(
"https://yourshop.com/success",
"https://yourshop.com/failure",
"https://yourshop.com/ipn",
),
)
.with_description("Order #1234")
.with_custom("order-1234")
.with_payer(Payer.create().with_email("customer@example.com"))
)
if payment.redirect_url is not None:
return redirect(payment.redirect_url)
if payment.is_paid:
... # payment settled inline (e.g. BLIK Level 0)
Transaction types (TransactionType): TRANSFERS, DCB_GATEWAY, CARD_AUTH, MB_WAY_DIRECT, BIZUM_DIRECT, BLIK_RECURRING, CARD_RECURRING.
Key RegisterPaymentRequest setters
| Method | Description |
|---|---|
with_description(str) | Transaction description visible to the customer |
with_custom(str) | Your own order identifier (returned in IPN) |
with_payer(Payer) | Customer e-mail and name |
with_channel(str) | 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: str, user_agent: str, user_ip: str) | BLIK Level 0 payment (6-digit code) |
with_blik_alias(alias: str, user_agent: str, user_ip: str) | 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(str) | Charge a stored card (MIT) |
with_payout(PayoutInstruction) | 1:1 payout instruction |
with_efaktura(InvoiceDetails | None) | eInvoice (KSeF) payment |
with_phone_number(phone: str, currency: str) | MB WAY |
Handling IPN
Verify IPN notifications with IpnVerifier.construct_event() - an invalid signature raises SignatureVerificationError:
from dpay import IpnEvent, IpnVerifier, SignatureVerificationError
def ipn_view(request):
try:
event = IpnVerifier.construct_event(request.body, "your_secret_hash")
except SignatureVerificationError:
return HttpResponse("Invalid signature", status=400)
if event.is_transfer:
mark_order_as_paid(event.id, event.amount)
if event.is_capture:
mark_order_as_captured(event.id, event.amount, event.capture_payment_id)
return HttpResponse(IpnEvent.ACK)
OKdpay.pl considers an IPN delivered only when the response body is exactly OK (the 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.is_paid # True for paid and captured
transaction.value.to_decimal() # "29.99"
transaction.refunded_amount # Money
transaction.available_refund_amount # Money
transaction.is_fully_refunded # bool
transaction.refunds # list[TransactionRefund]
Refunds
from dpay import Money
# full refund
refund = dpay.refunds.create("transaction-id")
# partial refund with a reason
refund = dpay.refunds.create("transaction-id", Money.pln(500), "complaint")
refund.is_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", Money.pln(500))
if not availability.is_available:
availability.message # e.g. "Transaction has not been paid"
availability.http_status # stable reason code, see the table
| 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[0].id
banks[0].name
BLIK - OneClick and Recurring aliases
alias = dpay.blik.alias("DPAY.UID.123456.abc12345")
alias.is_active
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:
from dpay import (
CardData,
CardEncryptor,
CardPaymentRequest,
DccDecision,
DeviceInfo,
)
public_key = dpay.cards.public_key()
encrypted = CardEncryptor().encrypt(
CardData("4111111111111111", "123", "12/30"),
transaction_id,
public_key,
)
device_info = DeviceInfo.create(...) # payer browser data
result = dpay.cards.pay_otp(
transaction_id,
CardPaymentRequest.create(device_info)
.with_encrypted_card_data(encrypted)
.with_card_holder("John", "Smith")
.with_channel_id(31),
)
if result.is_success:
... # payment captured
elif result.requires_three_ds_form:
html = result.three_ds_form_html # render in the payer's browser
elif result.has_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(DccDecision.ACCEPT)
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 is implemented in pure Python, so S2S cards require no additional cryptography library.
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.is_processed
details.net.to_decimal()
details.receiver
Error handling
All SDK exceptions derive from dpay.DPayError:
| Exception | When |
|---|---|
TransportError | Network error - payment status unknown, use payments.details() |
AuthenticationError | 401 - invalid checksum or secret hash |
InvalidRequestError | 400/422 - validation errors (field_errors) |
AccessDeniedError | 403 - operation not permitted |
NotFoundError | 404 - resource does not exist |
RateLimitError | 429 - rate limited (retry_after) |
PaymentRejectedError | Registration rejected (e.g. wrong BLIK code; transaction_id) |
CardPaymentError | Card payment rejection (error_code) |
SignatureVerificationError | Invalid IPN signature |
ApiServerError | 5xx or a malformed API response |
DPayValueError | Invalid argument (also derives from ValueError) |
from dpay import ApiError, InvalidRequestError, TransportError
try:
payment = dpay.payments.register(request)
except InvalidRequestError as error:
error.field_errors # dict[str, list[str]]
except ApiError as error:
error.http_status
error.error_code
except TransportError:
... # unknown whether the request arrived - verify via payments.details()
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:
from dpay import DPayClient
from dpay.testing import MockHttpClient
transport = MockHttpClient()
transport.queue_json(200, {"transactionId": "tx-1", "msg": "https://secure.dpay.pl/pay/1"})
dpay = DPayClient(service="test", secret_hash="test", http_client=transport)
payment = dpay.payments.register(request)
assert transport.last_request_body["value"] == "10.50"
For the asynchronous client use MockAsyncHttpClient.