Java SDK
✅ Dostępne - wersja 0.1.0
Oficjalna biblioteka Javy 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 long), a odpowiedzi API mapowane na typowane obiekty. Wyjątki są typu unchecked - kompilator nie zmusza do try/catch.
Wymagania
- Java 11 lub nowsza (testowane do Javy 25)
- Maven lub Gradle
Biblioteka nie ma żadnych zależności runtime - transport HTTP korzysta z java.net.http, a kryptografia z javax.crypto. SDK jest przeznaczone dla backendu (serwera); nie jest wspierane na Androidzie.
Instalacja
Gradle:
implementation 'pl.dpay:dpay-java-sdk:0.1.0'
Maven:
<dependency>
<groupId>pl.dpay</groupId>
<artifactId>dpay-java-sdk</artifactId>
<version>0.1.0</version>
</dependency>
Pakiet jest publikowany na Maven Central, a kod źródłowy dostępny na GitHub.
Konfiguracja
Najprościej podając dane z Panelu dpay.pl:
import pl.dpay.sdk.DPayClient;
DPayClient dpay = new DPayClient("nazwa_serwisu", "twoj_secret_hash");
Pełną kontrolę daje DPayConfig:
import java.time.Duration;
import pl.dpay.sdk.ApiHost;
import pl.dpay.sdk.DPayClient;
import pl.dpay.sdk.DPayConfig;
DPayClient dpay = new DPayClient(
DPayConfig.create("nazwa_serwisu", "twoj_secret_hash")
.withTimeout(Duration.ofSeconds(30))
.withBaseUrl(ApiHost.PANEL, "https://panel.dpay.pl"));
| Opcja | Typ | Opis |
|---|---|---|
service | String | Nazwa serwisu (Punktu Płatności) z panel.dpay.pl (wymagane) |
secretHash | String | Klucz Hash (Secret Hash) do generowania sum kontrolnych (wymagane) |
withTimeout | Duration | Timeout żądań HTTP (domyślnie 30 s) |
withHttpClient | HttpClient | Własny transport (proxy, retry, testy) |
withBaseUrl | ApiHost, String | Nadpisanie hosta API (API_PAYMENTS, PANEL, GATEWAY) |
Klient jest niemutowalny i bezpieczny wątkowo. Serwisy udostępnia jako metody:
| 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 |
Kwoty - obiekt Money
Wszystkie kwoty w SDK to obiekt pl.dpay.sdk.Money, wewnętrznie przechowujący grosze jako long. 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.
import pl.dpay.sdk.Currency;
import pl.dpay.sdk.Money;
Money.pln(1050); // 10.50 PLN
Money.of(500, Currency.EUR); // 5.00 EUR
Money.fromDecimal("10.50", Currency.PLN);
money.getMinor(); // 1050 (grosze)
money.toDecimal(); // "10.50"
Rejestracja płatności
Żądanie budujesz obiektem RegisterPaymentRequest - pola wymagane w create(), opcjonalne przez settery withX(). Suma kontrolna i pole transactionType są dokładane automatycznie:
import pl.dpay.sdk.DPayClient;
import pl.dpay.sdk.Money;
import pl.dpay.sdk.payment.Payer;
import pl.dpay.sdk.payment.RegisterPaymentRequest;
import pl.dpay.sdk.payment.RegisteredPayment;
import pl.dpay.sdk.payment.ReturnUrls;
import pl.dpay.sdk.payment.TransactionType;
DPayClient dpay = new DPayClient("nazwa_serwisu", "twoj_secret_hash");
RegisteredPayment payment = dpay.payments().register(
RegisterPaymentRequest.create(
Money.pln(1050),
TransactionType.TRANSFERS,
new ReturnUrls(
"https://twojsklep.pl/sukces",
"https://twojsklep.pl/blad",
"https://twojsklep.pl/ipn"))
.withDescription("Zamówienie #1234")
.withCustom("order-1234")
.withPayer(Payer.create().withEmail("klient@example.com")));
if (payment.getRedirectUrl() != null) {
response.sendRedirect(payment.getRedirectUrl());
return;
}
if (payment.isPaid()) {
// 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 |
|---|---|
withDescription(String) | Opis transakcji widoczny dla klienta |
withCustom(String) | Własne dane identyfikujące zamówienie (wracają w IPN) |
withPayer(Payer) | E-mail oraz imię i nazwisko klienta |
withChannel(String) | Płatność bezpośrednia wskazanym kanałem banku |
withCreditCard(boolean) / withPaypal(boolean) / withPaysafecard(boolean) / withInstallment(boolean) / withBlik(boolean) | Włączenie lub wyłączenie metod na bramce |
withNoBanks(boolean) | Ukrycie listy banków |
withBlikCode(String code, String userAgent, String userIp) | Płatność BLIK Level 0 (kod 6-cyfrowy) |
withBlikAlias(String alias, String userAgent, String userIp) | Płatność aliasem BLIK OneClick |
withRegisterBlikAlias(BlikAliasRegistration) | Rejestracja aliasu OneClick przy płatności |
withCardRecurring(CardRecurringRegistration) | Rejestracja mandatu card recurring |
withCardRecurringAlias(String) | Obciążenie zapisanej karty (MIT) |
withPayout(PayoutInstruction) | Instrukcja wypłaty 1:1 |
withEfaktura(InvoiceDetails) | Płatność za eFakturę (KSeF) |
withPhoneNumber(String phone, Currency currency) | MB WAY |
Obsługa IPN
Powiadomienia IPN weryfikujesz przez IpnVerifier.constructEvent() - nieprawidłowy podpis rzuca SignatureVerificationException:
import pl.dpay.sdk.exception.SignatureVerificationException;
import pl.dpay.sdk.ipn.IpnEvent;
import pl.dpay.sdk.ipn.IpnVerifier;
try {
IpnEvent event = IpnVerifier.constructEvent(requestBody, "twoj_secret_hash");
if (event.isTransfer()) {
markOrderAsPaid(event.getId(), event.getAmount());
}
if (event.isCapture()) {
markOrderAsCaptured(event.getId(), event.getAmount(), event.getCapturePaymentId());
}
response.setStatus(200);
response.getWriter().print(IpnEvent.ACK);
} catch (SignatureVerificationException e) {
response.setStatus(400);
response.getWriter().print("Invalid signature");
}
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.getAmount() (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
import pl.dpay.sdk.payment.Transaction;
Transaction transaction = dpay.payments().details("identyfikator-transakcji");
transaction.getStatus(); // TransactionStatus: PAID, CREATED, PROCESSING, EXPIRED, CAPTURED, UNKNOWN
transaction.getStatusValue(); // surowy string z API
transaction.isPaid(); // true dla paid i captured
transaction.getValue().toDecimal(); // "29.99"
transaction.getRefundedAmount(); // Money
transaction.getAvailableRefundAmount(); // Money
transaction.isFullyRefunded(); // boolean
transaction.getRefunds(); // List<TransactionRefund>
Statusy z odpowiedzi API są enumami z wartością UNKNOWN - nieznana wartość nie psuje deserializacji, a oryginalny string zawsze dostępny przez getStatusValue().
Zwroty
import pl.dpay.sdk.Money;
import pl.dpay.sdk.refund.Refund;
// zwrot pełny
Refund refund = dpay.refunds().create("identyfikator-transakcji");
// zwrot częściowy z powodem
Refund refund = dpay.refunds().create("identyfikator-transakcji", Money.pln(500), "reklamacja");
refund.isAccepted();
Przed zwrotem możesz sprawdzić jego dostępność - metoda zwraca wynik biznesowy (nie rzuca wyjątku), także gdy zwrot jest niemożliwy:
import pl.dpay.sdk.refund.RefundAvailability;
RefundAvailability availability =
dpay.refunds().checkAvailability("identyfikator-transakcji", Money.pln(500));
if (!availability.isAvailable()) {
availability.getMessage(); // np. "Transakcja nie została opłacona"
availability.getHttpStatus(); // 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
import java.util.List;
import pl.dpay.sdk.bank.Bank;
List<Bank> banks = dpay.banks().all(); // wszystkie banki dpay.pl
List<Bank> banks = dpay.banks().forService(); // banki dostępne dla Twojego serwisu
banks.get(0).getId();
banks.get(0).getName();
BLIK - aliasy OneClick i Recurring
import pl.dpay.sdk.blik.BlikAlias;
import pl.dpay.sdk.blik.BlikRecurringStatus;
BlikAlias alias = dpay.blik().alias("DPAY.UID.123456.abc12345");
alias.isActive();
alias.getApps();
dpay.blik().unregisterAlias("DPAY.UID.123456.abc12345");
BlikRecurringStatus status = dpay.blik().recurringStatus("PAYID-...");
status.getRegistration();
Rejestracja aliasu i płatność aliasem odbywają się przez rejestrację płatności (withBlikCode + withRegisterBlikAlias, płatność przez withBlikAlias).
Karty server-to-server
Dane karty szyfrujesz kluczem RSA pobieranym przed każdą próbą płatności:
import pl.dpay.sdk.card.CardData;
import pl.dpay.sdk.card.CardEncryptor;
import pl.dpay.sdk.card.CardPaymentRequest;
import pl.dpay.sdk.card.CardPaymentResult;
import pl.dpay.sdk.card.DccDecision;
import pl.dpay.sdk.payment.DeviceInfo;
String publicKey = dpay.cards().publicKey();
String encrypted = new CardEncryptor().encrypt(
new CardData("4111111111111111", "123", "12/30"),
transactionId,
publicKey);
DeviceInfo deviceInfo = DeviceInfo.create(/* dane przeglądarki płatnika */);
CardPaymentResult result = dpay.cards().payOtp(transactionId,
CardPaymentRequest.create(deviceInfo)
.withEncryptedCardData(encrypted)
.withCardHolder("Jan", "Kowalski")
.withChannelId(31));
if (result.isSuccess()) {
// płatność przechwycona
} else if (result.requiresThreeDsForm()) {
String html = result.getThreeDsFormHtml(); // wyrenderuj w przeglądarce płatnika
} else if (result.hasDccOffer()) {
result.getDccOffer(); // pokaż ofertę DCC płatnikowi
// decyzję odeślij ponownym payOtp z .withDccDecision(DccDecision.ACCEPT)
}
Dostępne są także: preAuth() (pre-autoryzacja), capture() i cancel() (przechwycenie i anulowanie), googlePay() oraz applePay(). Odrzucenie płatności przy HTTP 200 rzuca CardPaymentException z kodem błędu.
Karty S2S wymagają zgodności PCI-DSS po stronie merchanta. Szczegóły przepływu znajdziesz w dokumentacji kart S2S.
Wypłaty 1:1
import pl.dpay.sdk.payout.PayoutDetails;
PayoutDetails details = dpay.payouts().details(12345);
details.isProcessed();
details.getNet().toDecimal();
details.getReceiver();
Obsługa błędów
Wszystkie wyjątki SDK dziedziczą po pl.dpay.sdk.exception.DPayException i są typu unchecked - kompilator nie wymusza try/catch, łapiesz tam, gdzie faktycznie obsłużysz błąd:
| Wyjątek | Kiedy |
|---|---|
TransportException | Błąd sieci - status płatności nieznany, użyj payments().details() |
AuthenticationException | 401 - błędny checksum lub secret hash |
InvalidRequestException | 400/422 - błędy walidacji (getFieldErrors()) |
NotFoundException | 404 - zasób nie istnieje |
RateLimitException | 429 - limit żądań (getRetryAfter()) |
PaymentRejectedException | Rejestracja odrzucona (np. błędny kod BLIK; getTransactionId()) |
CardPaymentException | Odrzucenie płatności kartowej (getErrorCode()) |
SignatureVerificationException | Nieprawidłowy podpis IPN |
CardEncryptionException | Błąd szyfrowania danych karty |
ApiServerException | 5xx lub niepoprawna odpowiedź API |
import pl.dpay.sdk.exception.ApiException;
import pl.dpay.sdk.exception.InvalidRequestException;
import pl.dpay.sdk.exception.TransportException;
try {
RegisteredPayment payment = dpay.payments().register(request);
} catch (InvalidRequestException e) {
e.getFieldErrors(); // Map<String, List<String>>
} catch (ApiException e) {
e.getHttpStatus();
e.getErrorCode();
} catch (TransportException e) {
// nie wiadomo, czy żądanie dotarło - zweryfikuj przez payments().details()
}
Niepoprawny argument przekazany do SDK (np. błędny format kwoty) rzuca IllegalArgumentException.
Testowanie integracji
MockHttpClient z pakietu pl.dpay.sdk.testing pozwala testować integrację bez sieci:
import pl.dpay.sdk.DPayClient;
import pl.dpay.sdk.DPayConfig;
import pl.dpay.sdk.testing.MockHttpClient;
MockHttpClient transport = new MockHttpClient();
transport.queueJson(200, "{\"transactionId\":\"tx-1\",\"msg\":\"https://secure.dpay.pl/pay/1\"}");
DPayClient dpay = new DPayClient(
DPayConfig.create("test", "test").withHttpClient(transport));
RegisteredPayment payment = dpay.payments().register(request);
// transport.getLastRequestBody() zawiera dokładne wysłane body