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
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:
| Field | Type | Required | Description |
|---|---|---|---|
transactionType | string | Yes | "transfers" |
service | string | Yes | Service name from the panel |
value | string | Yes | Amount in PLN |
url_success | string | Yes | URL used after a successful payment |
url_fail | string | Yes | URL used after a failed payment |
url_ipn | string | Yes | URL for IPN notifications |
checksum | string | Yes | SHA-256 checksum |
blik_code | string | Yes | Six-digit BLIK code |
user_ip | string | Yes | Customer's IP address |
user_agent | string | Yes | Customer'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
| Field | Type | Description |
|---|---|---|
service | string | Service name from the panel |
transaction_id | string | Transaction ID returned during registration |
checksum | string | SHA-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
| Status | Description |
|---|---|
created | Transaction created and awaiting confirmation |
processing | Transaction is being processed |
paid | Payment completed successfully |
captured | Funds captured; final status for a pre-authorized payment |
expired | Transaction canceled or expired |
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 code | Description | Action |
|---|---|---|
DECLINE | Transaction declined | Ask the customer to retry |
EXPIRED_CARD | Payment instrument expired | Inform the customer |
INSUFFICIENT_FUNDS | Insufficient funds | Inform the customer |
USER_DECLINED | Customer declined the transaction in their banking app | Ask the customer to retry |
TIMEOUT | Timed out while waiting for confirmation | Ask for a new code |
SYSTEM_ERROR | System error | Retry 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 code | Scenario |
|---|---|
777200 | Payment succeeds |
777201 | Payment succeeds after a longer wait |
777400 | Customer declines in their banking app |
777401 | Timeout with no confirmation |
777402 | Insufficient funds |
777500 | System 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;
}
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_TICKETdecline - 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".