Generowanie checksum (sum kontrolnych)
Checksum (suma kontrolna) to mechanizm zabezpieczający integralność danych przesyłanych między Twoim serwerem a dpay.pl. Kazde zapytanie do API wymaga prawidlowego checksum wygenerowanego z użyciem Twojego Secret Hash.
Zasada działania
Checksum to skrot SHA-256 wygenerowany z połączenia określonych pól rozdzielonych separatorem. dpay.pl weryfikuje checksum po swójej stronie - jeśli nie zgadza się z oczekiwanym, zapytanie zostaje odrzućone.
Secret Hash to klucz prywatny. Nigdy nie ujawniaj go publicznie, nie umieszczaj w kodzie frontendowym, nie commituj do repozytorium kodu. Przechowuj go wyłącznie w zmiennych środowiskowych serwera.
Formaty checksum dla roznych endpointow
Rejestracja płatności (payments/register)
sha256({service}|{SecretHash}|{value}|{url_success}|{url_fail}|{url_ipn})
| Element | Opis |
|---|---|
service | Nazwa serwisu z panelu |
SecretHash | Twoj klucz prywatny |
value | Kwota płatności (np. "29.99") |
url_success | URL powodzenia |
url_fail | URL niepowodzenia |
url_ipn | URL powiadomień IPN |
Separator: | (pipe)
Przed obliczeniem checksum serwer normalizuje value do dwóch miejsc po przecinku. Wartość "10" hashuje się jako "10.00", a "10.5" jako "10.50". Zawsze generuj checksum z wartości po normalizacji - inaczej sumy kontrolne się nie zgodzą.
Zwroty, szczegóły transakcji i wypłat (pbl/refund, pbl/details, pbl/check-refund-availability, pbl/withdraws/details)
Dla tych endpointów serwer nie używa stałej listy pól. Checksum jest liczony z wartości wszystkich pól body w kolejności ich wysłania (z pominięciem pola checksum), a na końcu doklejany jest Twój Secret Hash:
sha256({pole_1}|{pole_2}|...|{pole_N}|{SecretHash})
Przykład - zwrot pełny (body: service, transaction_id, checksum):
sha256({service}|{transaction_id}|{SecretHash})
Przykład - zwrot częściowy (body: service, transaction_id, value, checksum):
sha256({service}|{transaction_id}|{value}|{SecretHash})
Separator: | (pipe) - Secret Hash także jest poprzedzony separatorem.
Do checksum musi wejść każde pole wysłane w body - również pola opcjonalne (np. reason przy zwrocie). Jeśli dodasz pole do zapytania, a pominiesz je w checksum, suma się nie zgodzi. Nieprawidłowy checksum dla tych endpointów zwraca HTTP 401 z komunikatem Unauthorized request (nie 400).
Direct Carrier Billing (DCB) - rejestracja przez API płatności
Rejestracja DCB przez standardowy endpoint rejestracji płatności (transactionType: dcb_gateway) używa standardowego checksum rejestracji:
sha256({service}|{SecretHash}|{value}|{url_success}|{url_fail}|{url_ipn})
Pole value podlega tej samej normalizacji do dwóch miejsc po przecinku, co przy zwykłej rejestracji płatności.
Direct Carrier Billing (DCB) - legacy endpoint rejestracji (secure.dpay.pl/dcb/register)
sha256({GUID}|{SecretHash}|{value}|{url_success}|{url_fail})
| Element | Opis |
|---|---|
GUID | Identyfikator GUID Punktu Płatności |
SecretHash | Twoj klucz prywatny |
value | Kwota w złotych z dwoma miejscami po przecinku (np. "10.23") |
url_success | URL powodzenia |
url_fail | URL niepowodzenia |
Separator: | (pipe)
W body zapytania legacy pole value przekazujesz w groszach (np. 1023), ale do checksum wchodzi kwota przeliczona na złote z dwoma miejscami po przecinku (1023 w body to "10.23" w checksum). Checksum legacy nie zawiera url_ipn.
Weryfikacja podpisu IPN
sha256({id}{SecretHash}{amount}{email}{type}{attempt}{version}{custom})
| Element | Opis |
|---|---|
id | Identyfikator transakcji |
SecretHash | Twoj klucz prywatny |
amount | Kwota transakcji |
email | E-mail klienta |
type | Typ powiadomienia |
attempt | Numer próby dostarczenia |
version | Wersja protokolu |
custom | Dane wlasne |
Bez separatora - wartości są konkatenowane bezpośrednio, bez żadnego znaku rozdzielającego.
Weryfikacja podpisu IPN - DCB (Direct Carrier Billing)
Dla powiadomień typu dcb payload nie zawiera pola email:
sha256({id}{SecretHash}{amount}{type}{attempt}{version}{custom})
Bez separatora - tak samo jak standardowy IPN, ale bez email.
Przykłady implementacji
PHP
<?php
/**
* Generowanie checksum dla rejestracji płatności
*/
function generatePaymentChecksum(
string $service,
string $secretHash,
string $value,
string $urlSuccess,
string $urlFail,
string $urlIpn
): string {
$data = implode('|', [
$service,
$secretHash,
$value,
$urlSuccess,
$urlFail,
$urlIpn,
]);
return hash('sha256', $data);
}
/**
* Generowanie checksum dla zwrotów, szczegółów transakcji i wypłat.
*
* $fields = wartości WSZYSTKICH pól body w kolejności wysłania
* (bez pola checksum), łącznie z polami opcjonalnymi (np. reason).
*/
function generateRequestChecksum(array $fields, string $secretHash): string
{
$data = implode('|', [...array_values($fields), $secretHash]);
return hash('sha256', $data);
}
// Zwrot pełny (body: service, transaction_id, checksum)
// $checksum = generateRequestChecksum([$service, $transactionId], $secretHash);
// Zwrot częściowy (body: service, transaction_id, value, checksum)
// $checksum = generateRequestChecksum([$service, $transactionId, '15.00'], $secretHash);
/**
* Generowanie checksum dla DCB (legacy endpoint secure.dpay.pl/dcb/register).
*
* Kwota w złotych z dwoma miejscami po przecinku (np. "10.23"),
* mimo że w body zapytania value podajesz w groszach. Bez url_ipn.
* Rejestracja DCB przez API płatności używa generatePaymentChecksum().
*/
function generateDcbChecksum(
string $guid,
string $secretHash,
string $valueInZlotych,
string $urlSuccess,
string $urlFail
): string {
$data = implode('|', [
$guid,
$secretHash,
$valueInZlotych,
$urlSuccess,
$urlFail,
]);
return hash('sha256', $data);
}
/**
* Weryfikacja podpisu IPN
*/
function verifyIpnSignature(array $data, string $secretHash): bool
{
$expected = hash('sha256',
$data['id'] . $secretHash . $data['amount'] . $data['email']
. $data['type'] . $data['attempt'] . $data['version'] . $data['custom']
);
return hash_equals($expected, $data['signature']);
}
// === Przykład użycia ===
$service = getenv('DPAY_SERVICE');
$secretHash = getenv('DPAY_SECRET_HASH');
// Checksum płatności
$checksum = generatePaymentChecksum(
$service,
$secretHash,
'29.99',
'https://mojsklep.pl/sukces',
'https://mojsklep.pl/błąd',
'https://mojsklep.pl/api/ipn'
);
echo $checksum; // np. "a1b2c3d4e5f6..."
JavaScript (Node.js)
const crypto = require('crypto');
/**
* Generowanie checksum dla rejestracji płatności
*/
function generatePaymentChecksum(service, secretHash, value, urlSuccess, urlFail, urlIpn) {
const data = [service, secretHash, value, urlSuccess, urlFail, urlIpn].join('|');
return crypto.createHash('sha256').update(data).digest('hex');
}
/**
* Generowanie checksum dla zwrotów, szczegółów transakcji i wypłat.
* fields = wartości WSZYSTKICH pól body w kolejności wysłania
* (bez pola checksum), łącznie z polami opcjonalnymi (np. reason).
*/
function generateRequestChecksum(fields, secretHash) {
const data = [...fields, secretHash].join('|');
return crypto.createHash('sha256').update(data).digest('hex');
}
// Zwrot pełny (body: service, transaction_id, checksum):
// generateRequestChecksum([service, transactionId], secretHash)
// Zwrot częściowy (body: service, transaction_id, value, checksum):
// generateRequestChecksum([service, transactionId, '15.00'], secretHash)
/**
* Generowanie checksum dla DCB (legacy endpoint secure.dpay.pl/dcb/register).
* Kwota w złotych z dwoma miejscami po przecinku (np. "10.23"),
* mimo że w body zapytania value podajesz w groszach. Bez url_ipn.
* Rejestracja DCB przez API płatności używa generatePaymentChecksum().
*/
function generateDcbChecksum(guid, secretHash, valueInZlotych, urlSuccess, urlFail) {
const data = [guid, secretHash, valueInZlotych, urlSuccess, urlFail].join('|');
return crypto.createHash('sha256').update(data).digest('hex');
}
/**
* Weryfikacja podpisu IPN
*/
function verifyIpnSignature(data, secretHash) {
const raw = [
data.id, secretHash, data.amount, data.email,
data.type, data.attempt, data.version, data.custom,
].join('');
const expected = crypto.createHash('sha256').update(raw).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(data.signature, 'hex')
);
}
// === Przykład użycia ===
const service = process.env.DPAY_SERVICE;
const secretHash = process.env.DPAY_SECRET_HASH;
const checksum = generatePaymentChecksum(
service,
secretHash,
'29.99',
'https://mojsklep.pl/sukces',
'https://mojsklep.pl/błąd',
'https://mojsklep.pl/api/ipn'
);
console.log(checksum);
Python
import hashlib
import hmac
import os
def generate_payment_checksum(
service: str,
secret_hash: str,
value: str,
url_success: str,
url_fail: str,
url_ipn: str,
) -> str:
"""Generowanie checksum dla rejestracji płatności."""
data = '|'.join([service, secret_hash, value, url_success, url_fail, url_ipn])
return hashlib.sha256(data.encode('utf-8')).hexdigest()
def generate_request_checksum(fields: list[str], secret_hash: str) -> str:
"""Checksum dla zwrotów, szczegółów transakcji i wypłat.
fields - wartości WSZYSTKICH pól body w kolejności wysłania
(bez pola checksum), łącznie z polami opcjonalnymi (np. reason).
"""
data = '|'.join([*fields, secret_hash])
return hashlib.sha256(data.encode('utf-8')).hexdigest()
# Zwrot pełny (body: service, transaction_id, checksum):
# generate_request_checksum([service, transaction_id], secret_hash)
# Zwrot częściowy (body: service, transaction_id, value, checksum):
# generate_request_checksum([service, transaction_id, '15.00'], secret_hash)
def generate_dcb_checksum(
guid: str,
secret_hash: str,
value_in_zlotych: str,
url_success: str,
url_fail: str,
) -> str:
"""Checksum dla DCB (legacy endpoint secure.dpay.pl/dcb/register).
Kwota w złotych z dwoma miejscami po przecinku (np. "10.23"),
mimo że w body zapytania value podajesz w groszach. Bez url_ipn.
Rejestracja DCB przez API płatności używa generate_payment_checksum().
"""
data = '|'.join([guid, secret_hash, value_in_zlotych, url_success, url_fail])
return hashlib.sha256(data.encode('utf-8')).hexdigest()
def verify_ipn_signature(data: dict, secret_hash: str) -> bool:
"""Weryfikacja podpisu IPN."""
raw = (
data['id'] + secret_hash + data['amount'] + data['email']
+ data['type'] + str(data['attempt']) + data['version'] + data['custom']
)
expected = hashlib.sha256(raw.encode('utf-8')).hexdigest()
return hmac.compare_digest(expected, data['signature'])
# === Przykład użycia ===
service = os.environ['DPAY_SERVICE']
secret_hash = os.environ['DPAY_SECRET_HASH']
checksum = generate_payment_checksum(
service,
secret_hash,
'29.99',
'https://mojsklep.pl/sukces',
'https://mojsklep.pl/błąd',
'https://mojsklep.pl/api/ipn',
)
print(checksum)
Najczęstsze błędy
1. Nieprawidłowa kolejność pol
Pola muszą byc w dokładnie takiej kolejności, jak podano w dokumentacji. Zamiana kolejności spowoduje wygenerowanie innego checksum.
2. Dodatkowe białoznaki
Upewnij się, ze wartości pól nie zawieraja dodatkowych spacji, znaków nowej linii ani innych białoznaków:
// ZLE - dodatkowa spacja
$value = '29.99 ';
// DOBRZE
$value = trim('29.99');
3. Kodowanie znaków
Użyj kodowania UTF-8. Znaki specjalne w URL-ach (np. polskie znaki) mogą powodowac rozne wyniki w zaleznosci od kodowania.
4. Kwota bez dwóch miejsc po przecinku
Serwer normalizuje value do dwóch miejsc po przecinku przed liczeniem checksum rejestracji:
// ZLE - serwer zahashuje "10" jako "10.00" i sumy się nie zgodzą
$value = '10';
// DOBRZE
$value = number_format(10, 2, '.', ''); // "10.00"
5. Pominięcie pola opcjonalnego (zwroty, szczegóły, wypłaty)
Dla endpointów pbl/refund, pbl/details, pbl/check-refund-availability i pbl/withdraws/details do checksum wchodzą wszystkie pola body w kolejności wysłania. Jeśli dodasz opcjonalne pole (np. reason), a nie uwzględnisz go w checksum, otrzymasz HTTP 401 Unauthorized request.
6. Porownywanie w sposób odporny na timing attacks
Przy weryfikacji podpisu IPN użyj porownywania odpornego na ataki czasowe:
// ZLE - podatne na timing attack
if ($expected === $data['signature']) { ... }
// DOBRZE - odporne na timing attack
if (hash_equals($expected, $data['signature'])) { ... }
// DOBRZE - Node.js
crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(signature, 'hex')
);
# DOBRZE - Python
import hmac
hmac.compare_digest(expected, signature)
Debugowanie checksum
Jeśli otrzymujesz błąd Invalid checksum (rejestracja płatności, HTTP 400) lub Unauthorized request (zwroty, szczegóły transakcji, wypłaty - HTTP 401), wykonaj nastepujace kroki:
- Wypisz dane wejsciowe - sprawdz dokładna wartość każdego pola
- Sprawdz separator - dla rejestracji płatności, zwrotów i DCB używaj separatora
|(pipe). Dla podpisu IPN wartości konkatenuj bez separatora - Zweryfikuj normalizację kwoty -
valuew checksum rejestracji zawsze z dwoma miejscami po przecinku ("10.00", nie"10") - Sprawdz komplet pól - dla zwrotów, szczegółów i wypłat do checksum wchodzą wszystkie pola body w kolejności wysłania, także opcjonalne
- Zweryfikuj Secret Hash - upewnij się, ze używasz aktualnego klucza z panelu
- Porownaj z narzedziem online - użyj dowolnego generatora SHA-256 i porównaj wynik z tym, co generuje Twoj kod
// Debugowanie - wypisz dane przed hashowaniem
$raw = $service . '|' . $secretHash . '|' . $value . '|' .
$urlSuccess . '|' . $urlFail . '|' . $urlIpn;
error_log('Checksum input: ' . $raw);
error_log('Checksum output: ' . hash('sha256', $raw));