Java SDK
✅ Available - version 0.1.0
Official Java library for integrating with the dpay.pl API. It automates checksum generation, IPN notification verification, and API calls: payment registration, transaction details, refunds, bank lists, BLIK aliases, server-to-server card payments, and 1:1 payouts. Amounts are represented by the Money object (minor units as long), and API responses are mapped to typed objects. Exceptions are unchecked - the compiler does not force try/catch.
Requirements
- Java 11 or newer (tested up to Java 25)
- Maven or Gradle
The library has no runtime dependencies - HTTP transport uses java.net.http and cryptography uses javax.crypto. The SDK is intended for backend (server) use; Android is not supported.
Installation
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>
The package is published on Maven Central, and the source code is available on GitHub.
Configuration
The simplest way is to pass the credentials from the dpay.pl Panel:
import pl.dpay.sdk.DPayClient;
DPayClient dpay = new DPayClient("service_name", "your_secret_hash");
DPayConfig gives you full control:
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("service_name", "your_secret_hash")
.withTimeout(Duration.ofSeconds(30))
.withBaseUrl(ApiHost.PANEL, "https://panel.dpay.pl"));
| Option | Type | Description |
|---|---|---|
service | String | Service name (Payment Point) from panel.dpay.pl (required) |
secretHash | String | Hash key (Secret Hash) for checksum generation (required) |
withTimeout | Duration | HTTP request timeout (default 30 s) |
withHttpClient | HttpClient | Custom transport (proxy, retry, tests) |
withBaseUrl | ApiHost, String | Override an API host (API_PAYMENTS, PANEL, GATEWAY) |
The client is immutable and thread-safe. It exposes services as methods:
| 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 |
Amounts - the Money object
Every amount in the SDK is a pl.dpay.sdk.Money object, internally holding minor units as long. The SDK takes care of the correct amount format for each endpoint (decimal amount or minor units), so you do not need to remember which endpoint expects which format.
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 (minor units)
money.toDecimal(); // "10.50"
Registering a payment
You build the request with RegisterPaymentRequest - required fields in create(), optional ones via withX() setters. The checksum and the transactionType field are added automatically:
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("service_name", "your_secret_hash");
RegisteredPayment payment = dpay.payments().register(
RegisterPaymentRequest.create(
Money.pln(1050),
TransactionType.TRANSFERS,
new ReturnUrls(
"https://yourshop.com/success",
"https://yourshop.com/failure",
"https://yourshop.com/ipn"))
.withDescription("Order #1234")
.withCustom("order-1234")
.withPayer(Payer.create().withEmail("customer@example.com")));
if (payment.getRedirectUrl() != null) {
response.sendRedirect(payment.getRedirectUrl());
return;
}
if (payment.isPaid()) {
// 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 |
|---|---|
withDescription(String) | Transaction description shown to the customer |
withCustom(String) | Your own order-identifying data (returned in IPN) |
withPayer(Payer) | Customer e-mail, first and last name |
withChannel(String) | Direct payment via a specific bank channel |
withCreditCard(boolean) / withPaypal(boolean) / withPaysafecard(boolean) / withInstallment(boolean) / withBlik(boolean) | Enable or disable methods on the gateway |
withNoBanks(boolean) | Hide the bank list |
withBlikCode(String code, String userAgent, String userIp) | BLIK Level 0 payment (6-digit code) |
withBlikAlias(String alias, String userAgent, String userIp) | Payment with a BLIK OneClick alias |
withRegisterBlikAlias(BlikAliasRegistration) | Register a OneClick alias during payment |
withCardRecurring(CardRecurringRegistration) | Register a card recurring mandate |
withCardRecurringAlias(String) | Charge a stored card (MIT) |
withPayout(PayoutInstruction) | 1:1 payout instruction |
withEfaktura(InvoiceDetails) | Payment for an e-invoice (KSeF) |
withPhoneNumber(String phone, Currency currency) | MB WAY |
IPN handling
You verify IPN notifications with IpnVerifier.constructEvent() - an invalid signature throws 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, "your_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 treats an IPN as delivered only when the response body is exactly OK (the IpnEvent.ACK constant). The HTTP status is not checked. Make sure your framework does not append anything to the body.
Always compare event.getAmount() (a string, e.g. "10.00") with the order amount in your database, and process each transaction only once (an IPN may arrive multiple times).
Transaction details
import pl.dpay.sdk.payment.Transaction;
Transaction transaction = dpay.payments().details("transaction-id");
transaction.getStatus(); // TransactionStatus: PAID, CREATED, PROCESSING, EXPIRED, CAPTURED, UNKNOWN
transaction.getStatusValue(); // raw string from the API
transaction.isPaid(); // true for paid and captured
transaction.getValue().toDecimal(); // "29.99"
transaction.getRefundedAmount(); // Money
transaction.getAvailableRefundAmount(); // Money
transaction.isFullyRefunded(); // boolean
transaction.getRefunds(); // List<TransactionRefund>
Statuses from API responses are enums with an UNKNOWN value - an unknown value does not break deserialization, and the original string is always available via getStatusValue().
Refunds
import pl.dpay.sdk.Money;
import pl.dpay.sdk.refund.Refund;
// full refund
Refund refund = dpay.refunds().create("transaction-id");
// partial refund with a reason
Refund refund = dpay.refunds().create("transaction-id", Money.pln(500), "complaint");
refund.isAccepted();
Before refunding you can check its availability - the method returns a business result (it does not throw), even when the refund is not possible:
import pl.dpay.sdk.refund.RefundAvailability;
RefundAvailability availability =
dpay.refunds().checkAvailability("transaction-id", Money.pln(500));
if (!availability.isAvailable()) {
availability.getMessage(); // e.g. "Transaction has not been paid"
availability.getHttpStatus(); // stable reason code, see table
}
| HTTP code | Reason for refusal |
|---|---|
400 | Amount exceeds the available refund amount |
401 | Payment channel does not support refunds |
402 | Transaction has not been paid |
406 | Insufficient balance to cover the refund |
409 | A refund request has already been submitted |
410 | Transaction has already been refunded |
411 | Charge transactions are not refundable |
Banks
import java.util.List;
import pl.dpay.sdk.bank.Bank;
List<Bank> banks = dpay.banks().all(); // all dpay.pl banks
List<Bank> banks = dpay.banks().forService(); // banks available for your service
banks.get(0).getId();
banks.get(0).getName();
BLIK - OneClick and Recurring aliases
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();
Registering an alias and paying with an alias happen through payment registration (withBlikCode + withRegisterBlikAlias, payment via withBlikAlias).
Cards server-to-server
You encrypt card data with an RSA key fetched before each payment attempt:
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(/* payer browser data */);
CardPaymentResult result = dpay.cards().payOtp(transactionId,
CardPaymentRequest.create(deviceInfo)
.withEncryptedCardData(encrypted)
.withCardHolder("Jan", "Kowalski")
.withChannelId(31));
if (result.isSuccess()) {
// payment captured
} else if (result.requiresThreeDsForm()) {
String html = result.getThreeDsFormHtml(); // render in the payer's browser
} else if (result.hasDccOffer()) {
result.getDccOffer(); // show the DCC offer to the payer
// send the decision back with another payOtp using .withDccDecision(DccDecision.ACCEPT)
}
Also available: preAuth() (pre-authorization), capture() and cancel(), googlePay() and applePay(). A payment rejected with HTTP 200 throws CardPaymentException with an error code.
S2S cards require PCI-DSS compliance on the merchant side. Flow details are in the S2S cards documentation.
1:1 payouts
import pl.dpay.sdk.payout.PayoutDetails;
PayoutDetails details = dpay.payouts().details(12345);
details.isProcessed();
details.getNet().toDecimal();
details.getReceiver();
Error handling
Every SDK exception extends pl.dpay.sdk.exception.DPayException and is unchecked - the compiler does not force try/catch, so you catch where you can actually handle the error:
| Exception | When |
|---|---|
TransportException | Network error - payment status unknown, use payments().details() |
AuthenticationException | 401 - invalid checksum or secret hash |
InvalidRequestException | 400/422 - validation errors (getFieldErrors()) |
NotFoundException | 404 - resource does not exist |
RateLimitException | 429 - request limit (getRetryAfter()) |
PaymentRejectedException | Registration rejected (e.g. invalid BLIK code; getTransactionId()) |
CardPaymentException | Card payment rejected (getErrorCode()) |
SignatureVerificationException | Invalid IPN signature |
CardEncryptionException | Card data encryption error |
ApiServerException | 5xx or an invalid API response |
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) {
// it is unknown whether the request arrived - verify via payments().details()
}
An invalid argument passed to the SDK (e.g. a malformed amount) throws IllegalArgumentException.
Testing integration
MockHttpClient from the pl.dpay.sdk.testing package lets you test integration without a network:
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() contains the exact request body sent