Przejdź do głównej zawartości

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]"
Nazwa pakietu

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",
)
OpcjaTypOpis
servicestrNazwa serwisu (Punktu Płatności) z panel.dpay.pl (wymagane)
secret_hashstrKlucz Hash (Secret Hash) do generowania sum kontrolnych (wymagane)
timeoutintTimeout żądań HTTP w sekundach (domyślnie 30)
http_clientHttpClientWłasny klient HTTP (testy, proxy)
base_urlsdict[str, str]Nadpisanie hostów API (klucze: api_payments, panel, gateway)

Klient udostępnia serwisy jako atrybuty:

SerwisZakres
dpay.paymentsRejestracja płatności, szczegóły transakcji
dpay.refundsZwroty i sprawdzanie dostępności zwrotu
dpay.banksLista banków pay-by-link
dpay.blikAliasy BLIK OneClick i Recurring
dpay.cardsPłatności kartowe server-to-server
dpay.payoutsSzczegół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

MetodaOpis
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)
Odpowiedź musi być dokładnie OK

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

informacja

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 HTTPPrzyczyna odmowy
400Kwota przekracza dostępną kwotę zwrotu
401Kanał płatności nie obsługuje zwrotów
402Transakcja nie została opłacona
406Niewystarczające saldo na pokrycie zwrotu
409Wniosek o zwrot został już złożony
410Transakcja została już zwrócona
411Transakcje 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.

informacja

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ątekKiedy
TransportErrorBłąd sieci - status płatności nieznany, użyj payments.details()
AuthenticationError401 - błędny checksum lub secret hash
InvalidRequestError400/422 - błędy walidacji (field_errors)
AccessDeniedError403 - brak uprawnień do operacji
NotFoundError404 - zasób nie istnieje
RateLimitError429 - limit żądań (retry_after)
PaymentRejectedErrorRejestracja odrzucona (np. błędny kod BLIK; transaction_id)
CardPaymentErrorOdrzucenie płatności kartowej (error_code)
SignatureVerificationErrorNieprawidłowy podpis IPN
ApiServerError5xx lub niepoprawna odpowiedź API
DPayValueErrorNiepoprawny 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.


Więcej informacji