Skip to main content

Cards: recurring payments (MIT)

Recurring card payments let you charge a customer's saved card without the customer being present, using the MIT (Merchant Initiated Transaction) model. After the customer gives consent once to establish a mandate, the merchant initiates subsequent charges, for example when renewing a subscription.

Pull model

The merchant initiates each subsequent charge—the pull model—by calling /register at the appropriate point in the billing cycle. dpay.pl does not schedule charges automatically. The merchant decides when and how much to charge, within the configured limits.

POST/api/v1_0/payments/registerRegister a card mandate and initiate recurring MIT charges.Full contract in the API Reference POST/api/v1_0/cards/payment/{transactionId}/pay/card-otpActivate a mandate with the first card payment and 3D Secure.Full contract in the API Reference

Benefits

  • Subscriptions and recurring plans: automatic renewals without customer interaction
  • Lower churn: customers do not have to enter card details again
  • Limit control: fixed or maximum charge amounts configured per mandate

How it works

The process has three steps:

  1. Register the mandate: the merchant calls /register with a register_card_recurring object, and dpay.pl returns card_recurring_alias.
  2. Activate the mandate: the customer completes the first card payment with 3DS. A successful payment activates the mandate.
  3. Create recurring charges (MIT): the merchant charges the saved card by sending card_recurring_alias server to server, without customer interaction.
Store the alias

The card_recurring_alias returned during registration is the only mandate identifier needed for subsequent charges. Store it securely on your server and associate it with the customer or subscription.

Requirements

  • An active Payment Point with recurring card payments enabled; contact dpay.pl to enable them
  • A Server-to-Server card integration; see Cards S2S, because activation uses the same card-payment flow
  • Secure server-side storage for card_recurring_alias

Endpoint

POST https://api-payments.dpay.pl/api/v1_0/payments/register
Content-Type: application/json

Step 1: Register the mandate

Registration creates a mandate and returns its alias. The first payment also establishes the mandate, so value must be greater than zero. The mandate becomes active only after a successful first card payment in Step 2.

Registration amount

The default registration flow—a single customer-paid flow—requires value > 0. To save a card without charging it and establish consent separately (card on file), use three-step tokenization through card_recurring_operation; add_card accepts value=0.

Request parameters

FieldTypeRequiredDescription
transactionTypestringYes"card_recurring"
servicestringYesService name from the panel
valuestringYesFirst-payment amount in PLN; must be greater than 0
creditcardintegerNo1 for a card payment. Optional: for card_recurring, dpay enforces the card flags automatically
url_successstringYesURL used after a successful payment
url_failstringYesURL used after a failed payment
url_ipnstringYesURL for IPN notifications
checksumstringYesSHA-256 checksum
emailstringNoCustomer email address
client_namestringNoCustomer first name
client_surnamestringNoCustomer last name
register_card_recurringobjectYesMandate data to register

register_card_recurring object

FieldTypeRequiredDescription
labelstringYesMandate label, maximum 50 characters
frequencystringNoOptional billing-cycle frequency: DAILY, WEEKLY, BIWEEKLY, MONTHLY, QUARTERLY, SEMIANNUAL, or ANNUAL
limit_amtintegerNoPer-charge limit in grosz
tot_limit_amtintegerNoTotal limit for all charges in grosz
is_limit_amt_fixedbooleanNotrue: the charge must equal limit_amt exactly; false: limit_amt is the maximum amount
expiration_datestringNoMandate expiration date in YYYY-MM-DD format; charges after this date are rejected
init_datestringNoBilling-cycle start date in YYYY-MM-DD format
Units: grosz vs PLN

Limits (limit_amt, tot_limit_amt) are provided in grosz, for example 5999 = PLN 59.99, while value is provided in PLN, for example "59.99". This is a common integration mistake.

Alias generated automatically

dpay.pl always generates card_recurring_alias in the DPAY.CARD.{id}.{random} format. Do not provide your own alias; it will be rejected.

Checksum generation

Generate the checksum exactly as for a standard payment:

sha256({service}|{SecretHash}|{value}|{url_success}|{url_fail}|{url_ipn})

Example request

cURL

curl -X POST https://api-payments.dpay.pl/api/v1_0/payments/register \
-H "Content-Type: application/json" \
-d '{
"transactionType": "card_recurring",
"service": "abc123",
"value": "19.99",
"creditcard": 1,
"url_success": "https://myshop.example/success",
"url_fail": "https://myshop.example/error",
"url_ipn": "https://myshop.example/api/ipn",
"checksum": "e3b0c44298fc1c149afb...",
"email": "customer@example.com",
"register_card_recurring": {
"label": "Premium subscription",
"frequency": "MONTHLY",
"limit_amt": 5999,
"tot_limit_amt": 71988,
"is_limit_amt_fixed": false,
"expiration_date": "2027-06-30"
}
}'

PHP

<?php
$service = getenv('DPAY_SERVICE');
$secretHash = getenv('DPAY_SECRET_HASH');

$value = '19.99'; // first payment that establishes the mandate (must be > 0)
$urlSuccess = 'https://myshop.example/success';
$urlFail = 'https://myshop.example/error';
$urlIpn = 'https://myshop.example/api/ipn';

$checksum = hash('sha256',
$service . '|' . $secretHash . '|' . $value . '|' .
$urlSuccess . '|' . $urlFail . '|' . $urlIpn
);

$payload = json_encode([
'transactionType' => 'card_recurring',
'service' => $service,
'value' => $value,
'creditcard' => 1,
'url_success' => $urlSuccess,
'url_fail' => $urlFail,
'url_ipn' => $urlIpn,
'checksum' => $checksum,
'email' => 'customer@example.com',
'register_card_recurring' => [
'label' => 'Premium subscription',
'frequency' => 'MONTHLY',
'limit_amt' => 5999, // amount in grosz
'tot_limit_amt' => 71988, // amount in grosz
'is_limit_amt_fixed' => false,
'expiration_date' => '2027-06-30',
],
]);

$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);
// Save $result['additionalInfo']['card_recurring_alias'] on the server

JavaScript (Node.js / Express)

const crypto = require('crypto');
const axios = require('axios');

async function registerCardMandate() {
const service = process.env.DPAY_SERVICE;
const secretHash = process.env.DPAY_SECRET_HASH;

const value = '19.99'; // first payment that establishes the mandate (must be > 0)
const urlSuccess = 'https://myshop.example/success';
const urlFail = 'https://myshop.example/error';
const urlIpn = 'https://myshop.example/api/ipn';

const checksum = crypto
.createHash('sha256')
.update(`${service}|${secretHash}|${value}|${urlSuccess}|${urlFail}|${urlIpn}`)
.digest('hex');

const response = await axios.post(
'https://api-payments.dpay.pl/api/v1_0/payments/register',
{
transactionType: 'card_recurring',
service,
value,
creditcard: 1,
url_success: urlSuccess,
url_fail: urlFail,
url_ipn: urlIpn,
checksum,
email: 'customer@example.com',
register_card_recurring: {
label: 'Premium subscription',
frequency: 'MONTHLY',
limit_amt: 5999, // amount in grosz
tot_limit_amt: 71988, // amount in grosz
is_limit_amt_fixed: false,
expiration_date: '2027-06-30',
},
}
);

// Save response.data.additionalInfo.card_recurring_alias
return response.data;
}

API response

{
"error": false,
"status": true,
"msg": "Card recurring mandate registered, awaiting initial customer payment",
"transactionId": "abc-def-123-456",
"additionalInfo": {
"card_recurring_alias": "DPAY.CARD.123.a1b2c3d4",
"operation": "mandate_registration"
}
}

Store additionalInfo.card_recurring_alias; it is required for subsequent charges. transactionId identifies the registration transaction that the customer pays in Step 2.

Step 2: Activate the mandate

The customer completes the first card payment for the transactionId returned in Step 1, exactly as for a standard Server-to-Server card payment; see Cards S2S. This includes 3DS authentication.

After the first payment succeeds, the mandate is automatically activated. Only an active mandate can be used for recurring charges. You receive the first payment result through IPN, just as for any card transaction.

info

Until the customer completes the first payment, the mandate remains inactive and charge attempts return No active card recurring mandate found for this alias.

Step 3: Create a recurring charge (MIT)

To charge the saved card, call /register with card_recurring_alias, transactionType=card_recurring, and a positive value. This is a server-to-server call: the customer does not participate and 3DS is not required.

Request parameters

FieldTypeRequiredDescription
transactionTypestringYes"card_recurring"
servicestringYesService name from the panel
valuestringYesCharge amount in PLN; must be greater than 0
card_recurring_aliasstringYesMandate alias returned during registration, maximum 128 characters
url_successstringYesURL used after a successful payment
url_failstringYesURL used after a failed payment
url_ipnstringYesURL for IPN notifications
checksumstringYesSHA-256 checksum
authorize_onlybooleanNoPre-authorize the saved token instead of charging it. This places a hold on funds; capture or cancel it later as described in Step 3b. Pre-authorization must be enabled.
Mutually exclusive fields

register_card_recurring and card_recurring_alias are mutually exclusive. Send only card_recurring_alias for a charge.

Example request

cURL

curl -X POST https://api-payments.dpay.pl/api/v1_0/payments/register \
-H "Content-Type: application/json" \
-d '{
"transactionType": "card_recurring",
"service": "abc123",
"value": "59.99",
"card_recurring_alias": "DPAY.CARD.123.a1b2c3d4",
"url_success": "https://myshop.example/success",
"url_fail": "https://myshop.example/error",
"url_ipn": "https://myshop.example/api/ipn",
"checksum": "e3b0c44298fc1c149afb..."
}'

PHP

<?php
$service = getenv('DPAY_SERVICE');
$secretHash = getenv('DPAY_SECRET_HASH');

$value = '59.99';
$urlSuccess = 'https://myshop.example/success';
$urlFail = 'https://myshop.example/error';
$urlIpn = 'https://myshop.example/api/ipn';

$checksum = hash('sha256',
$service . '|' . $secretHash . '|' . $value . '|' .
$urlSuccess . '|' . $urlFail . '|' . $urlIpn
);

// $aliasValue - the customer's previously saved mandate alias
$payload = json_encode([
'transactionType' => 'card_recurring',
'service' => $service,
'value' => $value,
'card_recurring_alias' => $aliasValue,
'url_success' => $urlSuccess,
'url_fail' => $urlFail,
'url_ipn' => $urlIpn,
'checksum' => $checksum,
]);

$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);

API response

{
"error": false,
"status": true,
"msg": "Internal processing",
"transactionId": "ghi-jkl-789-012"
}

The response confirms that the charge was accepted. The msg field and the transaction status depend on whether the charge settled immediately or is still processing at the moment of the reply:

  • synchronously settled: msg: "Transaction paid", and the transaction is already in the paid status (in additionalInfo.transaction);
  • still processing (or an authorize_only pre-authorization): msg: "Internal processing", and the status is not final yet (as in the example above).

Both shapes are valid. The final result is always confirmed by the IPN for transactionId, just as for any payment. Base your integration logic on the transaction status and IPN, not on the exact msg text; see transaction statuses.

Idempotency and retries

The charge endpoint is not idempotent: every call creates a new charge attempt. After a network timeout, do not retry blindly. Check the previous transaction first through IPN or a status query. Concurrent calls for the same alias are blocked with Card recurring charge already in progress for this alias.

Step 3b: Pre-authorize a saved token (authorize_only)

Instead of immediately charging the saved card, you can place a hold on funds using the mandate token. Add the optional "authorize_only": true field to the Step 3 charge call: transactionType=card_recurring, card_recurring_alias, and a positive value. dpay then performs a pre-authorization on the saved token instead of a charge.

The returned transactionId is the authorization identifier. You can then:

  • capture all or part of the held amount through the capture endpoint, or
  • release the hold through the cancellation endpoint.

Both endpoints are described in Cards: pre-authorization and capture.

Pre-authorization must be enabled

Token pre-authorization must be enabled for the Payment Point; contact dpay.pl. Without authorize_only, or when it is false, the call behaves as a standard recurring charge.

Example request

cURL

curl -X POST https://api-payments.dpay.pl/api/v1_0/payments/register \
-H "Content-Type: application/json" \
-d '{
"transactionType": "card_recurring",
"service": "abc123",
"value": "59.99",
"card_recurring_alias": "DPAY.CARD.123.a1b2c3d4",
"authorize_only": true,
"url_success": "https://myshop.example/success",
"url_fail": "https://myshop.example/error",
"url_ipn": "https://myshop.example/api/ipn",
"checksum": "e3b0c44298fc1c149afb..."
}'

Capture all or part of the held amount, or release it, as described in Cards: pre-authorization and capture.

Three-step tokenization (card on file)

By default, Step 1 saves the card and establishes consent in a single customer-paid flow. If your application separates saving the card from establishing consent—for example, a card is added when the account is created and consent is confirmed later—use the optional card_recurring_operation field. It splits the lifecycle into three independent /register calls:

Two things matter here:

  • The order is strict: add_card is a pair of calls, /register + /pay. The mandate becomes ready for cof_initial only after the add_card /pay completes (it moves from INACTIVE to TOKENIZED). The /register add_card call alone is not enough - the alias already exists, but the mandate is not tokenized yet. Calling cof_initial before the add_card /pay fails with No tokenized card mandate found for this alias. Complete the add_card tokenization (/pay) before cof_initial.
  • Where the 3DS form shows up: for cof_initial, only the second call (/pay) returns it - /register in that step returns only a transactionId.
OperationCallWhat it does
add_cardregister_card_recurring + value=0Saves the card through tokenization and returns card_recurring_alias. No charge, 3DS, or consent.
cof_initialcard_recurring_alias from add_cardEstablishes card-on-file consent for the saved card. /register returns a new transactionId (separate from the add_card transaction, same alias) - no form yet. The 3-D Secure challenge is returned only by the following /pay call on that transactionId (like a standard card payment; see Cards S2S). After customer confirmation, the mandate can be charged. Use value=0 for account verification without a charge, or value>0 for the first real charge.
charge (or omitted)card_recurring_alias + value>0Recurring MIT charge, as in Step 3. This is the default when the field is omitted.
Card on file must be enabled

The three-step flow requires card on file to be enabled for the Payment Point; contact dpay.pl. Without card_recurring_operation, registration and charging work as described in Steps 1–3.

Each operation consists of a /register call with the appropriate card_recurring_operation followed, for add_card and cof_initial, by a /pay call as described in Cards S2S. Authentication differs: add_card has no 3DS, while cof_initial uses a full 3DS challenge.

add_card: save the card without 3DS

Register an add_card operation with value=0, then finalize /pay with encrypted card data as described in Cards S2S. The card is saved without a charge or 3DS, and the returned card_recurring_alias represents the saved card.

curl -X POST https://api-payments.dpay.pl/api/v1_0/payments/register \
-H "Content-Type: application/json" \
-d '{
"transactionType": "card_recurring",
"service": "abc123",
"value": "0",
"card_recurring_operation": "add_card",
"url_success": "https://myshop.example/success",
"url_fail": "https://myshop.example/error",
"url_ipn": "https://myshop.example/api/ipn",
"checksum": "e3b0c44298fc1c149afb...",
"register_card_recurring": { "label": "Customer card" }
}'

/register response

{
"error": false,
"msg": "Card registered, awaiting tokenization via /pay (required before cof_initial).",
"status": true,
"transactionId": "8f3c1e20-4a7b-4c11-9e33-add0000000",
"additionalInfo": {
"card_recurring_alias": "DPAY.CARD.123.a1b2c3d4",
"operation": "mandate_registration"
}
}

Store additionalInfo.card_recurring_alias; it identifies the saved card in subsequent operations. transactionId is the tokenization transaction, which you finalize with /pay.

/register add_card must be followed by /pay

This response is only the first of the two add_card calls. The mandate is INACTIVE and not yet usable for cof_initial (with value=0 there is no customer payment at all - "awaiting" refers to tokenization, not a payment). Complete the /pay below first (the S2S tokenization); only that moves the mandate to TOKENIZED. Do not jump straight to cof_initial.

/pay response

Finalize /pay with encrypted card data - server to server, no 3DS (see Cards S2S):

{
"success": true,
"status": "success",
"message": {
"redirectText": "",
"redirectType": "SUCCESS",
"dccOffer": null
}
}

redirectType: "SUCCESS" means the card was saved and the mandate moved to the TOKENIZED state - only now is it ready for cof_initial. The tokenization transaction itself (the transactionId from /register) ends up cancelled - it is not a real payment, so it is never settled.

Establish card-on-file consent for the saved card using the card_recurring_alias returned by add_card. Register a cof_initial operation, then finalize /pay. This step presents the 3D Secure challenge that confirms customer consent.

Complete the /pay for add_card first

cof_initial requires a mandate in the TOKENIZED state, that is, after the add_card /pay has completed. If you call cof_initial right after /register add_card (skipping /pay), you get No tokenized card mandate found for this alias. Complete the add_card tokenization (/pay) before cof_initial. The order is strict: register add_card/pay add_cardonly then register cof_initial/pay cof_initial.

curl -X POST https://api-payments.dpay.pl/api/v1_0/payments/register \
-H "Content-Type: application/json" \
-d '{
"transactionType": "card_recurring",
"service": "abc123",
"value": "0",
"card_recurring_operation": "cof_initial",
"card_recurring_alias": "DPAY.CARD.123.a1b2c3d4",
"url_success": "https://myshop.example/success",
"url_fail": "https://myshop.example/error",
"url_ipn": "https://myshop.example/api/ipn",
"checksum": "e3b0c44298fc1c149afb..."
}'

/register response

{
"error": false,
"msg": "Card-on-file initial registered, awaiting 3DS confirmation",
"status": true,
"transactionId": "b7d40c9a-2f18-4d6e-8a51-cof0000000",
"additionalInfo": {
"card_recurring_alias": "DPAY.CARD.123.a1b2c3d4",
"operation": "cof_initial"
}
}
A new transactionId - not the form yet

/register for cof_initial returns a new, separate transactionId (different from the add_card tokenization transaction), but the same card_recurring_alias. You call the next /pay on this new transactionId - only that call returns the 3DS form.

value=0 establishes consent without a charge for account verification. value>0 combines consent with the first real charge.

/pay response

Finalize /pay on the transactionId from the /register response above (as in Cards S2S). If the issuer requires authentication (the typical case), the response contains a challenge form:

{
"success": true,
"status": "success",
"message": {
"redirectText": "<Base64 HTML form with auto-submit>",
"redirectType": "FORM",
"dccOffer": null
}
}

If the issuer does not require a challenge (a frictionless payment), /pay immediately returns redirectType: "SUCCESS" instead - no form.

Handling 3DS for cof_initial

cof_initial returns a 3DS challenge exactly like a standard card payment. When finalizing /pay, you receive redirectType: "FORM" and the Base64-encoded HTML form in redirectText. Handle it as described in Cards S2S: handling 3D Secure: render the form and, after the customer completes the challenge, call /pay again with threeDsConfirmed: true. Card-on-file consent is established and the mandate becomes ready for MIT charges only after challenge confirmation.

charge: recurring charge (MIT)

After consent is established with cof_initial, create subsequent charges exactly as described in Step 3: send card_recurring_alias and a positive value, with optional authorize_only. You can omit card_recurring_operation or set it to charge; the result is the same: a standalone /register call, with no /pay and no 3DS.

/register response

{
"error": false,
"msg": "Transaction paid",
"status": true,
"transactionId": "c2a19f5b-7e04-48d3-b1a2-mit0000000",
"additionalInfo": {
"transaction": {
"payment_id": "c2a19f5b-...",
"value": 35,
"status": "paid"
}
}
}

The example above shows the synchronously settled variant (msg: "Transaction paid", transaction status paid). Exactly as in Step 3, the response may also take the msg: "Internal processing" form with a non-final status when the charge is still processing—both shapes are valid. The rules are identical to Step 3: base your logic on the transaction status and IPN, not on the msg text, and note that the endpoint is not idempotent.

Limits

dpay.pl validates limits before attempting to charge the card:

LimitFieldBehavior
Per-charge, fixedlimit_amt + is_limit_amt_fixed: trueThe charge must be exactly equal to limit_amt
Per-charge, maximumlimit_amt + is_limit_amt_fixed: falseThe charge cannot exceed limit_amt
Totaltot_limit_amtThe sum of all settled charges and the current charge cannot exceed tot_limit_amt
Expirationexpiration_dateCharges after this date are rejected

Limits are provided in grosz. Exceeding a limit returns an error before dpay calls the card processor.

Unregister a mandate

To end a subscription, simply stop calling the charge endpoint. dpay.pl does not create any charge without your request. The mandate expires automatically on expiration_date. A dpay.pl operator can also deactivate a mandate manually in the administration panel, giving it the UNREGISTERED status; subsequent charges are then rejected.

Validation and constraints

RuleDescription
transactionTypeMust be "card_recurring"
Mutually exclusive fieldsregister_card_recurring and card_recurring_alias cannot be sent together
register_card_recurring.labelMaximum 50 characters
register_card_recurring.frequencyOptional: DAILY, WEEKLY, BIWEEKLY, MONTHLY, QUARTERLY, SEMIANNUAL, or ANNUAL
card_recurring_aliasMaximum 128 characters; a charge requires value > 0
Mandate aliasAlways generated by dpay.pl; you cannot provide your own
authorize_onlyOptional with card_recurring_alias: pre-authorizes instead of charging. Pre-authorization must be enabled for the Payment Point
Limitslimit_amt and tot_limit_amt are provided in grosz

Error handling

MessageCauseAction
Invalid checksumInvalid checksumCheck field order and values in the checksum
Card Recurring is not enabled for this service.Recurring cards are not enabled for the Payment PointContact dpay.pl
No active card recurring mandate found for this alias.The mandate does not exist or has not been activated because the customer did not complete the first paymentMake sure Step 2 completed successfully
Card recurring mandate has expired; please register a new mandate.Mandate is past expiration_dateRegister a new mandate
Card Recurring charge requires value > 0.Charge amount is 0Provide a positive value
Card Recurring: payment amount ... exceeds single payment limit ...limit_amt exceededAdjust the amount to the limit
Card Recurring: total spent ... exceeds total limit ...tot_limit_amt exceededThe mandate's total limit has been exhausted
No tokenized card mandate found for this alias. Complete the add_card tokenization (/pay) before cof_initial.cof_initial was called before the add_card /pay completed (the mandate is not tokenized yet), or the alias is wrongComplete the add_card /pay step before cof_initial; verify you use the alias returned by add_card
Card already registered.The card is already saved for this service during add_cardUse the existing card_recurring_alias instead of saving the card again
Card is not available for recurring charges. Please re-add the card.The saved card has no valid recurring-charge referenceSave the card again with add_card to obtain a new alias
Card recurring charge already in progress for this alias...A concurrent charge for this alias is in progressDo not retry blindly; check the previous attempt's status

Charge declines and card-data errors, such as Payment was declined by the card issuer. and Invalid card details., are described in the shared Cards S2S guide. The message value can vary by situation, so avoid matching it too strictly and treat unknown values as generic errors.

tip

If charges begin to fail permanently—for example, because the customer's card expired—or the mandate expires, register a new mandate. The customer provides their card details and consent again.