Skip to main content

Refunds

dpay.pl lets you refund completed transactions. Both full and partial refunds are supported. You can issue refunds programmatically through the API or manually from the administration panel.

POST/api/v1/pbl/refundFull refund contract: parameters, checksum, and error codes.Full contract in the API Reference

Supported payment methods

MethodFull refundPartial refundMultiple partial refunds
BLIKYesYesYes
BLIK BNPLYesYesYes
Bank transfers (PBL)YesYesYes
Payment cards (Visa / Mastercard)YesYesYes
MB WAYYesYesYes
PayPoYesYesNo - one refund only
TwistoYesYesNo - one refund only
PayPo and Twisto: one refund only

For PayPo and Twisto, you can issue exactly one refund for a transaction: either full or partial. After the first refund, even a partial one, the transaction is marked as refunded and subsequent attempts will fail with Transaction has already been refunded.

Time limit

You can issue a refund within 180 days of the transaction date for payment cards, MB WAY, PayPo, and Twisto. For BLIK, the effective limit is 179 days. Bank transfers (PBL) have no refund time limit.

API endpoint

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

Request parameters

FieldTypeRequiredDescription
servicestringYesService name from the panel
transaction_idstringYesIdentifier of the transaction to refund
valuestringNoRefund amount (for example, "15.00"). Omit it for a full refund
reasonstringNoRefund description or reason, max. 255 characters. Note: if you send this field, it must be included in the checksum (see below)
checksumstringYesSHA-256 checksum

Optional headers

HeaderDescription
DigitalPayments-PSP-Message-IDYour own refund identifier (max. 24 characters; longer values are truncated). It determines the refund transfer title: dpay.pl zwrot {header value}. Without this header, the title is dpay.pl {transaction_id}. This identifier is also returned in the response's message field
Partial refund

To issue a partial refund, provide the value parameter with an amount lower than the transaction amount. The amount must be a non-negative number (for example, "15.00").

Generating the checksum

The checksum is calculated from ALL fields in the order sent

The server verifies the checksum by concatenating the values of all body fields in the order in which they appear in the JSON (excluding checksum), separated by |, with secret_hash appended at the end. This means that:

  • every field you send, including optional value and reason, must be included in the checksum,
  • the order of fields in the body must match the order of values in the checksum,
  • omitting a sent field from the checksum, such as reason, results in the HTTP 401 error Unauthorized request.

For the formulas below, send the fields in the body in exactly the order shown.

Full refund

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

Partial refund (with the value parameter)

sha256({service}|{transaction_id}|{value}|{secret_hash})

Refund with a reason (with the reason parameter)

For a body ordered as service, transaction_id, value, reason:

sha256({service}|{transaction_id}|{value}|{reason}|{secret_hash})

PHP

$service = 'abc123';
$transactionId = 'abc-def-123-456';
$secretHash = '9a8b7c6d5e4f3a2b1c0d';

// Full refund
$checksum = hash('sha256',
$service . '|' . $transactionId . '|' . $secretHash
);

// Partial refund
$value = '15.00';
$checksumPartial = hash('sha256',
$service . '|' . $transactionId . '|' . $value . '|' . $secretHash
);

JavaScript (Node.js)

const crypto = require('crypto');

const service = 'abc123';
const transactionId = 'abc-def-123-456';
const secretHash = '9a8b7c6d5e4f3a2b1c0d';

// Full refund
const checksum = crypto
.createHash('sha256')
.update(`${service}|${transactionId}|${secretHash}`)
.digest('hex');

// Partial refund
const value = '15.00';
const checksumPartial = crypto
.createHash('sha256')
.update(`${service}|${transactionId}|${value}|${secretHash}`)
.digest('hex');

Python

import hashlib

service = 'abc123'
transaction_id = 'abc-def-123-456'
secret_hash = '9a8b7c6d5e4f3a2b1c0d'

# Full refund
data = f'{service}|{transaction_id}|{secret_hash}'
checksum = hashlib.sha256(data.encode('utf-8')).hexdigest()

# Partial refund
value = '15.00'
data_partial = f'{service}|{transaction_id}|{value}|{secret_hash}'
checksum_partial = hashlib.sha256(data_partial.encode('utf-8')).hexdigest()

Request examples

Full refund: cURL

curl -X POST https://panel.dpay.pl/api/v1/pbl/refund \
-H "Content-Type: application/json" \
-d '{
"service": "abc123",
"transaction_id": "abc-def-123-456",
"checksum": "e3b0c44298fc1c149afb..."
}'

Partial refund: cURL

curl -X POST https://panel.dpay.pl/api/v1/pbl/refund \
-H "Content-Type: application/json" \
-d '{
"service": "abc123",
"transaction_id": "abc-def-123-456",
"value": "15.00",
"checksum": "a1b2c3d4e5f6..."
}'

PHP

<?php
$service = getenv('DPAY_SERVICE');
$secretHash = getenv('DPAY_SECRET_HASH');
$transactionId = 'abc-def-123-456';
$value = '15.00'; // null for a full refund

// Generate the checksum
if ($value !== null) {
$checksum = hash('sha256',
$service . '|' . $transactionId . '|' . $value . '|' . $secretHash
);
} else {
$checksum = hash('sha256',
$service . '|' . $transactionId . '|' . $secretHash
);
}

$payload = [
'service' => $service,
'transaction_id' => $transactionId,
'checksum' => $checksum,
];

if ($value !== null) {
$payload['value'] = $value;
}

$ch = curl_init('https://panel.dpay.pl/api/v1/pbl/refund');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$result = json_decode($response, true);

if ($httpCode === 200 && $result['status'] === 'success') {
echo 'Refund completed successfully';
} else {
echo 'Refund error: ' . $result['message'];
}

API response

Success (HTTP 200)

{
"status": "success",
"refund": true,
"message": "dpay.pl abc-def-123-456"
}

The message field is the refund message identifier, which is used as the refund transfer title, rather than a fixed response text. By default, it has the form dpay.pl {transaction_id}. If you sent the DigitalPayments-PSP-Message-ID header, you receive dpay.pl zwrot {header value}.

Error

{
"status": "error",
"refund": false,
"message": "Transaction does not exist"
}
caution

For a nonexistent transaction_id, the 404 response may use a simplified format: text instead of a full JSON object with a message field. Do not rely solely on parsing the message field; make your decision based on the HTTP status code.

Checking refund availability

Before issuing a refund, you can check whether it is available without performing the refund itself:

POST https://panel.dpay.pl/api/v1/pbl/check-refund-availability
Content-Type: application/json
POST/api/v1/pbl/check-refund-availabilityCheck refund availability without issuing the refund.Full contract in the API Reference

The parameters and checksum are identical to those used for a refund: service, transaction_id, and optionally value, with the same checksum calculation rules. Example response:

{
"status": "success",
"refund": true,
"message": "Refund available"
}

When a refund is unavailable, you receive status: "error", refund: false, and a message explaining the reason (the same messages listed in the error table below).

GET variants

Both endpoints also have GET variants with parameters in the path:

GET https://panel.dpay.pl/api/v1/pbl/refund/{service}/{transaction_id}/{checksum}
GET https://panel.dpay.pl/api/v1/pbl/check-refund-availability/{service}/{transaction_id}/{checksum}

We recommend the POST variants because they allow you to provide value and reason and do not leave the checksum in intermediary access logs.

Multiple partial refunds

For methods that support multiple partial refunds (BLIK, cards, MB WAY, and PBL bank transfers), you can issue multiple refunds for a single transaction as long as their total does not exceed the original transaction amount.

dpay.pl tracks the total refunded amount in the refunded_amount field. Example:

OperationAmountrefunded_amountRemaining
Transaction100.00 PLN0.00100.00
Refund #130.00 PLN30.0070.00
Refund #225.00 PLN55.0045.00
Refund #345.00 PLN100.000.00
caution

Attempting to refund more than the transaction's remaining refundable amount results in an error.

Common errors

HTTPMessage (message)Cause
401Unauthorized requestInvalid checksum. Check the field order, Secret Hash, and whether all sent fields, including value and reason, were included in the checksum
404Transaction does not existThe specified transaction_id does not exist
402Transaction has not been paidThe transaction has not been paid; only paid transactions can be refunded
410Transaction has already been refundedThe transaction has already been refunded (for PayPo/Twisto, this also applies after one partial refund)
400Refund amount exceeds available refund amountThe refund amount exceeds the remaining refundable amount
406Insufficient balance to process the refundInsufficient balance to process the refund
409Transaction already has a refund form submitted!A refund form has already been submitted for this transaction
400The operator of the indicated channel does not support refundsThe payment method or channel does not support refunds through this route, or the time limit has expired (BLIK)
411Charge transactions cannot be refundedCharge transactions cannot be refunded
400Refund failedThe refund failed; try again or contact support

Refunds in the administration panel

You can also issue a refund manually:

  1. Sign in to panel.dpay.pl.
  2. Go to the Transactions section.
  3. Find the transaction you want to refund using the filters or search.
  4. Click the transaction to open its details.
  5. Click Refund funds.
  6. Select the refund type: full or partial, if available for the payment method.
  7. For a partial refund, enter the amount.
  8. Confirm the refund.