BLIK recurring payments (subscriptions)
BLIK recurring payments (called Recurring Payments by BLIK) let you charge a customer in subsequent periods without a BLIK code, which makes them suitable for subscriptions, plans and bills. The customer approves the recurring payment once in their banking app, and you then initiate subsequent charges from your server using its alias.
POST/api/v1_0/payments/registerComplete registration contract: parameters, checksum, and error codes.Full contract in the API Reference
How it works
- Register a recurring payment - a transaction with a BLIK code and a
recurring_registrationobject, with an amount of 0 (consent only) or an initial payment, for example for the first period. The customer sees the name and terms of the recurring payment in their banking app and, with a single confirmation, pays the initial payment and approves the recurring payment. - Charges - subsequent payments with the
recurring_aliasfield, sent by your server without a BLIK code. - Result and retry - a successful charge is confirmed by an IPN. A charge declined for a temporary reason (for example insufficient funds) can be retried.
- Management - checking the status, cancelling, and notifications about changes (for example when the customer cancels the recurring payment in their banking app).
The recurring_registration and recurring_alias fields and the /payments/recurring/* endpoints are shared by payment methods. The currently supported method is BLIK (methods: ["blik"]), and the registration requires a BLIK code.
Recurring payment models
You choose the model at registration. It determines who approves subsequent charges and which terms the customer accepts in their bank. dpay enables each model for your Payment Point.
| Model | Who approves a charge | When to use it |
|---|---|---|
A | The customer's bank, automatically - when the charge meets the terms accepted at registration (fixed amount, frequency, total limit, period) | A fixed amount on a fixed schedule, for example a PLN 59.99 monthly plan |
M | The customer - confirms every charge in the banking app | Variable amounts or dates when the customer should approve each payment |
O | Nobody - the bank charges the account without customer involvement if BLIK qualifies the charge as a merchant-initiated transaction (MIT) | Continuous services with variable amounts, for example utility bills, usage fees, surcharges after a service |
Model A - automatic
- At registration you provide the full set of terms: frequency (
frequency), the amount of every charge (limit_amt), the total limit (tot_limit_amt), the first charge date (init_date) and the expiration date (expiration_date). The customer accepts them in the banking app. - The amount is fixed - every charge must equal
limit_amt. - dpay rejects a charge with a different amount, one exceeding the total limit, or one sent before
init_date- before it reaches BLIK. - The bank checks the frequency. It declines a charge that does not meet the registration terms with
AUTOCONF_REQ_NOT_MET(withno_delay: falseit may ask the customer to confirm instead).
Model M - customer confirmation
- The customer confirms every charge in the banking app, so you learn the result only after their decision - which can take up to 72 hours.
no_delay: trueis not allowed in this model.frequency,init_dateandexpiration_dateare optional - they are sent to the bank with the invitation as information about the recurring payment.- The
limit_amt,tot_limit_amtandis_limit_amt_fixedlimits are optional and are not sent to the bank - dpay enforces them on every charge.
Model O - no customer involvement (MIT)
-
At registration you provide the label, the alias, the terms link and optionally the expiration date.
frequencyand limits are prohibited in this model - their absence is how the bank recognizes model O. -
A single charge can be at most PLN 2000 and must fall within an amount range active for your Payment Point:
Range Amounts 1 PLN 0 - 400.00 2 PLN 400.01 - 1000.00 3 PLN 1000.01 - 2000.00 -
dpay rejects a charge above PLN 2000 or in an inactive range (HTTP 400) before it reaches BLIK.
-
When BLIK does not qualify a charge as MIT (for example after a range has been temporarily disabled), the bank declines it with
SEC_DECLINED. Withno_delay: falsethe bank may ask the customer to confirm in the app instead.
- Continuous services only: subscriptions, bills (electricity, gas, internet), usage fees, surcharges after a service, rides and parking. Model O is not intended for one-off purchases (online stores, food delivery).
- Configuration at BLIK: dpay submits your Payment Point to BLIK together with the amount ranges and the materials from your request showing the registration flow (mockups, a recording or test access). BLIK reviews the flow and, after a positive review, configures the Payment Point - usually within about 3 business days.
- Amount ranges are granted by dpay after a risk assessment. A model O registration requires at least one active range.
- Amounts must not be split into smaller charges to fit a range.
Requirements
- An active Payment Point with BLIK enabled and BLIK recurring payments activated. Submit a request in the dpay panel: Payment points → service → Payment methods → BLIK - recurring payments. You choose the models (for model O also the amount ranges), describe the service, provide the terms and materials showing the consent flow, and accept the terms. We enable models A and M after reviewing the request, and model O additionally after the configuration at BLIK (see above). We will e-mail you our decision.
- The terms of the recurring payment (terms of service or an agreement) at a stable URL - see terms link.
- Secure server-side storage for the recurring payment alias, associated with the customer's account.
- Access to the customer's IP address and User-Agent during registration, as required by applicable regulations.
- A registration flow and customer communication that meet the obligations towards the customer and the BLIK requirements for the purchase flow (method name, list of banks, payment details before checkout).
You can build and test the integration without a request - in test mode all models are available and the responses reproduce BLIK behavior.
Endpoint
POST https://api-payments.dpay.pl/api/v1_0/payments/register
Content-Type: application/json
Step 1: Register a recurring payment
Registration is a transaction with a BLIK code that carries the invitation to a recurring payment. Send it only after the customer has explicitly chosen a recurring payment on your website. The registration amount can be:
- 0 - the customer is not charged and only approves the recurring payment,
- more than 0 (initial payment) - for example for the first subscription period or service activation. On a single screen in the banking app, with a single PIN, the customer pays the initial payment and approves the recurring payment.
Combining the first payment with the consent is one step for the customer instead of two - BLIK recommends this flow because the customer will not close the app believing they have already enabled recurring payments. The initial payment is a regular BLIK payment confirmed with a PIN: the model O amount ranges and the PLN 2000 cap do not apply to it, and it does not count towards tot_limit_amt. You refund it like any other payment - see refunds.
If the customer's bank does not support BLIK recurring payments, BLIK declines the whole transaction (code ER_PAYID_UNHANDLED) - the customer does not pay the initial payment without an active recurring payment.
Request parameters
| Field | Type | Required | Description |
|---|---|---|---|
transactionType | string | Yes | "transfers" |
service | string | Yes | Service name from the panel |
value | string | Yes | 0 - consent to the recurring payment only; more than 0 - an initial payment charged together with the consent |
url_success | string | Yes | URL after successful registration |
url_fail | string | Yes | URL after failed registration |
url_ipn | string | No | URL for IPN notifications; without it no IPN is sent (you get the result by webhook) |
checksum | string | Yes | SHA-256 checksum |
blik_code | string | Yes | 6-digit BLIK code |
user_ip | string | Yes | Customer IP address |
user_agent | string | Yes | Customer User-Agent header |
recurring_registration | object | Yes | Recurring payment terms (below) |
alias_ipn_url | string | No | URL for change notifications (max. 500 characters); when omitted, notifications go to url_ipn |
description | string | No | Description of the registration transaction, passed to BLIK (first 35 characters). Without it, BLIK receives the recurring payment name (label) |
recurring_registration object
Which fields are required depends on the model. A prohibited field causes a validation error (HTTP 422).
| Field | Type | A | M | O | Description |
|---|---|---|---|---|---|
label | string | Yes | Yes | Yes | Name of the recurring payment shown to the customer in the banking app (max. 35 characters - the BLIK limit), for example "Premium plan". How to build the label: BLIK requirements |
model | string | Yes | Yes | Yes | Model: "A", "M" or "O" |
terms_url | string | Yes | Yes | Yes | Link to the recurring payment terms the customer accepts (max. 2048 characters) - see terms link |
terms_version | string | No | No | No | Version of the terms, for example "2026-09" (max. 64 characters) |
alias | string | Recommended | Recommended | Recommended | Your recurring payment identifier (max. 128 characters), unique within the service - see the note below |
methods | array | No | No | No | Methods the customer pays the recurring payment with. Currently only ["blik"] (default) |
frequency | string | Yes | No | Prohibited | Frequency: a number from 1 to 999 and a unit D (days), W (weeks), M (months) or Y (years), for example "1M", "2W", "30D" |
limit_amt | integer | Yes | No | Prohibited | Amount in grosze (for example 5999 = PLN 59.99). Model A: the amount of every charge. Model M: the maximum charge amount (or the exact amount with is_limit_amt_fixed: true) |
tot_limit_amt | integer | Yes | No | Prohibited | Total limit of recurring charges in grosze (excluding the initial payment) |
is_limit_amt_fixed | boolean | No | No | Prohibited | Model M: true - every charge must equal limit_amt, false - limit_amt is the maximum amount. In model A the amount is always fixed - omit the field or send true |
init_date | string | Yes | No | - | Date of the first recurring charge (YYYY-MM-DD, today or later) - when the initial payment covers the first period, send the start of the next one. In model A a charge before this date is rejected. Ignored in model O |
expiration_date | string | Yes | No | No | Expiration date of the recurring payment (YYYY-MM-DD, after today, at most 10 years from today). Without a date it is valid until cancelled |
Send your own identifier in recurring_registration.alias (for example "SUB-12345") and store it on your server, associated with the customer's account. You need it for charges, status checks and cancellation. If you omit it, dpay assigns an alias itself and returns it in the response in additionalInfo.recurring_registration.alias - but if the response does not reach you (for example after a timeout), you will not learn it. Registering a second active recurring payment with the same alias is rejected.
Terms link
The terms_url field is required. It links to the document with the recurring payment terms the customer accepts at registration - terms of service, an agreement or a price list. dpay stores it together with terms_version and returns it in the recurring payment status. In a complaint, it is the evidence of the terms the customer agreed to.
-
The content behind the link must not change. When you change the terms, publish a new document at a new address (with a new
terms_version) and use it for new registrations. Leave the previous document unchanged - customers registered earlier accepted its content. -
It can be an agreement generated for a specific customer. A link to an individual document often contains an agreement identifier, checksums or a UUID. dpay accepts links of up to 2048 characters, for example:
https://myshop.com/agreements/9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08/2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae/0b5c3e2a-7f7d-4f1e-9d1c-3a2b1c0d9e8f -
Availability - the document should open without logging in and stay available for at least 13 months after the last charge (the time the customer has to file a complaint).
Checksum generation
The checksum is generated in the same way as for a standard payment:
sha256({service}|{SecretHash}|{value}|{url_success}|{url_fail}|{url_ipn})
The amount in the checksum is normalized to two decimal places - for a registration with value: 0, hash "0.00".
Example request
cURL (model A, first month in the initial payment)
The customer pays PLN 59.99 for October at registration, and subsequent PLN 59.99 charges follow monthly from 1 November.
curl -X POST https://api-payments.dpay.pl/api/v1_0/payments/register \
-H "Content-Type: application/json" \
-d '{
"transactionType": "transfers",
"service": "abc123",
"value": "59.99",
"url_success": "https://myshop.com/success",
"url_fail": "https://myshop.com/error",
"url_ipn": "https://myshop.com/api/ipn",
"checksum": "e3b0c44298fc1c149afb...",
"blik_code": "123456",
"user_ip": "192.168.1.100",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"description": "Premium plan",
"recurring_registration": {
"label": "Premium plan",
"alias": "SUB-1234567890",
"model": "A",
"frequency": "1M",
"limit_amt": 5999,
"tot_limit_amt": 71988,
"init_date": "2026-11-01",
"expiration_date": "2027-09-30",
"terms_url": "https://myshop.com/terms/2026-09",
"terms_version": "2026-09"
}
}'
PHP (model O, consent only, individual agreement)
<?php
$service = getenv('DPAY_SERVICE');
$secretHash = getenv('DPAY_SECRET_HASH');
$value = '0.00';
$urlSuccess = 'https://myshop.com/success';
$urlFail = 'https://myshop.com/error';
$urlIpn = 'https://myshop.com/api/ipn';
$checksum = hash('sha256',
$service . '|' . $secretHash . '|' . $value . '|' .
$urlSuccess . '|' . $urlFail . '|' . $urlIpn
);
// Agreement generated for the customer - the content at this address must not change
$agreementUrl = 'https://myshop.com/agreements/' . $agreement->uuid . '/' . $agreement->checksum;
$payload = json_encode([
'transactionType' => 'transfers',
'service' => $service,
'value' => 0,
'url_success' => $urlSuccess,
'url_fail' => $urlFail,
'url_ipn' => $urlIpn,
'checksum' => $checksum,
'blik_code' => $_POST['blik_code'],
'user_ip' => $_SERVER['REMOTE_ADDR'],
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
'description' => 'Energy bill',
'recurring_registration' => [
'label' => 'Energy - contract 2026/123',
'alias' => 'ENERGY-' . $customerId,
'model' => 'O',
'terms_url' => $agreementUrl,
'terms_version' => '2026/123',
],
]);
$ch = curl_init('https://api-payments.dpay.pl/api/v1_0/payments/register');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
In model M the registration object can contain only the required fields, or additionally information for the customer and limits enforced by dpay:
{
"label": "Account top-ups",
"alias": "TOPUP-1234567890",
"model": "M",
"frequency": "1M",
"limit_amt": 20000,
"is_limit_amt_fixed": false,
"terms_url": "https://myshop.com/terms/2026-09"
}
API response
{
"error": false,
"msg": "Internal processing",
"status": true,
"transactionId": "abc-def-123-456",
"additionalInfo": {
"recurring_registration": {
"alias": "SUB-1234567890",
"methods": ["blik"]
}
}
}
The status field is a boolean (true/false). Store transactionId (the identifier of the registration transaction) and additionalInfo.recurring_registration.alias - the alias you send in the recurring_alias field with charges. After this response, the customer approves the recurring payment in the banking app.
If the registration is declined immediately, the response is {"error": true, "msg": "Transaction canceled", "status": false, ...} and the decline code is in additionalInfo.error.
Registration result
- Approval - you receive a change notification with
change_type: "ALIAS_REGISTER", and the registration transaction moves topaid(you receive a standard IPN for it with the initial payment amount or 0). From now on you can charge the customer. - Rejection or no approval - the recurring payment is not activated, the initial payment is not charged, and the registration transaction moves to
expired(no IPN). You can check the current state with the status endpoint.
Step 2: Create a recurring charge
Your server initiates subsequent payments without a BLIK code and without customer involvement (in model M the customer confirms them in the banking app).
Request parameters
| Field | Type | Required | Description |
|---|---|---|---|
transactionType | string | Yes | "transfers" |
service | string | Yes | Service name from the panel |
value | string | Yes | Charge amount in PLN (must be greater than 0) |
url_success | string | Yes | URL after successful payment |
url_fail | string | Yes | URL after failed payment |
url_ipn | string | No | URL for IPN notifications; without it no IPN is sent (you get the result by webhook) |
checksum | string | Yes | SHA-256 checksum with the charge amount and the alias at the end (see below) |
recurring_alias | string | Yes | Recurring payment alias from the registration (your recurring_registration.alias, also returned in additionalInfo.recurring_registration.alias; max. 128 characters) |
no_delay | boolean | No | Defaults to true - the bank responds immediately without waiting for the customer. false - the bank may hold the charge for up to 72 hours and ask the customer to confirm it in the app; requires prior approval by dpay. In model M omit this field - true is rejected |
description | string | No | Charge description, for example "Premium plan 10/2026". The bank shows it to the customer in the account history - BLIK accepts the first 35 characters. Without it, the customer sees the recurring payment name (label) |
user_ip | string | No | IP address, optional for server-to-server charges |
user_agent | string | No | User-Agent, optional for server-to-server charges |
Do not send blik_code, blik_alias, register_blik_alias, or recurring_registration with a charge. The recurring_alias field cannot be combined with them. The recurring_registration and recurring_alias fields work only with transactionType: "transfers".
Charge checksum
sha256({service}|{SecretHash}|{value}|{url_success}|{url_fail}|{url_ipn}|{recurring_alias})
The alias at the end binds the charge to a specific customer: a checksum without the alias or with another alias is rejected (Invalid checksum). Without url_ipn, keep the empty segment: ...|{url_fail}||{recurring_alias}.
Validation before the charge reaches BLIK
Before a charge reaches BLIK, dpay checks:
- the recurring payment - it must be registered for your service and active (dpay checks its current status, for example whether the customer has cancelled it in the banking app),
- model A - the amount equals
limit_amt, successful charges plus the current one do not exceedtot_limit_amt, and the charge is not earlier thaninit_date, - model M - the limits provided at registration (if any),
- model O - the amount is at most PLN 2000 and within an active range.
A violation returns HTTP 400 with a description in message (see error handling). Nothing is sent to BLIK and the customer is not charged.
Example request
curl -X POST https://api-payments.dpay.pl/api/v1_0/payments/register \
-H "Content-Type: application/json" \
-d '{
"transactionType": "transfers",
"service": "abc123",
"value": "59.99",
"url_success": "https://myshop.com/success",
"url_fail": "https://myshop.com/error",
"url_ipn": "https://myshop.com/api/ipn",
"checksum": "e3b0c44298fc1c149afb...",
"recurring_alias": "SUB-1234567890",
"description": "Premium plan 11/2026"
}'
API response
{
"error": false,
"msg": "Internal processing",
"status": true,
"transactionId": "abc-def-123-456"
}
The charge has been passed to the bank. Store transactionId - you need it to check the status and to retry the charge if necessary.
If the charge is declined immediately, the response is {"error": true, "msg": "Transaction canceled", "status": false, ...} and the decline code is in additionalInfo.error (with a description in additionalInfo.error_description).
Charge result
| Result | Transaction status | Notification |
|---|---|---|
| Charge succeeded | paid | IPN transfer to url_ipn - as for any payment |
| Charge declined | expired | No IPN |
With no_delay: true the result is usually known within a few seconds. In model M and with no_delay: false the bank may wait for the customer for up to 72 hours.
Declined transactions do not generate an IPN, so check the status of charges whose IPN has not arrived in the expected time - by querying the transaction status. After an expired status you can try to retry the charge - the response tells you whether the decline code allows it.
Decline codes
The most common decline codes (in additionalInfo.error of the registration or charge response, or in the retry endpoint response):
| Code | Meaning | What to do |
|---|---|---|
INSUFFICIENT_FUNDS | Insufficient funds in the customer's account | Retry the charge or try a new one later |
LIMIT_EXCEEDED | Transaction limit exceeded at the customer's bank | Retry the charge or inform the customer |
SYSTEM_ERROR, GENERAL_ERROR, ISS_OUTOFSERVICE | Temporary technical problem at the bank or BLIK | Retry the charge |
USER_DECLINED | The customer rejected the charge in the banking app | Do not retry - contact the customer |
TIMEOUT | The customer did not confirm the charge in time | Do not retry - create a new charge |
AUTOCONF_REQ_NOT_MET | Model A: the charge does not match the registration terms (amount, date, frequency) | Check the amount and the charge schedule |
SEC_DECLINED | Model O: BLIK did not qualify the charge as MIT | Contact dpay support; the customer can pay with a BLIK code |
ALIAS_DECLINED | The recurring payment was declined or cancelled | Offer the customer a new registration |
ALIAS_NOT_FOUND | The recurring payment does not exist at BLIK | Offer the customer a new registration |
RECURRING_NOT_ENABLED | The recurring payment model is not enabled for your Payment Point | Contact dpay support |
AMOUNT_LIMIT_EXCEEDED | The amount exceeds the maximum charge amount | Check the amount |
ER_PAYID_UNHANDLED | Registration: the customer's bank does not support BLIK recurring payments, the whole transaction is declined | Offer the customer another payment method |
WRONG_TICKET_BLOCKED | Registration: BLIK code payments temporarily stopped after a series of wrong codes (issued by dpay) | Ask the customer to try again later - see protection against Wrong Ticket attacks |
INTERNAL_ERROR | Unknown result (communication error) | Check the transaction status before trying again |
Retry a declined charge
BLIK allows retrying a declined charge with the same transaction identifier, which rules out charging the customer twice. Use the dedicated endpoint for this - do not resend the same charge through payments/register, because that creates a new transaction.
Retry rules:
- only a charge declined with
INSUFFICIENT_FUNDS,LIMIT_EXCEEDED,SYSTEM_ERROR,GENERAL_ERRORorISS_OUTOFSERVICEcan be retried, - at most 3 retries within 5 minutes of creating the original charge,
- after that, create a new charge (step 2),
- dpay checks the recurring payment and the amount in the same way as for the original charge.
/api/v1_0/payments/recurring/retryComplete contract for retrying a declined recurring charge.Full contract in the API Reference
Endpoint
POST https://api-payments.dpay.pl/api/v1_0/payments/recurring/retry
Content-Type: application/json
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
service | string | Yes | Service name from the panel |
transaction_id | string | Yes | transactionId from the charge response (max. 64 characters) |
checksum | string | Yes | SHA-256 checksum |
Checksum generation
sha256({service}|{SecretHash}|{transaction_id})
Example request
curl -X POST https://api-payments.dpay.pl/api/v1_0/payments/recurring/retry \
-H "Content-Type: application/json" \
-d '{
"service": "abc123",
"transaction_id": "abc-def-123-456",
"checksum": "9c1b4f2e7a..."
}'
Response - retry sent
{
"status": "success",
"data": {
"transactionId": "abc-def-123-456",
"retry": {
"status": "pending",
"count": 1
}
}
}
| Field | Description |
|---|---|
retry.status | pending - the retry reached the bank and the result arrives as for a charge (an IPN on success, expired on decline). failed - BLIK rejected the retry immediately and the transaction stays expired |
retry.count | Retry number (1-3) |
retry.error, retry.error_description | Decline code and description - only with failed |
Response - retry not allowed
When a retry is not allowed, the API returns HTTP 400 and nothing is sent to BLIK:
{
"status": "failed",
"message": "Recurring charge cannot be retried (DECLINE_NOT_RETRYABLE).",
"errors": {
"retry": "DECLINE_NOT_RETRYABLE",
"decline_reason": "USER_DECLINED"
}
}
Code in errors.retry | Meaning | What to do |
|---|---|---|
PAYMENT_NOT_DECLINED | The charge was not declined - it is in progress or succeeded | Wait for the result |
DECLINE_NOT_RETRYABLE | The decline code (in errors.decline_reason) does not allow a retry | Follow the decline code |
RETRY_LIMIT_REACHED | The retry limit has been used | Create a new charge |
RETRY_WINDOW_EXPIRED | 5 minutes have passed since the original charge | Create a new charge |
ALIAS_NOT_AVAILABLE | The recurring payment is no longer active | Offer the customer a new registration |
RECURRING_NOT_ENABLED | The recurring payment model is not enabled for your Payment Point | Contact dpay support |
PAYMENT_NOT_FOUND | The charge was not found in the BLIK system | Check transaction_id |
Service unavailable | Temporary loss of connection to the BLIK system | Call the retry endpoint again shortly |
Other errors use a key matching the field: errors.transaction_id (Transaction not found, Not a recurring charge, Transaction already paid, Transaction not retryable), errors.recurring_alias (Alias not found, Alias not active), and amount validation messages as for a charge.
Check the recurring payment status
Retrieve the current status of a recurring payment, together with the registration terms, from the status endpoint:
POST https://api-payments.dpay.pl/api/v1_0/payments/recurring/status
Content-Type: application/json
/api/v1_0/payments/recurring/statusComplete contract: recurring payment status and registration terms.Full contract in the API Reference
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
service | string | Yes | Service name from the panel |
alias | string | Yes | Recurring payment alias (max. 128 characters) |
checksum | string | Yes | SHA-256 checksum |
Checksum generation
sha256({service}|{SecretHash}|{alias})
Example request
curl -X POST https://api-payments.dpay.pl/api/v1_0/payments/recurring/status \
-H "Content-Type: application/json" \
-d '{
"service": "abc123",
"alias": "SUB-1234567890",
"checksum": "f5a3b2c1d0..."
}'
Response
{
"status": "success",
"data": {
"alias": "SUB-1234567890",
"method": "blik",
"status": "ACTIVE",
"expiration_date": "2027-09-30",
"registration": {
"transaction_id": "abc-def-123-456",
"label": "Premium plan",
"model": "A",
"frequency": "1M",
"limit_amt": 5999,
"tot_limit_amt": 71988,
"is_limit_amt_fixed": true,
"init_date": "2026-11-01",
"terms_url": "https://myshop.com/terms/2026-09",
"terms_version": "2026-09",
"registered_at": "2026-09-26T12:00:00+02:00"
}
}
}
| Field | Type | Description |
|---|---|---|
alias | string | Recurring payment alias |
method | string | Payment method - "blik" |
status | string|null | Current status: "ACTIVE" (can be charged), "INACTIVE" (registration not approved yet), "UNREGISTERED" (cancelled by you or the customer), "EXPIRED" (the expiration date has passed), "DECLINED" (registration declined) |
expiration_date | string|null | Expiration date |
registration | object | Registration terms: transaction_id of the registration transaction, label, model, frequency, limits, init_date, terms_url, terms_version and the registration date (registered_at). Fields not provided at registration are null |
When no recurring payment with this alias was registered for your service, the API returns HTTP 400 with errors.alias: Alias not found.
Cancel a recurring payment
Cancel the recurring payment when the customer cancels the service on your website, or when you replace it with a new one (for example when the plan changes or it is about to expire - its validity cannot be extended). In that case, register the new recurring payment first and cancel the old one after the new one becomes active. Charges with a cancelled alias are rejected.
POST/api/v1_0/payments/recurring/cancelComplete contract for cancelling a recurring payment.Full contract in the API Reference
Endpoint
POST https://api-payments.dpay.pl/api/v1_0/payments/recurring/cancel
Content-Type: application/json
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
service | string | Yes | Service name from the panel |
alias | string | Yes | Recurring payment alias (max. 128 characters) |
reason | string | No | Reason for cancelling (max. 255 characters) |
checksum | string | Yes | SHA-256 checksum |
Checksum generation
sha256({service}|{SecretHash}|{alias}|cancel)
The constant cancel at the end distinguishes cancellation from a status query - a status checksum cannot cancel the recurring payment.
Example request
curl -X POST https://api-payments.dpay.pl/api/v1_0/payments/recurring/cancel \
-H "Content-Type: application/json" \
-d '{
"service": "abc123",
"alias": "SUB-1234567890",
"reason": "Plan cancelled",
"checksum": "f5a3b2c1d0..."
}'
Response
{
"status": "success",
"data": {
"alias": "SUB-1234567890",
"status": "UNREGISTERED"
}
}
Errors are returned with HTTP 400. errors.cancel contains Alias not found (no active recurring payment with this alias), a BLIK error code, or Service unavailable (try again shortly).
Recurring payment change notifications
On every status change of a recurring payment, dpay.pl sends an alias_update notification to alias_ipn_url (or url_ipn when it was not provided). The notification format, signature verification and retry schedule are the same as for BLIK OneClick - see alias notifications. For a recurring payment, alias_type is "PAYID".
change_type | Meaning | What to do |
|---|---|---|
ALIAS_REGISTER | The customer approved the recurring payment - it is active | Mark it as active and schedule charges |
ALIAS_UPDATE | Recurring payment data changed | Refresh the status with the status endpoint |
ALIAS_UNREGISTER | The recurring payment was cancelled - by you or by the customer in the banking app | Stop charging; if the customer has not cancelled the service, offer a new recurring payment |
ALIAS_EXPIRED | The expiration date has passed | Stop charging and offer a new recurring payment |
ALIAS_DECLINED | The registration was declined | Do not charge the customer |
Correlate the notification by the id field - it is the transactionId of the registration transaction. The alias_key field contains the identifier in the BLIK system, which is not equal to your alias (recurring_registration.alias).
Obligations towards the customer
BLIK recurring payments are subject to BLIK complaint rules - the customer can dispute a charge at their bank up to 13 months after authorization. The most common grounds are missing consent, unclear terms, a charge after cancellation, and missing notice before a payment. Therefore:
- Consent - send the registration only after an explicit action by the customer on your website. Show the terms and the price clearly, separately from other content.
- Terms - in
terms_url, send the document the customer actually accepted, and do not change its content - see terms link. - Purchase flow - the name "BLIK Recurring Payments", the list of supporting banks and the payment details before the code - see BLIK requirements.
- Label -
labelshould clearly identify the service. The customer sees it in the banking app in the list of their recurring payments - see how to build the label. - Notice - inform the customer at least 14 days before the first paid period, 30 days before a renewal, and 14 days before a price change.
- Charges - charge only within the terms agreed with the customer. Use the retry endpoint to retry declines.
- Cancellation - make cancellation easy on your website. After a cancellation, do not charge the customer and cancel the recurring payment.
- Evidence - keep, for at least 13 months after the last charge: proof of consent (date, IP address, version of the terms), correspondence with the customer, and the cancellation history. dpay will ask for them in a complaint.
Complaints and fraud reports
A customer can dispute a charge with their bank (a complaint), and a bank can report a payment as fraudulent. The case appears in the panel under Disputes, and you receive an e-mail with the response deadline:
| Case | Your deadline | dpay's deadline towards BLIK |
|---|---|---|
| Complaint | 5 calendar days | 15 days - no response means the complaint is upheld |
| Fraud report | 1 business day | the next business day |
- dpay fills in the consent record from the registration (date, terms, the customer's IP address and browser), the charge history and the status of the recurring payment. You provide the consent screen and the notices sent to the customer before charging - without them, defending against a lack-of-consent or lack-of-notice claim is very difficult.
- An upheld complaint means a correction: the amount goes back to the customer's bank and is deducted from your balance. For fraud, if the funds are still in your balance, we return them to the bank with a good-faith correction.
- After a correction, dpay can request arbitration within 30 days. A won arbitration returns the correction to your balance.
Fees (net, deducted from the balance):
| Item | Amount |
|---|---|
| Handling of each complaint | PLN 50 |
| Upheld complaint | PLN 100 |
| Lost arbitration | PLN 1000 |
VAT is added to these amounts. The handling fee applies to every complaint, whatever the outcome. The fees for an upheld complaint and a lost arbitration (they include the BLIK fees) apply only when you lose - there are none when you win.
Testing in test mode
In test mode, you can test the whole recurring payments integration - registration, charges, declines, retries, status, cancellation and notifications - with no real money. dpay does not connect to BLIK then: the responses come from our simulator, which reproduces the BLIK behavior verified in tests on the BLIK test environment and in the certification.
- Turn on the service's Test mode in the panel - see test environment.
- No request is needed: in test mode all models (A, M and O), all model O amount ranges and
no_delay: falseare available. Field validation, checksums, the PLN 2,000 limit and the model A limits work as in production. - Requests, responses, IPNs and webhooks have the same format as in production. Webhooks from test mode have
livemode: false. - A recurring payment registered in test mode works only in test mode. After you turn test mode off, a charge of its alias returns
Alias not found- register your customers again in production. - The times in the tables are approximate - results arrive asynchronously, as from the bank.
Registration
The registration result depends on the code in blik_code:
| BLIK code | Result |
|---|---|
777200 | Success - the transaction is paid after about 5 s, and after another 5 s or so the recurring payment is active |
777201 | Success after a longer wait for the customer (about 10 s) |
777400 | The customer declined the payment in the banking app (USER_DECLINED) |
777401 | The customer did not confirm in time (TIMEOUT) |
777402 | Insufficient funds for the initial fee (INSUFFICIENT_FUNDS) |
777500 | System error (SYSTEM_ERROR) |
777000 | The customer's bank does not support recurring payments (ER_PAYID_UNHANDLED) - use it to test the error screen required by BLIK |
| Any other code | Success, as for 777200 |
- Success - the registration transaction moves to
paid(IPNtransfer, webhookpayment.succeeded), and then the recurring payment moves toACTIVE: you receive analias_updatenotification withchange_type: "ALIAS_REGISTER"and arecurring_payment.activatedwebhook. In test mode, thealias_keyfield has the formDPAY.PAYID.0..... - Decline - an immediate response with the code in
additionalInfo.error, the transaction isexpired, the recurring payment isDECLINEDand you receive arecurring_payment.declinedwebhook.
Charges
A charge responds immediately with Internal processing, and the result depends on the amount - just like the BLIK test environment, which picks the bank response by amount:
| Amount | Result | Retry |
|---|---|---|
| PLN 60.73 | Declined with INSUFFICIENT_FUNDS | Allowed, succeeds |
| PLN 60.74 | Declined with LIMIT_EXCEEDED | Allowed, succeeds |
| PLN 60.78 | Declined with SYSTEM_ERROR | Allowed, succeeds |
| PLN 60.79 | Declined with INSUFFICIENT_FUNDS on every attempt | Allowed - the fourth retry returns RETRY_LIMIT_REACHED |
| PLN 60.75 | Declined with TIMEOUT | Not allowed (DECLINE_NOT_RETRYABLE) |
| PLN 60.76 | Declined with SEC_DECLINED - the charge was not qualified as MIT | Not allowed |
| PLN 60.77 | Declined with USER_DECLINED - the customer declined the charge in the app | Not allowed |
| PLN 60.65 | Success after about 60 s - as after the customer's confirmation (model M, no_delay: false) | - |
| PLN 2.37 | Success, and about 5 s later the customer removes the recurring payment in the banking app | - |
| Any other amount | Success after about 5 s | - |
- Success - status
paid, IPNtransferand apayment.succeededwebhook. - Decline - status
expired, no IPN, apayment.failedwebhook with the decline code (for examplefailure.code: "insufficient_funds",failure.provider_code: "INSUFFICIENT_FUNDS"). - PLN 2.37 - after the successful charge, the recurring payment moves to
UNREGISTERED: you receivealias_updatewithchange_type: "ALIAS_UNREGISTER"and arecurring_payment.canceledwebhook withcanceled_by: "customer". The next charge returnsAlias not active. - The amount must pass the model validation. In model A, a charge must equal
limit_amt- to test a decline, register the payment withlimit_amt: 6073.
Retries, status, cancellation and expiration
- Retry - the same rules and codes as in production: only the listed declines, at most 3 retries within 5 minutes of the charge. The retry result arrives after about 5 s.
- Status -
INACTIVEuntil activation, thenACTIVE,UNREGISTERED,EXPIREDorDECLINED. - Cancellation - status
UNREGISTEREDimmediately and arecurring_payment.canceledwebhook withcanceled_by: "merchant". As in BLIK, noalias_updatefollows a cancellation by you. - Expiration - a recurring payment with
expiration_dateexpires after that date. You get theEXPIREDstatus,alias_updatewithchange_type: "ALIAS_EXPIRED"and arecurring_payment.expiredwebhook on the first charge or status request after that date. - Refunds - refund successful test charges like any payment in test mode.
Rate limits
| Endpoint | Limit |
|---|---|
POST /payments/register | 120 requests/min |
POST /payments/recurring/status | 60 requests/min |
POST /payments/recurring/retry | 30 requests/min |
POST /payments/recurring/cancel | 30 requests/min |
Error handling
Field validation errors (for example recurring_registration.frequency in model O, a missing recurring_registration.init_date in model A, or a missing recurring_registration.terms_url) are returned with HTTP 422 and a list of fields in errors. Business validation errors are returned with HTTP 400 in the format {"status": "failed", "message": "...", "errors": {...}}. Common messages:
| Message | Cause |
|---|---|
Recurring payments are not enabled for this service. | Recurring payments are not enabled for the service |
BLIK Recurring model M is not enabled for this service. | The selected model is not active for the service |
BLIK Recurring model O requires at least one active amount range for this service. Contact dpay support. | Model O without an active amount range |
An active recurring payment with this alias already exists. | An active recurring payment with this alias already exists (errors.alias: Alias already active) |
Recurring charge requires value > 0. | Charge with an amount of 0 |
No recurring payment registered for this alias. | No recurring payment with this alias was registered for the service (errors.recurring_alias: Alias not found) |
Recurring payment is not active (status: ...). | The recurring payment expired, was cancelled, or the registration was not approved (errors.recurring_alias: Alias not active) |
BLIK Recurring model M does not allow no_delay=true: ... | no_delay: true for a model M recurring payment |
Setting no_delay=false requires merchant approval (...). Contact support. | no_delay: false used without dpay approval |
BLIK Recurring: payment amount must be exactly ... PLN (fixed limit). | Amount different from the fixed registration amount (model A, or model M with is_limit_amt_fixed: true) |
BLIK Recurring: payment amount ... PLN exceeds single payment limit ... PLN. | Model M: single charge limit exceeded |
BLIK Recurring: total spent ... PLN + requested ... PLN exceeds total limit ... PLN. | Total limit tot_limit_amt exceeded |
BLIK Recurring model A: the first payment is scheduled for .... | Model A: charge before init_date |
BLIK Recurring model O: payment amount ... PLN exceeds the maximum of 2000.00 PLN. | Model O: amount above PLN 2000 |
BLIK Recurring model O: payment amount ... PLN is in the ... range, which is not active for this service. Contact dpay support. | Model O: amount in an inactive range |
Declines by the bank and the BLIK system are described in decline codes.
If the recurring payment has expired or the customer has cancelled it in the banking app, offer a new registration (step 1) the next time you interact with the customer, for example when the subscription renews.