Skip to main content

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]"
Package name

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",
)
OptionTypeDescription
servicestrPayment Point name from panel.dpay.pl (required)
secret_hashstrSecret Hash used for checksum generation (required)
timeoutintHTTP request timeout in seconds (default 30)
http_clientHttpClientCustom HTTP client (tests, proxy)
base_urlsdict[str, str]API host overrides (keys: api_payments, panel, gateway)

The client exposes services as attributes:

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

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

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

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

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

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.is_processed
details.net.to_decimal()
details.receiver

Error handling

All SDK exceptions derive from dpay.DPayError:

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


More information