Skip to main content

BLIK Level 0

BLIK Level 0 is a direct integration in which the customer enters a six-digit BLIK code on your website without being redirected to an external payment gateway. This provides a smoother user experience.

How BLIK Level 0 works​

Unlike the standard Simple Gateway integration, the customer is not redirected. The entire process takes place on your website.

POST/api/v1_0/payments/registerComplete registration contract: parameters, checksum, and error codes.Full contract in the API Reference

Requirements​

  • An active Payment Point with BLIK enabled
  • A form on your website for entering the BLIK code
  • Access to the customer's IP address and User-Agent, as required by applicable regulations

Endpoint​

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

The payment registration endpoint accepts up to 120 requests per minute.

Request parameters​

The request is the same as for standard payment registration, with additional BLIK-specific fields:

FieldTypeRequiredDescription
transactionTypestringYes"transfers"
servicestringYesService name from the panel
valuestringYesAmount in PLN
url_successstringYesURL used after a successful payment
url_failstringYesURL used after a failed payment
url_ipnstringYesURL for IPN notifications
checksumstringYesSHA-256 checksum
blik_codestringYesSix-digit BLIK code
user_ipstringYesCustomer's IP address
user_agentstringYesCustomer's User-Agent header

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": "transfers",
"service": "abc123",
"value": "29.99",
"url_success": "https://myshop.example/success",
"url_fail": "https://myshop.example/error",
"url_ipn": "https://myshop.example/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"
}'

PHP​

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

$value = '29.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
);

$payload = json_encode([
'transactionType' => 'transfers',
'service' => $service,
'value' => $value,
'url_success' => $urlSuccess,
'url_fail' => $urlFail,
'url_ipn' => $urlIpn,
'checksum' => $checksum,
'blik_code' => $_POST['blik_code'], // Code submitted through the form
'user_ip' => $_SERVER['REMOTE_ADDR'],
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
]);

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

JavaScript (Node.js / Express)​

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

async function processBlikPayment(req, res) {
const service = process.env.DPAY_SERVICE;
const secretHash = process.env.DPAY_SECRET_HASH;

const value = '29.99';
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: 'transfers',
service,
value,
url_success: urlSuccess,
url_fail: urlFail,
url_ipn: urlIpn,
checksum,
blik_code: req.body.blik_code,
user_ip: req.ip,
user_agent: req.headers['user-agent'],
}
);

return response.data;
}

API response​

Success: payment awaiting confirmation​

{
"error": false,
"msg": "Internal processing",
"status": true,
"transactionId": "abc-def-123-456"
}

The status field is a Boolean (true or false). After receiving this response, the customer must confirm the transaction in their banking app. You receive the payment result through IPN.

Check transaction status​

In addition to waiting for an IPN notification, you can poll the dpay API for the current transaction status. This is useful when you want to update the user interface in real time.

Endpoint​

POST https://panel.dpay.pl/api/v1/pbl/details
Content-Type: application/json

Parameters​

FieldTypeDescription
servicestringService name from the panel
transaction_idstringTransaction ID returned during registration
checksumstringSHA-256 checksum

Checksum generation​

sha256({service}|{transaction_id}|{SecretHash})

Example response​

The response contains the top-level status: "success" field and a transaction object. The most important fields are shown below; see Transaction Details in the API Reference for the complete response:

{
"status": "success",
"transaction": {
"id": "abc-def-123-456",
"status": "paid",
"value": "29.99",
"creation_date": "2026-03-15 12:00:00"
}
}

Possible statuses​

StatusDescription
createdTransaction created and awaiting confirmation
processingTransaction is being processed
paidPayment completed successfully
capturedFunds captured; final status for a pre-authorized payment
expiredTransaction canceled or expired
tip

We recommend polling every 2–3 seconds for no more than 2 minutes. The BLIK code expires after that time.

The complete endpoint documentation is available in Transaction Details in the API Reference.

Error: payment declined​

{
"error": true,
"msg": "Transaction canceled",
"status": false,
"transactionId": "42191111-A7AE-392E-8C09-7965C1DC6B0B",
"additionalInfo": {
"error": "USER_DECLINED",
"error_description": "User declined the transaction"
}
}

Error handling​

The error code is returned in additionalInfo.error, with an optional description in additionalInfo.error_description.

In test mode (sandbox), the following simulation codes are used:

Error codeDescriptionAction
DECLINETransaction declinedAsk the customer to retry
EXPIRED_CARDPayment instrument expiredInform the customer
INSUFFICIENT_FUNDSInsufficient fundsInform the customer
USER_DECLINEDCustomer declined the transaction in their banking appAsk the customer to retry
TIMEOUTTimed out while waiting for confirmationAsk for a new code
SYSTEM_ERRORSystem errorRetry later

In production, additionalInfo.error contains the raw BLIK decline code passed through without modification, or INTERNAL_ERROR for an internal error. Treat the list as open-ended and handle unknown codes as a generic payment decline.

For the BLIK code itself you will most often see ER_WRONG_TICKET (the code does not exist, for example a typo), ER_TIC_EXPIRED (the code has expired) or ER_TIC_USED (the code has already been used) - ask the customer for a new code. The WRONG_TICKET_BLOCKED code is issued by dpay - see protection against Wrong Ticket attacks.

BLIK form example (frontend)​

<form id="blik-form">
<label for="blik-code">BLIK code</label>
<input
type="text"
id="blik-code"
name="blik_code"
maxlength="6"
pattern="[0-9]{6}"
inputmode="numeric"
placeholder="______"
required
/>
<button type="submit">Pay</button>
</form>

<div id="blik-status" style="display: none;">
<p>Confirm the payment in your banking app...</p>
</div>

<script>
document.getElementById('blik-form').addEventListener('submit', async (e) => {
e.preventDefault();

const blikCode = document.getElementById('blik-code').value;

// Show the waiting message
document.getElementById('blik-form').style.display = 'none';
document.getElementById('blik-status').style.display = 'block';

const response = await fetch('/api/pay/blik', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ blik_code: blikCode }),
});

const result = await response.json();

if (result.error) {
alert('Error: ' + result.msg);
document.getElementById('blik-form').style.display = 'block';
document.getElementById('blik-status').style.display = 'none';
}
// Wait for the IPN; the payment status changes after confirmation
});
</script>

Testing in sandbox mode​

In test mode, you can use special BLIK codes that simulate different scenarios without contacting the BLIK provider:

BLIK codeScenario
777200Payment succeeds
777201Payment succeeds after a longer wait
777400Customer declines in their banking app
777401Timeout with no confirmation
777402Insufficient funds
777500System error

The complete list of test data and details is available in Test environment.

BLIK code validation​

A BLIK code always consists of exactly six digits. Validate it on both the client and server:

// Server-side validation
if (!preg_match('/^\d{6}$/', $blikCode)) {
http_response_code(400);
echo json_encode(['error' => 'The BLIK code must contain exactly 6 digits']);
exit;
}
BLIK code validity

A BLIK code is valid for 2 minutes after it is generated in the banking app. Make sure the form tells the customer to enter it promptly.

Protection against Wrong Ticket attacks​

A Wrong Ticket attack is sending a series of random BLIK codes in the hope that one of them matches the active code of another user who is paying at that moment. As a BLIK settlement agent, dpay must detect and stop such attacks, so after a series of wrong codes it temporarily stops BLIK code payments:

  • from the customer IP address sent in user_ip - for your service,
  • for your whole service, when wrong codes come from many addresses.

A stopped code does not reach BLIK, and the response has WRONG_TICKET_BLOCKED in additionalInfo.error. Alias payments (BLIK OneClick, recurring payment charges) keep working. The block expires on its own; if it covered your whole service, dpay will contact you to find the cause.

On your side:

  • Send the customer's real IP address in user_ip. An address block then stops the attacker instead of all your customers.
  • Limit the number of code attempts, for example with a CAPTCHA or by locking the form after a few wrong codes in one session.
  • Do not retry the payment automatically after an ER_WRONG_TICKET decline - ask the customer for a new code.
  • After WRONG_TICKET_BLOCKED, show the customer a message, for example "BLIK code payment is temporarily unavailable. Try again later or choose another payment method".