Skip to main content

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"));
OptionTypeDescription
serviceStringService name (Payment Point) from panel.dpay.pl (required)
secretHashStringHash key (Secret Hash) for checksum generation (required)
withTimeoutDurationHTTP request timeout (default 30 s)
withHttpClientHttpClientCustom transport (proxy, retry, tests)
withBaseUrlApiHost, StringOverride an API host (API_PAYMENTS, PANEL, GATEWAY)

The client is immutable and thread-safe. It exposes services as methods:

ServiceScope
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

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

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

info

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 codeReason for refusal
400Amount exceeds the available refund amount
401Payment channel does not support refunds
402Transaction has not been paid
406Insufficient balance to cover the refund
409A refund request has already been submitted
410Transaction has already been refunded
411Charge 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.

info

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:

ExceptionWhen
TransportExceptionNetwork error - payment status unknown, use payments().details()
AuthenticationException401 - invalid checksum or secret hash
InvalidRequestException400/422 - validation errors (getFieldErrors())
NotFoundException404 - resource does not exist
RateLimitException429 - request limit (getRetryAfter())
PaymentRejectedExceptionRegistration rejected (e.g. invalid BLIK code; getTransactionId())
CardPaymentExceptionCard payment rejected (getErrorCode())
SignatureVerificationExceptionInvalid IPN signature
CardEncryptionExceptionCard data encryption error
ApiServerException5xx 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

More information