Przejdź do głównej zawartości

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.

Adres w żądaniu

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.

ZdarzenieKiedy
payment.succeededPłatność opłacona.
payment.failedPłatność odrzucona albo wygasła - z kodem odmowy w failure.
payment.capturedDopełniona preautoryzacja karty.
refund.succeeded, refund.failedZwrot zrealizowany albo odrzucony.
recurring_payment.activated, recurring_payment.canceled, recurring_payment.expired, recurring_payment.declinedZmiana płatności cyklicznej.
payout.paid, payout.failedWypłata na rachunek merchanta zrealizowana albo odrzucona.

Zdarzenia partnera​

ZdarzenieKiedy
merchant.updatedZmienił się status onboardingu merchanta - nowy status w data.object.status (patrz niżej).
reverification.submittedMerchant złożył dane w reweryfikacji okresowej.
authorization.revokedMerchant 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_verifiedZaproszona osoba zweryfikowała tożsamość (tylko ścieżka eID - nie polegaj na tym zdarzeniu, ścieżka manualna go nie emituje).
invitation.completedZaproszona osoba ukończyła swój przepływ - jedyny gwarantowany terminal sukcesu zaproszenia.
invitation.signedZaproszony reprezentant podpisał umowę.
partner_payout.paid, partner_payout.declineddpay wypłacił albo odrzucił wypłatę Twojej marży (patrz rozliczenia ISV). Te zdarzenia dotyczą Ciebie, nie merchanta - nie mają merchant_ref.
Nowe typy zdarzeń

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.

StatusZnaczenie
in_progressOnboarding w toku (dane, dokumenty).
awaiting_signatureUmowa czeka na podpis.
partially_signedCzęść reprezentantów podpisała umowę.
agreement_signedUmowa podpisana.
pending_transferOczekiwanie na opłatę aktywacyjną.
under_reviewWeryfikacja manualna dpay.
completedKonto aktywne.
blockedKonto 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"
}
}
}
PoleOpis
idIdentyfikator zdarzenia (evt_...), ten sam w nagłówku webhook-id i przy każdej ponownej próbie. Deduplikuj po nim.
typeTyp zdarzenia.
api_versionWersja schematu treści.
createdCzas zdarzenia w UTC.
livemodetrue dla produkcji, false dla sandboxa i płatności testowych.
serviceSerwis płatności merchanta albo null dla zdarzeń konta.
merchant_refTwoja referencja merchanta - ta sama co w nagłówku On-Behalf-Of. Brak w partner_payout.*.
data.objectObiekt 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: 14900 to 149,00 zł) - w przeciwieństwie do pola value w rejestracji płatności, które jest w złotych.
  • id płatności to transactionId z odpowiedzi rejestracji.
  • status płatności: pending, processing, succeeded, captured, failed, reversed albo charged_back.
  • Przy payment.failed pole failure niesie code (kod odmowy dpay), message, category (customer, merchant, risk albo system), retryable i provider_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 - podpisy v1,<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.

Podpisuj surowe body

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 2xx od 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łówek webhook-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).

Rotacja sekretu

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​