Przejdź do głównej zawartości

Obsługa IPN (Instant Payment Notification)

IPN to mechanizm powiadamiania Twojego serwera o udanej transakcji. dpay.pl wysyła zapytanie POST na adres url_ipn podany przy rejestracji transakcji. Pole jest opcjonalne - bez adresu dpay.pl nie wysyła IPN o tej płatności.

informacja

IPN jest wysyłany wyłącznie dla udanych transakcji (status opłacony). Nieudane płatności nie generują powiadomień IPN - odmowy z kodem przyczyny, zwroty i wypłaty zgłaszają webhooki.

Dlaczego IPN jest ważny?​

Przekierowanie klienta na url_success nie oznacza, że płatność została zrealizowana - klient mógł zamknąć przeglądarkę, stracić połączenie lub zmanipulować URL. Tylko IPN jest wiarygodnym źródłem informacji o statusie płatności.

Zasada

Nigdy nie aktualizuj statusu zamówienia na podstawie przekierowania klienta. Zawsze czekaj na potwierdzenie przez IPN.

Schemat IPN v1​

dpay.pl wysyła na Twój endpoint zapytanie POST z nagłówkiem Content-Type: application/json i następującym ciałem:

{
"id": "abc-def-123-456",
"amount": "29.99",
"email": "klient@example.com",
"type": "transfer",
"attempt": 1,
"version": 1,
"custom": "order-789",
"signature": "a1b2c3d4e5f6..."
}

Opis pól​

PoleTypOpis
idstringIdentyfikator transakcji dpay.pl
amountstringKwota transakcji
emailstringAdres e-mail klienta (jeśli podany)
typestringTyp powiadomienia: "transfer" lub "dcb"
attemptintegerNumer próby dostarczenia (aktualnie zawsze 1)
versionintegerWersja protokołu IPN (aktualnie 1)
customstringDane własne przekazane przy rejestracji (opcjonalne)
signaturestringPodpis cyfrowy do weryfikacji autentyczności

Typy powiadomień​

TypOpis
transferStandardowa płatność zakończona sukcesem - możesz zrealizować zamówienie
dcbPłatność Direct Carrier Billing - payload nie zawiera pola email
Przechwycenia kart

Przechwycenie (capture) preautoryzowanej płatności kartowej nie generuje IPN. O każdym przechwyceniu, także częściowym, informuje zdarzenie webhook payment.captured.

Weryfikacja podpisu (signature)​

Zawsze weryfikuj podpis IPN, aby upewnić się, że powiadomienie pochodzi od dpay.pl, a nie od osoby trzeciej.

Podpis generowany jest algorytmem SHA-256 z bezpośredniej konkatenacji wartości pól i Twojego Secret Hash:

sha256({id}{SecretHash}{amount}{email}{type}{attempt}{version}{custom})
uwaga

W przeciwieństwie do checksum rejestracji płatności, podpis IPN nie używa separatora |. Wartości są konkatenowane bezpośrednio.

DCB (Direct Carrier Billing)​

Dla powiadomień typu dcb payload nie zawiera pola email, a formuła sygnatury jest inna:

sha256({id}{SecretHash}{amount}{type}{attempt}{version}{custom})

PHP​

<?php
// Odczytaj dane IPN
$data = json_decode(file_get_contents('php://input'), true);

if (!$data) {
http_response_code(400);
echo 'Invalid payload';
exit;
}

$privateKey = 'your_secret_hash'; // Twój Secret Hash

// Wygeneruj oczekiwany podpis (bez separatorów - bezpośrednia konkatenacja)
$checksum = hash('sha256',
$data['id'] . $privateKey . $data['amount'] . $data['email']
. $data['type'] . $data['attempt'] . $data['version'] . $data['custom']
);

// Zweryfikuj podpis
if ($checksum !== $data['signature']) {
// Podpis nieprawidłowy - odrzuć powiadomienie
http_response_code(200);
echo 'OK';
exit;
}

// Podpis prawidłowy - przetwórz płatność
$transactionId = $data['id'];
$amount = $data['amount'];
$type = $data['type'];

switch ($type) {
case 'transfer':
// Standardowa płatność - zaktualizuj status zamówienia na "opłacone"
markOrderAsPaid($transactionId, $amount);
break;
}

// Odpowiedź z treścią "OK" - dpay.pl nie będzie ponawiać próby
http_response_code(200);
echo 'OK';

Node.js (Express)​

const crypto = require('crypto');
const express = require('express');
const app = express();

app.use(express.json());

app.post('/api/ipn', (req, res) => {
const data = req.body;
const secretHash = process.env.DPAY_SECRET_HASH;

// Wygeneruj oczekiwany podpis (bez separatorów - bezpośrednia konkatenacja)
const expectedSignature = crypto
.createHash('sha256')
.update(
[data.id, secretHash, data.amount, data.email,
data.type, data.attempt, data.version, data.custom].join('')
)
.digest('hex');

// Zweryfikuj podpis
if (expectedSignature !== data.signature) {
console.error('Nieprawidłowy podpis IPN');
return res.status(200).send('OK');
}

// Przetwórz płatność
switch (data.type) {
case 'transfer':
markOrderAsPaid(data.id, data.amount);
break;
}

res.status(200).send('OK');
});

Python (Flask)​

import hashlib
from flask import Flask, request

app = Flask(__name__)

@app.route('/api/ipn', methods=['POST'])
def handle_ipn():
data = request.get_json()
secret_hash = os.environ['DPAY_SECRET_HASH']

# Wygeneruj oczekiwany podpis (bez separatorów - bezpośrednia konkatenacja)
raw = (
data['id'] + secret_hash + data['amount'] + data['email']
+ data['type'] + str(data['attempt']) + str(data['version']) + data['custom']
)
expected_signature = hashlib.sha256(raw.encode('utf-8')).hexdigest()

# Zweryfikuj podpis
if expected_signature != data['signature']:
return 'OK', 200 # Odrzuć cicho

# Przetwórz płatność
if data['type'] == 'transfer':
mark_order_as_paid(data['id'], data['amount'])

return 'OK', 200

Najlepsze praktyki​

1. Zawsze odpowiadaj treścią "OK"​

dpay.pl uznaje IPN za dostarczony wyłącznie, gdy treść (body) Twojej odpowiedzi to dokładnie "OK" (białe znaki na początku i końcu są ignorowane). Kod HTTP nie jest sprawdzany - odpowiedź 200 z inną treścią (np. stroną HTML, JSON-em, pustym body) spowoduje ponowienie IPN.

HTTP/1.1 200 OK
Content-Type: text/plain

OK
ostrzeżenie

Upewnij się, że Twój framework nie dokleja do odpowiedzi niczego poza tekstem OK (np. debug toolbar, ramka HTML szablonu). Każdy dodatkowy znak w body oznacza, że IPN zostanie uznany za niedostarczony i będzie ponawiany.

2. Idempotentność​

Twój endpoint IPN może otrzymać to samo powiadomienie wielokrotnie (np. z powodu timeoutu sieci). Upewnij się, że przetwarzasz każdą transakcję tylko raz:

// Sprawdź, czy transakcja nie została już przetworzona
$order = getOrderByTransactionId($data['id']);
if ($order && $order['status'] === 'paid') {
// Już przetworzone - odpowiedz OK i zakończ
http_response_code(200);
echo 'OK';
exit;
}

3. Weryfikuj kwotę​

Zawsze sprawdzaj, czy kwota z IPN zgadza się z kwotą zamówienia w Twojej bazie danych:

$order = getOrderByTransactionId($data['id']);
if (floatval($data['amount']) !== floatval($order['amount'])) {
// Niezgodność kwoty - możliwa próba oszustwa
logSecurityAlert('Amount mismatch', $data);
http_response_code(200);
echo 'OK';
exit;
}

4. Nie wykonuj długich operacji synchronicznie​

Jeśli przetwarzanie płatności wymaga dłuższego czasu (np. generowanie pliku, wysyłanie e-maila), dodaj zadanie do kolejki i odpowiedz natychmiast:

// Dodaj do kolejki zamiast przetwarzać synchronicznie
$queue->push('process_payment', [
'transaction_id' => $data['id'],
'amount' => $data['amount'],
]);

http_response_code(200);
echo 'OK';

5. Logowanie​

Loguj wszystkie przychodzące IPN do celów diagnostycznych:

file_put_contents(
'/var/log/dpay-ipn.log',
date('Y-m-d H:i:s') . ' ' . json_encode($data) . PHP_EOL,
FILE_APPEND
);

Mechanizm ponawiania​

Jeśli treść odpowiedzi Twojego serwera nie będzie równa "OK" (albo wystąpi timeout lub błąd połączenia), dpay.pl ponowi próbę dostarczenia IPN:

PróbaOpóźnienieCzas od pierwszej próby
1Natychmiast0 min
22 min2 min
34 min6 min
48 min14 min
516 min30 min
632 min~1 godz
764 min~2 godz
8128 min~4 godz
9256 min~8.5 godz
10512 min~17 godz

Opóźnienia stosują schemat exponential backoff (2^n minut). Po 10 nieudanych próbach powiadomienie nie będzie już ponawiane. Możesz ręcznie sprawdzić status transakcji w panelu administracyjnym.

Wyłączenie ponawiania​

Ponawianie IPN można wyłączyć - ustawienie jest dostępne per serwis (Punkt Płatności) lub per konto użytkownika (ustawienie serwisu ma pierwszeństwo przed ustawieniem konta; domyślnie ponawianie jest włączone). Po wyłączeniu dpay.pl wykona tylko jedną próbę dostarczenia IPN. Przełącznik znajdziesz w ustawieniach Punktu Płatności w panelu.

Samodzielne odpytywanie o status transakcji​

Oprócz oczekiwania na IPN, możesz samodzielnie sprawdzić status transakcji za pomocą API. Jest to przydatne w kilku scenariuszach:

  • IPN nie dotarł - np. z powodu problemów sieciowych po stronie Twojego serwera
  • Reconciliation - okresowe uzgadnianie statusów transakcji z bazą danych
  • Dodatkowe zabezpieczenie - jako uzupełnienie mechanizmu IPN

Endpoint​

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

Parametry zapytania​

PoleTypWymaganeOpis
servicestringtakNazwa serwisu (Punktu Płatności)
transaction_idstringtakUUID transakcji
checksumstringtakSuma kontrolna - sha256(service|transaction_id|secret_hash)
Checksum zależy od kolejności pól w body

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 na końcu. Formuła sha256(service|transaction_id|secret_hash) działa tylko wtedy, gdy pola w body wyślesz dokładnie w tej kolejności: najpierw service, potem transaction_id. Inna kolejność lub dodatkowe pola nieuwzględnione w checksumie dadzą błąd autoryzacji.

Przykład odpowiedzi​

{
"status": "success",
"transaction": {
"id": "abc-def-123-456",
"value": "29.99",
"rate": 2,
"minimal_fee": 20,
"permanent_fee": 0,
"status": "paid",
"payment_method": "blik",
"urls": {
"success": "https://mojsklep.pl/sukces",
"fail": "https://mojsklep.pl/blad",
"ipn": "https://mojsklep.pl/api/ipn"
},
"creation_date": "2026-01-15 12:30:00",
"payment_date": "2026-01-15 12:31:45",
"settled": true,
"refunded": false,
"refunded_amount": 10.00,
"available_refund_amount": 19.99,
"fully_refunded": false,
"direct": false
},
"payer": {},
"refunds": [
{
"payment_id": "ZWROT-abc-def-123-456-QWERTY12",
"value": -10.00,
"status": "paid",
"creation_date": "2026-01-16 09:00:00",
"payment_date": "2026-01-16 09:00:00"
}
]
}

Pola związane ze zwrotami​

PoleTypOpis
transaction.refundedboolCzy transakcja została zwrócona (flaga legacy)
transaction.refunded_amountnumberSuma zwróconych kwot w PLN
transaction.available_refund_amountnumberPozostała kwota możliwa do zwrotu w PLN (value - refunded_amount, minimum 0)
transaction.fully_refundedboolCzy transakcja jest w pełni zwrócona. Dla metod wspierających wielokrotne częściowe zwroty (np. BLIK, karty) liczone jako available_refund_amount == 0; dla pozostałych metod odpowiada polu refunded
refundsarrayLista zwrotów powiązanych z transakcją. Zawsze obecna (pusta tablica przy braku zwrotów). Sortowanie rosnąco po creation_date
refunds[].payment_idstringIdentyfikator zwrotu w formacie ZWROT-{parent_payment_id}-{8 znaków}
refunds[].valuenumberKwota zwrotu w PLN, zawsze ujemna (np. -50.00)
refunds[].statusstringStatus zwrotu (patrz sekcja "Statusy transakcji" poniżej). Domyślnie paid
refunds[].creation_datestringData utworzenia zwrotu (YYYY-MM-DD HH:MM:SS)
refunds[].payment_datestringData realizacji zwrotu (YYYY-MM-DD HH:MM:SS)

Statusy transakcji​

StatusOpis
paidTransakcja opłacona
createdTransakcja utworzona, oczekuje na płatność
processingPłatność w trakcie przetwarzania
expiredTransakcja wygasła
capturedPłatność kartowa przechwycona

Przykład - PHP​

<?php
$service = 'my_service';
$transactionId = 'abc-def-123-456';
$secretHash = 'your_secret_hash';

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

$response = file_get_contents('https://panel.dpay.pl/api/v1/pbl/details', false,
stream_context_create([
'http' => [
'method' => 'POST',
'header' => 'Content-Type: application/json',
'content' => json_encode([
'service' => $service,
'transaction_id' => $transactionId,
'checksum' => $checksum,
]),
],
])
);

$result = json_decode($response, true);

if ($result['status'] === 'success' && $result['transaction']['status'] === 'paid') {
// Transakcja opłacona - zaktualizuj status zamówienia
markOrderAsPaid($transactionId, $result['transaction']['value']);
}
wskazówka

Samodzielne odpytywanie o status to dobra praktyka jako mechanizm awaryjny. Możesz np. uruchomić zadanie cron, które co kilka minut sprawdza statusy transakcji oczekujących na płatność dłużej niż 15 minut.

Testowanie IPN​

  1. Najprościej przetestujesz IPN, wykonując transakcję testową w środowisku testowym - po jej opłaceniu dpay.pl wyśle prawdziwe IPN na Twój url_ipn.
  2. Do testów lokalnych użyj narzędzi typu ngrok, aby udostępnić lokalny serwer pod publicznym adresem URL.
informacja

Opcja Wyślij testowe IPN w panelu jest dostępna wyłącznie dla usług Direct Carrier Billing (DCB). Dla zwykłych Punktów Płatności skorzystaj z transakcji testowych.

# Uruchom tunel ngrok
ngrok http 3000

# Użyj wygenerowanego adresu jako url_ipn:
# https://abc123.ngrok.io/api/ipn