Zwroty (Refunds)
dpay.pl umożliwia realizację zwrotów środków dla zakończonych transakcji. Obsługiwane są zarówno zwroty pełne, jak i częściowe. Zwroty można wykonywać programowo przez API lub ręcznie z poziomu panelu administracyjnego.
POST/api/v1/pbl/refundPełny kontrakt zwrotu: parametry, checksum i kody błędów.Pełny kontrakt w API Reference
Obsługiwane metody płatności
| Metoda | Zwrot pełny | Zwrot częściowy | Wiele zwrotów częściowych |
|---|---|---|---|
| BLIK | Tak | Tak | Tak |
| BLIK BNPL | Tak | Tak | Tak |
| Przelewy bankowe (PBL) | Tak | Tak | Tak |
| Karty płatnicze (Visa / Mastercard) | Tak | Tak | Tak |
| MB WAY | Tak | Tak | Tak |
| PayPo | Tak | Tak | Nie - tylko jeden zwrot |
| Twisto | Tak | Tak | Nie - tylko jeden zwrot |
Dla PayPo i Twisto możesz wykonać dokładnie jeden zwrot do transakcji - pełny albo częściowy.
Po pierwszym zwrocie (nawet częściowym) transakcja jest oznaczana jako zwrócona i kolejne próby
zakończą się błędem Transaction has already been refunded.
Zwrot można wykonać w ciągu 180 dni od daty transakcji dla kart płatniczych, MB WAY, PayPo i Twisto. Dla BLIK limit wynosi efektywnie 179 dni. Przelewy bankowe (PBL) nie mają limitu czasowego na zwrot.
Endpoint API
POST https://panel.dpay.pl/api/v1/pbl/refund
Content-Type: application/json
Parametry zapytania
| Pole | Typ | Wymagane | Opis |
|---|---|---|---|
service | string | Tak | Nazwa serwisu z panelu |
transaction_id | string | Tak | Identyfikator transakcji do zwrotu |
value | string | Nie | Kwota zwrotu (np. "15.00"). Jeśli pominięte - zwrot pełny |
reason | string | Nie | Opis (powód) zwrotu, max 255 znaków. Uwaga: jeśli wyślesz to pole, musi wejść do checksum (patrz niżej) |
checksum | string | Tak | Suma kontrolna SHA-256 |
Nagłówki opcjonalne
| Nagłówek | Opis |
|---|---|
DigitalPayments-PSP-Message-ID | Własny identyfikator zwrotu (max 24 znaki, dłuższy zostanie obcięty). Kształtuje tytuł przelewu zwrotnego: dpay.pl zwrot {wartość nagłówka}. Bez nagłówka tytuł to dpay.pl {transaction_id}. Ten identyfikator wraca też w polu message odpowiedzi |
Aby wykonać zwrot częściowy, przekaż parametr value z kwotą mniejszą niż wartość transakcji. Kwota musi być liczbą nieujemną (np. "15.00").
Generowanie checksum
Serwer weryfikuje checksum, łącząc wartości wszystkich pól body w kolejności, w jakiej
występują w JSON (z pominięciem checksum), rozdzielone |, z secret_hash dołączonym
na końcu. Oznacza to, że:
- każde pole, które wysyłasz (również opcjonalne
valueireason), musi wejść do checksum, - kolejność pól w body musi odpowiadać kolejności wartości w checksumie,
- pominięcie wysłanego pola w checksumie (np.
reason) zakończy się błędem HTTP 401Unauthorized request.
Dla poniższych formuł pola wysyłaj w body dokładnie w tej kolejności.
Zwrot pełny
sha256({service}|{transaction_id}|{secret_hash})
Zwrot częściowy (z parametrem value)
sha256({service}|{transaction_id}|{value}|{secret_hash})
Zwrot z powodem (z parametrem reason)
Przy body w kolejności service, transaction_id, value, reason:
sha256({service}|{transaction_id}|{value}|{reason}|{secret_hash})
PHP
$service = 'abc123';
$transactionId = 'abc-def-123-456';
$secretHash = '9a8b7c6d5e4f3a2b1c0d';
// Zwrot pełny
$checksum = hash('sha256',
$service . '|' . $transactionId . '|' . $secretHash
);
// Zwrot częściowy
$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';
// Zwrot pełny
const checksum = crypto
.createHash('sha256')
.update(`${service}|${transactionId}|${secretHash}`)
.digest('hex');
// Zwrot częściowy
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'
# Zwrot pełny
data = f'{service}|{transaction_id}|{secret_hash}'
checksum = hashlib.sha256(data.encode('utf-8')).hexdigest()
# Zwrot częściowy
value = '15.00'
data_partial = f'{service}|{transaction_id}|{value}|{secret_hash}'
checksum_partial = hashlib.sha256(data_partial.encode('utf-8')).hexdigest()
Przykłady zapytań
Zwrot pełny - 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..."
}'
Zwrot częściowy - 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 dla zwrotu pełnego
// Generowanie 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 'Zwrot zrealizowany pomyślnie';
} else {
echo 'Błąd zwrotu: ' . $result['message'];
}
Odpowiedź API
Sukces (HTTP 200)
{
"status": "success",
"refund": true,
"message": "dpay.pl abc-def-123-456"
}
Pole message to identyfikator komunikatu zwrotu (tytuł przelewu zwrotnego), nie stały
tekst. Domyślnie ma postać dpay.pl {transaction_id}. Jeśli wysłałeś nagłówek
DigitalPayments-PSP-Message-ID, otrzymasz dpay.pl zwrot {wartość nagłówka}.
Błąd
{
"status": "error",
"refund": false,
"message": "Transaction does not exist"
}
Przy nieistniejącym transaction_id odpowiedź 404 może mieć uproszczoną postać (tekst
zamiast pełnego obiektu JSON z polem message). Nie polegaj wyłącznie na parsowaniu pola
message - decyzję podejmuj po kodzie HTTP.
Sprawdzenie dostępności zwrotu
Zanim zlecisz zwrot, możesz sprawdzić, czy jest on możliwy - bez wykonywania samego zwrotu:
POST https://panel.dpay.pl/api/v1/pbl/check-refund-availability
Content-Type: application/json
/api/v1/pbl/check-refund-availabilitySprawdź dostępność zwrotu bez jego wykonania.Pełny kontrakt w API Reference
Parametry i checksum są identyczne jak dla zwrotu (service, transaction_id, opcjonalnie
value; te same zasady liczenia checksum). Przykład odpowiedzi:
{
"status": "success",
"refund": true,
"message": "Refund available"
}
Gdy zwrot nie jest możliwy, otrzymasz status: "error", refund: false i komunikat przyczyny
(te same komunikaty co w tabeli błędów niżej).
Warianty GET
Oba endpointy mają też warianty GET z parametrami w ścieżce:
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}
Zalecamy jednak warianty POST - pozwalają przekazać value i reason oraz nie zostawiają
checksum w logach dostępowych pośredników.
Wielokrotne zwroty częściowe
Dla metod obsługujących wielokrotne zwroty częściowe (BLIK, karty, MB WAY oraz przelewy bankowe PBL) możesz wykonać wiele zwrotów do jednej transakcji, dopóki suma zwrotów nie przekroczy kwoty pierwotnej transakcji.
dpay.pl śledzi łączną zwróconą kwotę w polu refunded_amount. Przykład:
| Operacja | Kwota | refunded_amount | Pozostało |
|---|---|---|---|
| Transakcja | 100.00 PLN | 0.00 | 100.00 |
| Zwrot #1 | 30.00 PLN | 30.00 | 70.00 |
| Zwrot #2 | 25.00 PLN | 55.00 | 45.00 |
| Zwrot #3 | 45.00 PLN | 100.00 | 0.00 |
Próba zwrotu kwoty przekraczającej pozostałą wartość transakcji zakończy się błędem.
Najczęstsze błędy
| HTTP | Komunikat (message) | Przyczyna |
|---|---|---|
| 401 | Unauthorized request | Nieprawidłowy checksum. Sprawdź kolejność pól, Secret Hash i czy wszystkie wysłane pola (w tym value i reason) weszły do checksum |
| 404 | Transaction does not exist | Podany transaction_id nie istnieje |
| 402 | Transaction has not been paid | Transakcja nie została opłacona - zwrot możliwy tylko dla opłaconych |
| 410 | Transaction has already been refunded | Transakcja już zwrócona (dla PayPo/Twisto również po jednym zwrocie częściowym) |
| 400 | Refund amount exceeds available refund amount | Kwota zwrotu przekracza pozostałą do zwrotu wartość |
| 406 | Insufficient balance to process the refund | Niewystarczające saldo do realizacji zwrotu |
| 409 | Transaction already has a refund form submitted! | Dla tej transakcji zgłoszono już formularz zwrotu |
| 400 | The operator of the indicated channel does not support refunds | Metoda/kanał płatności nie obsługuje zwrotów tą drogą lub minął limit czasu (BLIK) |
| 411 | Charge transactions cannot be refunded | Transakcji typu charge nie można zwrócić |
| 400 | Refund failed | Błąd realizacji zwrotu - spróbuj ponownie lub skontaktuj się z obsługą |
Zwroty w panelu administracyjnym
Możesz również wykonać zwrot ręcznie:
- Zaloguj się do panel.dpay.pl.
- Przejdź do sekcji Transakcje.
- Znajdź transakcję do zwrotu (użyj filtrów lub wyszukiwarki).
- Kliknij na transakcję, aby otworzyć jej szczegóły.
- Kliknij przycisk Zwróć środki.
- Wybierz typ zwrotu - pełny lub częściowy (jeśli dostępny dla danej metody).
- W przypadku zwrotu częściowego podaj kwotę.
- Potwierdź zwrot.