Python SDK
✅ Dostępne - wersja 0.1.0
Oficjalna biblioteka Pythona do integracji z API dpay.pl. Automatyzuje generowanie sum kontrolnych, weryfikację powiadomień IPN oraz wywołania API: rejestrację płatności, szczegóły transakcji, zwroty, listę banków, aliasy BLIK, płatności kartowe server-to-server i wypłaty 1:1. Kwoty są reprezentowane obiektem Money (grosze jako int), a odpowiedzi API mapowane na typowane obiekty. Dostępny w wariancie synchronicznym i asynchronicznym.
Wymagania
- Python 3.10 lub nowszy (testowane do Pythona 3.14)
- Klient synchroniczny nie ma żadnych zależności runtime - korzysta z biblioteki standardowej
- Klient asynchroniczny wymaga
httpx(instalowanego przez extra[async])
Instalacja
pip install dpay-python-sdk
Wariant asynchroniczny:
pip install "dpay-python-sdk[async]"
Instalujesz dpay-python-sdk, a importujesz dpay. Nie instaluj pakietu o nazwie dpay z PyPI - to niepowiązany projekt, który zajmuje ten sam moduł najwyższego poziomu i przykryłby SDK dpay.pl.
Pakiet jest publikowany na PyPI, a kod źródłowy dostępny na GitHub.
Konfiguracja
Utwórz instancję klienta DPayClient, podając dane z Panelu dpay.pl:
from dpay import DPayClient
dpay = DPayClient(
service="nazwa_serwisu",
secret_hash="twoj_secret_hash",
)
| Opcja | Typ | Opis |
|---|---|---|
service | str | Nazwa serwisu (Punktu Płatności) z panel.dpay.pl (wymagane) |
secret_hash | str | Klucz Hash (Secret Hash) do generowania sum kontrolnych (wymagane) |
timeout | int | Timeout żądań HTTP w sekundach (domyślnie 30) |
http_client | HttpClient | Własny klient HTTP (testy, proxy) |
base_urls | dict[str, str] | Nadpisanie hostów API (klucze: api_payments, panel, gateway) |
Klient udostępnia serwisy jako atrybuty:
| Serwis | Zakres |
|---|---|
dpay.payments | Rejestracja płatności, szczegóły transakcji |
dpay.refunds | Zwroty i sprawdzanie dostępności zwrotu |
dpay.banks | Lista banków pay-by-link |
dpay.blik | Aliasy BLIK OneClick i Recurring |
dpay.cards | Płatności kartowe server-to-server |
dpay.payouts | Szczegóły wypłat 1:1 |
Klient asynchroniczny
AsyncDPayClient ma identyczne API - te same serwisy, te same obiekty żądań i odpowiedzi - a różni się wyłącznie tym, że metody są korutynami:
from dpay.aio import AsyncDPayClient
async with AsyncDPayClient(
service="nazwa_serwisu",
secret_hash="twoj_secret_hash",
) as dpay:
payment = await dpay.payments.register(request)
transaction = await dpay.payments.details(payment.transaction_id)
Poza kontekstem async with zamknij klienta ręcznie przez await dpay.aclose().
Kwoty - obiekt Money
Wszystkie kwoty w SDK to obiekt dpay.Money, wewnętrznie przechowujący grosze jako int. SDK sam dba o właściwy format kwoty dla każdego endpointu (kwota dziesiętna albo grosze), więc nie musisz pamiętać, który endpoint oczekuje którego formatu.
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 (grosze)
money.to_decimal() # "10.50"
Money jest niemutowalny i porównywalny - dwa obiekty o tej samej kwocie i walucie są sobie równe.
Rejestracja płatności
Żądanie budujesz obiektem RegisterPaymentRequest - pola wymagane w create(), opcjonalne przez settery with_*(). Suma kontrolna i pole transactionType są dokładane automatycznie:
from dpay import (
DPayClient,
Money,
Payer,
RegisterPaymentRequest,
ReturnUrls,
TransactionType,
)
dpay = DPayClient(
service="nazwa_serwisu",
secret_hash="twoj_secret_hash",
)
payment = dpay.payments.register(
RegisterPaymentRequest.create(
Money.pln(1050),
TransactionType.TRANSFERS,
ReturnUrls(
"https://twojsklep.pl/sukces",
"https://twojsklep.pl/blad",
"https://twojsklep.pl/ipn",
),
)
.with_description("Zamówienie #1234")
.with_custom("order-1234")
.with_payer(Payer.create().with_email("klient@example.com"))
)
if payment.redirect_url is not None:
return redirect(payment.redirect_url)
if payment.is_paid:
... # płatność rozliczona inline (np. BLIK Level 0)
Typy transakcji (TransactionType): TRANSFERS, DCB_GATEWAY, CARD_AUTH, MB_WAY_DIRECT, BIZUM_DIRECT, BLIK_RECURRING, CARD_RECURRING.
Ważniejsze settery RegisterPaymentRequest
| Metoda | Opis |
|---|---|
with_description(str) | Opis transakcji widoczny dla klienta |
with_custom(str) | Własne dane identyfikujące zamówienie (wracają w IPN) |
with_payer(Payer) | E-mail oraz imię i nazwisko klienta |
with_channel(str) | Płatność bezpośrednia wskazanym kanałem banku |
with_credit_card(bool) / with_paypal(bool) / with_paysafecard(bool) / with_installment(bool) / with_blik(bool) | Włączenie lub wyłączenie metod na bramce |
with_no_banks(bool) | Ukrycie listy banków |
with_blik_code(code: str, user_agent: str, user_ip: str) | Płatność BLIK Level 0 (kod 6-cyfrowy) |
with_blik_alias(alias: str, user_agent: str, user_ip: str) | Płatność aliasem BLIK OneClick |
with_register_blik_alias(BlikAliasRegistration) | Rejestracja aliasu OneClick przy płatności |
with_card_recurring(CardRecurringRegistration) | Rejestracja mandatu card recurring |
with_card_recurring_alias(str) | Obciążenie zapisanej karty (MIT) |
with_payout(PayoutInstruction) | Instrukcja wypłaty 1:1 |
with_efaktura(InvoiceDetails | None) | Płatność za eFakturę (KSeF) |
with_phone_number(phone: str, currency: str) | MB WAY |
Obsługa IPN
Powiadomienia IPN weryfikujesz przez IpnVerifier.construct_event() - nieprawidłowy podpis rzuca SignatureVerificationError:
from dpay import IpnEvent, IpnVerifier, SignatureVerificationError
def ipn_view(request):
try:
event = IpnVerifier.construct_event(request.body, "twoj_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 uznaje IPN za dostarczony wyłącznie, gdy treść odpowiedzi to dokładnie OK (stała IpnEvent.ACK). Kod HTTP nie jest sprawdzany. Upewnij się, że framework nie dokleja niczego do body.
Zawsze porównaj event.amount (string, np. "10.00") z kwotą zamówienia w Twojej bazie i przetwarzaj każdą transakcję tylko raz (IPN może przyjść wielokrotnie).
Szczegóły transakcji
transaction = dpay.payments.details("identyfikator-transakcji")
transaction.status # 'paid', 'created', 'processing', 'expired', 'captured'
transaction.is_paid # True dla paid i 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]
Zwroty
from dpay import Money
# zwrot pełny
refund = dpay.refunds.create("identyfikator-transakcji")
# zwrot częściowy z powodem
refund = dpay.refunds.create("identyfikator-transakcji", Money.pln(500), "reklamacja")
refund.is_accepted
Przed zwrotem możesz sprawdzić jego dostępność - metoda zwraca wynik biznesowy (nie rzuca wyjątku), także gdy zwrot jest niemożliwy:
availability = dpay.refunds.check_availability("identyfikator-transakcji", Money.pln(500))
if not availability.is_available:
availability.message # np. "Transakcja nie została opłacona"
availability.http_status # stabilny kod przyczyny, patrz tabela
| Kod HTTP | Przyczyna odmowy |
|---|---|
400 | Kwota przekracza dostępną kwotę zwrotu |
401 | Kanał płatności nie obsługuje zwrotów |
402 | Transakcja nie została opłacona |
406 | Niewystarczające saldo na pokrycie zwrotu |
409 | Wniosek o zwrot został już złożony |
410 | Transakcja została już zwrócona |
411 | Transakcje obciążeniowe nie podlegają zwrotom |
Banki
banks = dpay.banks.all() # wszystkie banki dpay.pl
banks = dpay.banks.for_service() # banki dostępne dla Twojego serwisu
banks[0].id
banks[0].name
BLIK - aliasy OneClick i Recurring
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
Rejestracja aliasu i płatność aliasem odbywają się przez rejestrację płatności (with_blik_code + with_register_blik_alias, płatność przez with_blik_alias).
Karty server-to-server
Dane karty szyfrujesz kluczem RSA pobieranym przed każdą próbą płatności:
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(...) # dane przeglądarki płatnika
result = dpay.cards.pay_otp(
transaction_id,
CardPaymentRequest.create(device_info)
.with_encrypted_card_data(encrypted)
.with_card_holder("Jan", "Kowalski")
.with_channel_id(31),
)
if result.is_success:
... # płatność przechwycona
elif result.requires_three_ds_form:
html = result.three_ds_form_html # wyrenderuj w przeglądarce płatnika
elif result.has_dcc_offer:
offer = result.dcc_offer # pokaż ofertę DCC płatnikowi
# decyzję odeślij ponownym pay_otp z .with_dcc_decision(DccDecision.ACCEPT)
Dostępne są także: pre_auth() (pre-autoryzacja), capture() i cancel() (przechwycenie i anulowanie), google_pay() oraz apple_pay(). Odrzucenie płatności przy HTTP 200 rzuca CardPaymentError z kodem błędu.
Szyfrowanie RSA jest zaimplementowane w czystym Pythonie, więc karty S2S nie wymagają żadnej dodatkowej biblioteki kryptograficznej.
Karty S2S wymagają zgodności PCI-DSS po stronie merchanta. Szczegóły przepływu znajdziesz w dokumentacji kart S2S.
Wypłaty 1:1
details = dpay.payouts.details(12345)
details.is_processed
details.net.to_decimal()
details.receiver
Obsługa błędów
Wszystkie wyjątki SDK dziedziczą po dpay.DPayError:
| Wyjątek | Kiedy |
|---|---|
TransportError | Błąd sieci - status płatności nieznany, użyj payments.details() |
AuthenticationError | 401 - błędny checksum lub secret hash |
InvalidRequestError | 400/422 - błędy walidacji (field_errors) |
AccessDeniedError | 403 - brak uprawnień do operacji |
NotFoundError | 404 - zasób nie istnieje |
RateLimitError | 429 - limit żądań (retry_after) |
PaymentRejectedError | Rejestracja odrzucona (np. błędny kod BLIK; transaction_id) |
CardPaymentError | Odrzucenie płatności kartowej (error_code) |
SignatureVerificationError | Nieprawidłowy podpis IPN |
ApiServerError | 5xx lub niepoprawna odpowiedź API |
DPayValueError | Niepoprawny argument (dziedziczy także po 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:
... # nie wiadomo, czy żądanie dotarło - zweryfikuj przez payments.details()
Testowanie integracji
SDK zawiera transport testowy, który kolejkuje odpowiedzi i nagrywa wysłane żądania, więc testy nie wymagają dostępu do sieci:
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"
Dla klienta asynchronicznego użyj MockAsyncHttpClient.