Przejdź do głównej zawartości

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

MetodaZwrot pełnyZwrot częściowyWiele zwrotów częściowych
BLIKTakTakTak
BLIK BNPLTakTakTak
Przelewy bankowe (PBL)TakTakTak
Karty płatnicze (Visa / Mastercard)TakTakTak
MB WAYTakTakTak
PayPoTakTakNie - tylko jeden zwrot
TwistoTakTakNie - tylko jeden zwrot
PayPo i Twisto - 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.

Limit czasowy

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

PoleTypWymaganeOpis
servicestringTakNazwa serwisu z panelu
transaction_idstringTakIdentyfikator transakcji do zwrotu
valuestringNieKwota zwrotu (np. "15.00"). Jeśli pominięte - zwrot pełny
reasonstringNieOpis (powód) zwrotu, max 255 znaków. Uwaga: jeśli wyślesz to pole, musi wejść do checksum (patrz niżej)
checksumstringTakSuma kontrolna SHA-256

Nagłówki opcjonalne

NagłówekOpis
DigitalPayments-PSP-Message-IDWł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
Zwrot częściowy

Aby wykonać zwrot częściowy, przekaż parametr value z kwotą mniejszą niż wartość transakcji. Kwota musi być liczbą nieujemną (np. "15.00").

Generowanie checksum

Checksum liczony ze WSZYSTKICH pól w kolejności wysłania

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 value i reason), 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 401 Unauthorized 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"
}
uwaga

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
POST/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:

OperacjaKwotarefunded_amountPozostało
Transakcja100.00 PLN0.00100.00
Zwrot #130.00 PLN30.0070.00
Zwrot #225.00 PLN55.0045.00
Zwrot #345.00 PLN100.000.00
uwaga

Próba zwrotu kwoty przekraczającej pozostałą wartość transakcji zakończy się błędem.

Najczęstsze błędy

HTTPKomunikat (message)Przyczyna
401Unauthorized requestNieprawidłowy checksum. Sprawdź kolejność pól, Secret Hash i czy wszystkie wysłane pola (w tym value i reason) weszły do checksum
404Transaction does not existPodany transaction_id nie istnieje
402Transaction has not been paidTransakcja nie została opłacona - zwrot możliwy tylko dla opłaconych
410Transaction has already been refundedTransakcja już zwrócona (dla PayPo/Twisto również po jednym zwrocie częściowym)
400Refund amount exceeds available refund amountKwota zwrotu przekracza pozostałą do zwrotu wartość
406Insufficient balance to process the refundNiewystarczające saldo do realizacji zwrotu
409Transaction already has a refund form submitted!Dla tej transakcji zgłoszono już formularz zwrotu
400The operator of the indicated channel does not support refundsMetoda/kanał płatności nie obsługuje zwrotów tą drogą lub minął limit czasu (BLIK)
411Charge transactions cannot be refundedTransakcji typu charge nie można zwrócić
400Refund failedBłąd realizacji zwrotu - spróbuj ponownie lub skontaktuj się z obsługą

Zwroty w panelu administracyjnym

Możesz również wykonać zwrot ręcznie:

  1. Zaloguj się do panel.dpay.pl.
  2. Przejdź do sekcji Transakcje.
  3. Znajdź transakcję do zwrotu (użyj filtrów lub wyszukiwarki).
  4. Kliknij na transakcję, aby otworzyć jej szczegóły.
  5. Kliknij przycisk Zwróć środki.
  6. Wybierz typ zwrotu - pełny lub częściowy (jeśli dostępny dla danej metody).
  7. W przypadku zwrotu częściowego podaj kwotę.
  8. Potwierdź zwrot.