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 skonfigurowany dla danej transakcji.
IPN jest wysyłany wyłącznie dla udanych transakcji (status opłacony). Nieudane płatności nie generują powiadomień IPN.
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.
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
| Pole | Typ | Opis |
|---|---|---|
id | string | Identyfikator transakcji dpay.pl |
amount | string | Kwota transakcji |
email | string | Adres e-mail klienta (jeśli podany) |
type | string | Typ powiadomienia: "transfer" lub "capture" |
attempt | integer | Numer próby dostarczenia powiadomienia (1, 2, 3...) |
version | string | Wersja protokołu IPN (aktualnie "1") |
custom | string | Dane własne przekazane przy rejestracji (opcjonalne) |
capture_payment_id | string | Identyfikator przechwycenia - tylko dla typu "capture" |
signature | string | Podpis cyfrowy do weryfikacji autentyczności |
Typy powiadomień
| Typ | Opis |
|---|---|
transfer | Standardowa płatność zakończona sukcesem - możesz zrealizować zamówienie |
capture | Przechwycenie płatności kartowej - zawiera dodatkowe pole capture_payment_id |
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 połączenia wartości pól i Twojego Secret Hash:
sha256({id}|{SecretHash}|{amount}|{email}|{type}|{attempt}|{version}|{custom})
Separator: | (pipe) - identycznie jak przy generowaniu checksum płatności.
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
$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;
case 'capture':
// Przechwycenie karty - przetwórz z capture_payment_id
$captureId = $data['capture_payment_id'];
markOrderAsCaptured($transactionId, $amount, $captureId);
break;
}
// Odpowiedź 200 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
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;
case 'capture':
markOrderAsCaptured(data.id, data.amount, data.capture_payment_id);
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
raw = '|'.join([
data['id'], secret_hash, data['amount'], data['email'],
data['type'], str(data['attempt']), 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'])
elif data['type'] == 'capture':
mark_order_as_captured(data['id'], data['amount'], data['capture_payment_id'])
return 'OK', 200
Najlepsze praktyki
1. Zawsze odpowiadaj kodem HTTP 200
dpay.pl oczekuje odpowiedzi z kodem 200 i treścią "OK". Jeśli Twój serwer odpowie innym kodem, dpay.pl ponowi próbę dostarczenia IPN.
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 Twój serwer nie odpowie kodem 200, dpay.pl ponowi próbę dostarczenia IPN:
| Próba | Opóźnienie | Czas od pierwszej próby |
|---|---|---|
| 1 | Natychmiast | 0 min |
| 2 | 2 min | 2 min |
| 3 | 4 min | 6 min |
| 4 | 8 min | 14 min |
| 5 | 16 min | 30 min |
| 6 | 32 min | ~1 godz |
| 7 | 64 min | ~2 godz |
| 8 | 128 min | ~4 godz |
| 9 | 256 min | ~8.5 godz |
| 10 | 512 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.
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
| Pole | Typ | Wymagane | Opis |
|---|---|---|---|
service | string | tak | Nazwa serwisu (Punktu Płatności) |
transaction_id | string | tak | UUID transakcji |
checksum | string | tak | Suma kontrolna - sha256(service|transaction_id|secret_hash) |
Przykład odpowiedzi
{
"service": "success",
"transaction": {
"transaction_id": "abc-def-123-456",
"value": "29.99",
"status": "paid",
"creation_date": "2026-01-15 12:30:00",
"payment_date": "2026-01-15 12:31:45",
"settled": true,
"refunded": false
}
}
Statusy transakcji
| Status | Opis |
|---|---|
paid | Transakcja opłacona |
created | Transakcja utworzona, oczekuje na płatność |
processing | Płatność w trakcie przetwarzania |
canceled | Transakcja anulowana |
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['service'] === 'success' && $result['transaction']['status'] === 'paid') {
// Transakcja opłacona - zaktualizuj status zamówienia
markOrderAsPaid($transactionId, $result['transaction']['value']);
}
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
- W panelu panel.dpay.pl przejdź do Punktu Płatności.
- Użyj opcji Wyślij testowe IPN, aby wysłać przykładowe powiadomienie.
- Do testów lokalnych użyj narzędzi typu ngrok, aby udostępnić lokalny serwer pod publicznym adresem URL.
# Uruchom tunel ngrok
ngrok http 3000
# Użyj wygenerowanego adresu jako url_ipn:
# https://abc123.ngrok.io/api/ipn