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 skonfigurowany dla danej transakcji.

informacja

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.

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 "capture"
attemptintegerNumer próby dostarczenia powiadomienia (1, 2, 3...)
versionstringWersja protokołu IPN (aktualnie "1")
customstringDane własne przekazane przy rejestracji (opcjonalne)
capture_payment_idstringIdentyfikator przechwycenia - tylko dla typu "capture"
signaturestringPodpis cyfrowy do weryfikacji autentyczności

Typy powiadomień

TypOpis
transferStandardowa płatność zakończona sukcesem - możesz zrealizować zamówienie
capturePrzechwycenie 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ó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.

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)

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

StatusOpis
paidTransakcja opłacona
createdTransakcja utworzona, oczekuje na płatność
processingPłatność w trakcie przetwarzania
canceledTransakcja 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']);
}
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. W panelu panel.dpay.pl przejdź do Punktu Płatności.
  2. Użyj opcji Wyślij testowe IPN, aby wysłać przykładowe powiadomienie.
  3. 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