Webhooki
Webhooki to Twoje źródło prawdy o zdarzeniach w dpay Connect. Zamiast odpytywać API w pętli, dostajesz push na swój endpoint zawsze, gdy zmieni się status onboardingu merchanta, płatności, zwrotu, wypłaty albo zgody na operacje w jego imieniu.
Konfiguracja
Endpointy dodajesz w portalu partnera, w zakładce Webhooki:
- Adres - HTTPS na porcie 443, publiczny (adresy sieci prywatnych i domeny dpay są odrzucane).
- Zdarzenia - wszystkie (także dodane w przyszłości) albo wybrane typy.
- Tryb - produkcyjny albo testowy. Konto partnera w sandboxie ma tylko endpointy testowe i dostaje wyłącznie zdarzenia z
livemode: false.
Każdy endpoint ma własny sekret podpisu (whsec_...), pokazywany tylko raz - przy dodaniu endpointu i przy rotacji. Możesz mieć do 16 endpointów.
W portalu zobaczysz też log dostarczeń z każdą próbą (kod HTTP, czas odpowiedzi, początek treści odpowiedzi), ponowisz pojedyncze dostarczenie albo wszystkie nieudane z wybranego okresu i wyślesz zdarzenie testowe webhook.test.
Zdarzenia pojedynczej płatności możesz też odebrać na adres podany przy jej rejestracji - patrz obiekt webhook w płatnościach on-behalf.
Katalog zdarzeń
Zdarzenia Twoich merchantów
Dostajesz zdarzenia wszystkich merchantów, którzy przeszli onboarding w Twoim kanale albo płacą przez Twoją platformę - z polem merchant_ref i tym samym obiektem, który dostaje merchant.
| Zdarzenie | Kiedy |
|---|---|
payment.succeeded | Płatność opłacona. |
payment.failed | Płatność odrzucona albo wygasła - z kodem odmowy w failure. |
payment.captured | Dopełniona preautoryzacja karty. |
refund.succeeded, refund.failed | Zwrot zrealizowany albo odrzucony. |
recurring_payment.activated, recurring_payment.canceled, recurring_payment.expired, recurring_payment.declined | Zmiana płatności cyklicznej. |
payout.paid, payout.failed | Wypłata na rachunek merchanta zrealizowana albo odrzucona. |
Zdarzenia partnera
| Zdarzenie | Kiedy |
|---|---|
merchant.updated | Zmienił się status onboardingu merchanta - nowy status w data.object.status (patrz niżej). |
reverification.submitted | Merchant złożył dane w reweryfikacji okresowej. |
authorization.revoked | Merchant cofnął zgodę na operacje w jego imieniu. Cofnięcie obejmuje wszystkie zakresy naraz i działa natychmiast - przestań wysyłać żądania On-Behalf-Of dla tego merchant_ref. |
invitation.identity_verified | Zaproszona osoba zweryfikowała tożsamość (tylko ścieżka eID - nie polegaj na tym zdarzeniu, ścieżka manualna go nie emituje). |
invitation.completed | Zaproszona osoba ukończyła swój przepływ - jedyny gwarantowany terminal sukcesu zaproszenia. |
invitation.signed | Zaproszony reprezentant podpisał umowę. |
partner_payout.paid, partner_payout.declined | dpay wypłacił albo odrzucił wypłatę Twojej marży (patrz rozliczenia ISV). Te zdarzenia dotyczą Ciebie, nie merchanta - nie mają merchant_ref. |
Katalog może się rozszerzać. Zaprojektuj handler tak, aby nieznany type był ignorowany (a nie powodował błędu). Nie zakładaj zamkniętej listy.
Status onboardingu (merchant.updated)
data.object.status to ogólny status konta merchanta (patrz cykl życia). Każda zmiana daje jedno zdarzenie.
| Status | Znaczenie |
|---|---|
in_progress | Onboarding w toku (dane, dokumenty). |
awaiting_signature | Umowa czeka na podpis. |
partially_signed | Część reprezentantów podpisała umowę. |
agreement_signed | Umowa podpisana. |
pending_transfer | Oczekiwanie na opłatę aktywacyjną. |
under_review | Weryfikacja manualna dpay. |
completed | Konto aktywne. |
blocked | Konto zablokowane. |
Podpis umowy na koncie, które jest już aktywne, zablokowane albo dalej w procesie, nie cofa statusu - zdarzenie pokazuje wtedy bieżący stan konta.
Koperta zdarzenia
Każdy webhook to POST z JSON-em o tej samej kopercie. Typ zdarzenia niesie pole type, a obiekt, którego dotyczy - data.object.
POST /webhooki/dpay HTTP/1.1
Content-Type: application/json
webhook-id: evt_01k6a8n3x6qj5r2m9w4t7b0c1d
webhook-timestamp: 1790503200
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{
"id": "evt_01k6a8n3x6qj5r2m9w4t7b0c1d",
"type": "merchant.updated",
"api_version": "2026-10-01",
"created": "2026-09-27T10:00:00Z",
"livemode": true,
"service": null,
"merchant_ref": "faktura-klient-123",
"data": {
"object": {
"object": "merchant",
"id": "faktura-klient-123",
"status": "completed",
"channel": "fakturka"
}
}
}
| Pole | Opis |
|---|---|
id | Identyfikator zdarzenia (evt_...), ten sam w nagłówku webhook-id i przy każdej ponownej próbie. Deduplikuj po nim. |
type | Typ zdarzenia. |
api_version | Wersja schematu treści. |
created | Czas zdarzenia w UTC. |
livemode | true dla produkcji, false dla sandboxa i płatności testowych. |
service | Serwis płatności merchanta albo null dla zdarzeń konta. |
merchant_ref | Twoja referencja merchanta - ta sama co w nagłówku On-Behalf-Of. Brak w partner_payout.*. |
data.object | Obiekt zdarzenia: merchant, payment, refund, recurring_payment, payout, invitation, authorization, reverification albo partner_payout. |
Płatność:
{
"id": "evt_01k6a8q2m4pz7h8c3v5n9t2x6y",
"type": "payment.succeeded",
"api_version": "2026-10-01",
"created": "2026-09-27T10:05:00Z",
"livemode": true,
"service": "fakturka-sklep",
"merchant_ref": "faktura-klient-123",
"data": {
"object": {
"object": "payment",
"id": "8F2A11C4-5B6D-4E7F-8A9B-0C1D2E3F4A5B",
"status": "succeeded",
"amount": 14900,
"currency": "PLN",
"amount_refunded": 0,
"amount_captured": 0,
"description": "Faktura FV/2026/09/123",
"created": "2026-09-27T10:03:12Z",
"paid_at": "2026-09-27T10:05:00Z",
"payment_method": { "type": "blik", "blik": { "flow": "code" } },
"failure": null,
"recurring": null,
"references": { "merchant": "FV/2026/09/123", "provider": "1234567890" },
"custom": null
}
}
}
- Kwoty są w groszach (
amount: 14900to 149,00 zł) - w przeciwieństwie do polavaluew rejestracji płatności, które jest w złotych. idpłatności totransactionIdz odpowiedzi rejestracji.statuspłatności:pending,processing,succeeded,captured,failed,reversedalbocharged_back.- Przy
payment.failedpolefailureniesiecode(kod odmowy dpay),message,category(customer,merchant,riskalbosystem),retryableiprovider_code(kod dostawcy, jeśli jest). Pełna lista: Kody odmów. Obiekty płatności, zwrotu, płatności cyklicznej i wypłaty opisuje strona Webhooki dla merchantów. - Obiekty zaproszeń nie zawierają danych osobowych zaproszonej osoby:
{"object": "invitation", "id": "twoja-referencja-zaproszenia", "role": "representative", "status": "completed"}.
Weryfikacja podpisu
Zawsze weryfikuj podpis, zanim zaufasz treści webhooka. dpay podpisuje każdy webhook zgodnie ze standardem Standard Webhooks:
webhook-id- identyfikator zdarzenia,webhook-timestamp- czas wysłania (Unix, sekundy),webhook-signature- podpisyv1,<base64>oddzielone spacją (w oknie rotacji sekretu są dwa).
Kluczem jest sekret endpointu bez prefiksu whsec_, zdekodowany z base64. Podpisujesz ciąg "{webhook-id}.{webhook-timestamp}.{surowe_body}":
key = base64_decode(secret bez "whsec_")
signature = base64(HMAC_SHA256(key, webhook_id + "." + webhook_timestamp + "." + raw_body))
webhook-signature: v1,<signature>
Zamiast pisać weryfikację samodzielnie, możesz użyć gotowej biblioteki Standard Webhooks dla swojego języka.
Licz HMAC na surowej treści żądania, dokładnie tak jak przyszła (bajt w bajt). Nie parsuj JSON-a i nie serializuj go ponownie przed weryfikacją - zmiana białych znaków czy kolejności kluczy zepsuje podpis. Znacznik czasu jest częścią podpisu, więc chroni też przed atakiem replay.
Przykład: PHP
function verifyDpayWebhook(string $rawBody, array $headers, string $secret): bool
{
$id = $headers['webhook-id'] ?? '';
$timestamp = $headers['webhook-timestamp'] ?? '';
$signatures = $headers['webhook-signature'] ?? '';
// odrzuć brak id i zbyt stary znacznik czasu (ochrona przed replay)
if ($id === '' || !ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
return false;
}
$key = base64_decode(substr($secret, strlen('whsec_')));
$expected = base64_encode(hash_hmac('sha256', "{$id}.{$timestamp}.{$rawBody}", $key, true));
foreach (explode(' ', $signatures) as $signature) {
[$version, $value] = array_pad(explode(',', $signature, 2), 2, '');
if ($version === 'v1' && hash_equals($expected, $value)) {
return true;
}
}
return false;
}
Przykład: Node.js
const crypto = require('crypto');
function verifyDpayWebhook(rawBody, headers, secret) {
const id = headers['webhook-id'] || '';
const timestamp = headers['webhook-timestamp'] || '';
const signatures = (headers['webhook-signature'] || '').split(' ');
if (!id || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return false; // brak id albo za stary znacznik - możliwy replay
}
const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
const expected = crypto
.createHmac('sha256', key)
.update(`${id}.${timestamp}.${rawBody}`)
.digest('base64');
return signatures.some((entry) => {
const [version, value] = entry.split(',');
return version === 'v1' && value && value.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(value), Buffer.from(expected));
});
}
Odpowiadaj szybko i idempotentnie
- Odpowiadaj
2xxod razu. Ciężką pracę (aktualizacja bazy, wysyłka maili) zrób asynchronicznie po zwróceniu odpowiedzi. Zbyt wolna odpowiedź jest traktowana jak błąd i webhook zostanie ponowiony. - Bądź idempotentny. To samo zdarzenie może przyjść więcej niż raz. Deduplikuj po
id(nagłówekwebhook-id) - nie księguj tej samej płatności dwa razy. - Nie filtruj po adresie IP. Webhooki wysyłamy przez infrastrukturę Cloudflare, bez stałej puli adresów - zabezpieczeniem jest podpis.
Ponawianie i wyłączenie endpointu
Sukces to odpowiedź 2xx w ciągu 10 sekund, bez przekierowań. Jeśli jej nie ma, dpay ponawia dostarczenie - łącznie do 10 prób w około 3 doby: od razu, po 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 14 h, 20 h i 24 h (z rozrzutem ±10%).
Jeśli przez 72 godziny żadne dostarczenie na endpoint się nie uda, wyłączamy go i wysyłamy e-mail do użytkowników portalu partnera. Po naprawie włącz endpoint w portalu i ponów nieudane dostarczenia z wybranego okresu (do 31 dni).
Nowy sekret endpointu działa od razu, a poprzedni podpisuje obok jeszcze przez 24 godziny - w tym czasie nagłówek webhook-signature niesie dwa podpisy. Zaktualizuj sekret po swojej stronie w tym oknie. Pozostali użytkownicy portalu dostają e-mail o rotacji.
Co dalej
- Cykl życia konta - znaczenie statusów w
merchant.updated. - Płatności on-behalf - zdarzenia
payment.*i adres w żądaniu. - Wypłaty on-behalf - zdarzenia
payout.*.