Przejdź do głównej zawartości

Webhooki

Webhooki powiadamiają Twój serwer o zdarzeniach na koncie dpay: opłaconych i odrzuconych płatnościach, zwrotach, płatnościach cyklicznych i wypłatach. Każde zdarzenie wysyłamy żądaniem POST z JSON-em na Twoje endpointy, z podpisem zgodnym ze Standard Webhooks i z ponowieniami przez około 3 doby.

Webhooki a IPN

IPN działa dalej bez zmian. Webhooki dają więcej: osobne typy zdarzeń (także odmowy z kodem przyczyny, zwroty i wypłaty), endpointy konfigurowane w panelu, log dostarczeń z ponowieniami i podpis sekretem endpointu. Oba kanały są niezależne - jeśli używasz obu, to samo zdarzenie przyjdzie dwiema drogami. Nowa integracja może działać na samych webhookach i pominąć url_ipn.

Szybki start​

  1. W panelu panel.dpay.pl przejdź do Inne > Webhooki i kliknij Dodaj endpoint.
  2. Podaj adres HTTPS, wybierz zdarzenia, zakres serwisów i tryb (produkcyjny albo testowy).
  3. Skopiuj sekret podpisu whsec_... - pokazujemy go tylko raz.
  4. W swoim endpoincie zweryfikuj podpis i odpowiedz kodem 2xx.
  5. Kliknij Wyślij testowe zdarzenie i sprawdź wynik w logu dostarczeń.

Endpointy w panelu​

Zakładka Webhooki jest w menu Inne. Widzi ją konto główne i użytkownicy zespołu z uprawnieniem Klucze API. Konto może mieć do 16 endpointów.

PoleOpis
Adres URLTylko HTTPS na porcie 443, do 500 znaków, bez loginu i hasła w adresie. Adres musi prowadzić do serwera dostępnego z internetu - adresy sieci prywatnych i lokalnych oraz domeny dpay są odrzucane.
OpisDla Ciebie, np. „Sklep - produkcja”.
ZdarzeniaWszystkie (także typy dodane w przyszłości) albo wybrane.
ZakresWszystkie serwisy konta (także przyszłe) albo wybrane serwisy. Zdarzenia konta, czyli wypłaty (payout.*), trafiają tylko na endpointy obejmujące wszystkie serwisy.
TrybProdukcyjny albo testowy, wybierany przy tworzeniu. Endpoint testowy dostaje tylko zdarzenia testowe (livemode: false: serwisy w trybie testowym, płatności testowe), a produkcyjny tylko produkcyjne. Dla obu trybów utwórz dwa endpointy.

W szczegółach endpointu:

  • Sekret podpisu - każdy endpoint ma własny sekret whsec_..., pokazywany tylko przy utworzeniu i przy rotacji. Po wygenerowaniu nowego sekretu poprzedni podpisuje obok jeszcze przez 24 godziny, więc podmienisz go bez przestoju.
  • Wyślij testowe zdarzenie - wysyła webhook.test tylko na ten endpoint, niezależnie od wybranych zdarzeń. Wynik zobaczysz w logu po kilku sekundach.
  • Log dostarczeń - każde dostarczenie ze statusem (czeka, dostarczone, nieudane) i wszystkimi próbami: kodem HTTP, czasem odpowiedzi, błędem i początkiem Twojej odpowiedzi (do 2 KB, przechowywany 90 dni). Nieudane dostarczenie możesz ponowić od razu, a przyciskiem Ponów nieudane - wszystkie nieudane z wybranego okresu (do 31 dni i 1000 dostarczeń naraz).
  • Wyłącz / Włącz - wyłączenie kończy czekające dostarczenia jako nieudane. Po włączeniu możesz je ponowić z logu.

Na liście endpointów widać ostatnie dostarczenie i skuteczność z ostatnich 7 dni. Wszystkie dostarczenia zdarzeń jednej płatności - na endpointy i na adres z żądania - zobaczysz też w szczegółach transakcji w historii, na karcie Webhooki, i tam ponowisz nieudane.

Katalog zdarzeń​

ZdarzenieObiektKiedy
payment.succeededpaymentPłatność opłacona.
payment.failedpaymentPłatność odrzucona - przyczyna w failure (patrz Kody odmów) - albo wygasła, bo nie została opłacona w ciągu 7 dni (kod expired).
payment.capturedpaymentPobranie środków z preautoryzacji karty - po każdym pobraniu, także częściowym. Pobraną kwotę niesie amount_captured, a po pobraniu całości status zmienia się na captured.
refund.succeededrefundZwrot wykonany. Zwrot BLIK jest wykonany, gdy przyjmie go BLIK.
refund.failedrefundZwrot odrzucony - przyczyna w failure.
recurring_payment.activatedrecurring_paymentPłatność cykliczna aktywna - możesz obciążać jej alias.
recurring_payment.canceledrecurring_paymentPłatność cykliczna anulowana - kto ją anulował, mówi canceled_by.
recurring_payment.expiredrecurring_paymentPłatność cykliczna wygasła.
recurring_payment.declinedrecurring_paymentRejestracja płatności cyklicznej odrzucona.
payout.paidpayoutWypłata zrealizowana - poszła do banku.
payout.failedpayoutWypłata odrzucona.

Obciążenia płatności cyklicznej to zwykłe zdarzenia payment.* - obiekt płatności ma wtedy wypełnione pole recurring. Zdarzenie webhook.test przychodzi tylko po kliknięciu Wyślij testowe zdarzenie i nie ma go w katalogu do wyboru.

Nowe typy zdarzeń

Katalog będzie się rozszerzać, a obiekty mogą dostać nowe pola. Nieznany type i nieznane pole po prostu pomiń - nie traktuj ich jak błędu. Endpoint z zaznaczonymi wszystkimi zdarzeniami dostanie nowe typy automatycznie.

Koperta zdarzenia​

Każde zdarzenie ma tę samą kopertę. Typ niesie pole type, a obiekt, którego dotyczy zdarzenie - data.object.

POST /webhooki/dpay HTTP/1.1
Host: sklep.example
Content-Type: application/json
User-Agent: dpay-webhooks/1.0 (+https://dpay.pl)
webhook-id: evt_01k6a8q2m4pz7h8c3v5n9t2x6y
webhook-timestamp: 1790503500
webhook-signature: v1,Q2n7kX0fW3mR9tYp1aZs5vE8uJ6hL4cB2dN0gK7oI1M=

{
"id": "evt_01k6a8q2m4pz7h8c3v5n9t2x6y",
"type": "payment.succeeded",
"api_version": "2026-10-01",
"created": "2026-09-27T10:05:00Z",
"livemode": true,
"service": "abc123",
"data": {
"object": {
"object": "payment",
"id": "8F2A11C4-5B6D-4E7F-8A9B-0C1D2E3F4A5B",
"status": "succeeded",
"amount": 14900,
"currency": "PLN",
"amount_refunded": 0,
"amount_captured": 0,
"description": "Zamówienie 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": "2026/09/123", "provider": "1234567890" },
"custom": "order-789"
}
}
}
PoleOpis
idIdentyfikator zdarzenia (evt_...), ten sam w nagłówku webhook-id i przy każdej ponownej próbie. Deduplikuj po nim.
typeTyp zdarzenia z katalogu.
api_versionWersja schematu treści (dziś 2026-10-01).
createdCzas zdarzenia w UTC (ISO 8601).
livemodetrue dla zdarzeń produkcyjnych, false dla testowych.
serviceNazwa serwisu (to samo pole service co w API) albo null dla zdarzeń konta i zdarzenia testowego.
data.objectObiekt zdarzenia: payment, refund, recurring_payment albo payout.

Treść zdarzenia budujemy raz, przy pierwszej próbie - każde ponowienie wysyła te same bajty. Obiekt pokazuje stan z chwili zdarzenia.

Obiekty​

Wszystkie kwoty są w groszach (14900 to 149,00 zł) - inaczej niż pole value w rejestracji płatności, które jest w złotych. Czasy są w UTC w formacie ISO 8601.

Płatność (payment)​

PoleOpis
idIdentyfikator płatności - transactionId z odpowiedzi rejestracji.
statuspending, processing, succeeded, captured, failed, reversed albo charged_back.
amount, currencyKwota płatności i waluta (np. PLN).
amount_refundedSuma zwrotów.
amount_capturedSuma pobrań z preautoryzacji karty.
descriptionOpis z rejestracji albo null.
created, paid_atUtworzenie i opłacenie (null, dopóki płatność nie jest opłacona).
payment_methodMetoda płatności: type i obiekt o tej samej nazwie ze szczegółami (patrz niżej) albo null, gdy metoda nie jest jeszcze znana.
failurePrzyczyna odmowy przy payment.failed (patrz Kody odmów), w innych zdarzeniach null.
recurringPrzy płatności cyklicznej: alias, registration (id płatności rejestrującej) i sequence (first albo subsequent); w innych płatnościach null.
references.merchantTwój numer zamówienia z pola reference w rejestracji.
references.providerIdentyfikator transakcji u dostawcy (np. BLIK, operator kart), jeśli jest.
customWartość pola custom z rejestracji.

Szczegóły metody płatności (payment_method):

typeSzczegóły
blikflow: code, oneclick albo recurring; przy BLIK OneClick także alias (value, label).
cardbrand, last4, exp_month, exp_year, country, funding (credit, debit, charge), wallet (apple_pay, google_pay, click_to_pay albo null), authorization_code.
bank_transferbank i country banku.
twistoplan: standard albo 3x.
blik_pay_later, mb_way, paypal, paypo, paysafecard, multibancoBez dodatkowych pól.

Zwrot (refund)​

PoleOpis
idIdentyfikator zwrotu (ZWROT-...). Przy zwrocie odrzuconym, zanim powstał, może być null.
paymentIdentyfikator zwracanej płatności.
statussucceeded albo failed.
amount, currencyKwota zwrotu i waluta.
created, succeeded_atUtworzenie i wykonanie zwrotu.
failurePrzyczyna odrzucenia przy refund.failed, np. refund_window_expired.

Płatność cykliczna (recurring_payment)​

PoleOpis
idIdentyfikator płatności rejestrującej.
aliasAlias, którym obciążasz płatność cykliczną.
statuspending, active, canceled, expired albo declined.
canceled_byPrzy anulowaniu: merchant (Ty), customer (klient w banku) albo dpay; w innych stanach null.
payment_methodMetoda płatności - jak w obiekcie płatności.
termsWarunki: model, frequency, limit_amount i total_limit_amount (w groszach), starts_on i expires_on (daty RRRR-MM-DD).
createdUtworzenie płatności rejestrującej.

Wypłata (payout)​

PoleOpis
idIdentyfikator wypłaty.
statuspending, paid albo failed.
amount, fee, currencyKwota wypłaty netto, prowizja i waluta.
bank_accountTylko 4 ostatnie cyfry rachunku: {"last4": "1234"}.
created, paid_atUtworzenie i realizacja wypłaty.
failurePrzy payout.failed kod payout_failed, a w provider_code - co stało się ze środkami (patrz Kody odmów).

Weryfikacja podpisu​

Zawsze weryfikuj podpis, zanim zaufasz treści webhooka. Podpis jest zgodny ze standardem Standard Webhooks i jest w trzech nagłówkach:

  • webhook-id - identyfikator zdarzenia,
  • webhook-timestamp - czas wysłania (Unix, sekundy),
  • webhook-signature - podpisy v1,<base64> rozdzielone spacją (w oknie rotacji sekretu są dwa).

Kluczem HMAC jest sekret bez prefiksu whsec_, zdekodowany z base64. Podpisujesz ciąg {webhook-id}.{webhook-timestamp}.{surowe body}:

key = base64_decode(sekret 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. Nie parsuj JSON-a i nie serializuj go ponownie przed weryfikacją - zmiana białych znaków albo kolejności kluczy zepsuje podpis. Znacznik czasu jest częścią podpisu, więc odrzucając zbyt stare żądania chronisz się też przed powtórzeniem (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 identyfikatora i znacznik czasu starszy niż 5 minut (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;
}

$rawBody = file_get_contents('php://input');
$headers = array_change_key_case(getallheaders(), CASE_LOWER);

if (!verifyDpayWebhook($rawBody, $headers, getenv('DPAY_WEBHOOK_SECRET'))) {
http_response_code(400);
exit;
}

$event = json_decode($rawBody, true);
// Zapisz zdarzenie (idempotentnie po $event['id']) i odpowiedz od razu
http_response_code(200);

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 || !/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return false; // brak identyfikatora albo za stary znacznik czasu - 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));
});
}

// Express: potrzebujesz surowego body, nie sparsowanego JSON-a
app.post('/webhooki/dpay', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifyDpayWebhook(req.body.toString('utf8'), req.headers, process.env.DPAY_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}

const event = JSON.parse(req.body);
// Zapisz zdarzenie (idempotentnie po event.id) i odpowiedz od razu
res.sendStatus(200);
});

Przykład: Python​

import base64
import hashlib
import hmac
import time


def verify_dpay_webhook(raw_body: bytes, headers: dict, secret: str) -> bool:
webhook_id = headers.get('webhook-id', '')
timestamp = headers.get('webhook-timestamp', '')
signatures = headers.get('webhook-signature', '').split(' ')

# Odrzuć brak identyfikatora i znacznik czasu starszy niż 5 minut (ochrona przed replay)
if not webhook_id or not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return False

key = base64.b64decode(secret.removeprefix('whsec_'))
signed = f'{webhook_id}.{timestamp}.'.encode() + raw_body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()

for entry in signatures:
version, _, value = entry.partition(',')
if version == 'v1' and hmac.compare_digest(expected, value):
return True

return False

Odpowiedź, ponowienia i wyłączenie endpointu​

  • Odpowiedz kodem 2xx w ciągu 10 sekund. Dłuższą pracę (zapis w bazie, e-maile) wykonaj asynchronicznie, po odpowiedzi. Przekierowanie (3xx), inny kod i przekroczenie czasu to nieudana próba.
  • Ponowienia: nieudane dostarczenie ponawiamy - łą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%.
  • Wyłączenie: jeśli przez 72 godziny żadne dostarczenie na endpoint się nie uda, wyłączamy go i wysyłamy e-mail na adres konta. Po naprawie włącz endpoint w panelu i ponów nieudane dostarczenia (Ponów nieudane). Panel ostrzega wcześniej, od kiedy dostarczenia nie przechodzą.
  • Idempotencja: to samo zdarzenie może przyjść więcej niż raz (dostarczenie „co najmniej raz”). Deduplikuj po id - nie realizuj tego samego zamówienia dwa razy.
  • Kolejność: zdarzenia mogą przyjść w innej kolejności, niż powstały, np. gdy jedno czeka na ponowienie. Porównuj created i status obiektu, a gdy potrzebujesz pewności, pobierz aktualny stan z API.
  • Równoległość: na jeden endpoint wysyłamy najwyżej 10 żądań naraz.
  • Adresy IP: nie filtruj żądań po adresie IP - webhooki wysyłamy przez infrastrukturę Cloudflare, bez stałej puli adresów. Zabezpieczeniem jest podpis.

Adres w żądaniu rejestracji​

Zdarzenia pojedynczej płatności możesz też odebrać na adres podany przy jej rejestracji, bez konfiguracji w panelu - przydatne np. we wtyczkach e-commerce i na platformach z wieloma sklepami. Obiekt webhook dodajesz do rejestracji transakcji:

{
"service": "abc123",
"value": "149.00",
"url_success": "https://sklep.example/sukces",
"url_fail": "https://sklep.example/blad",
"transactionType": "transfers",
"reference": "2026/09/123",
"webhook": {
"url": "https://sklep.example/webhooki/dpay",
"events": ["payment.succeeded", "payment.failed"]
},
"checksum": "..."
}
  • webhook.url - adres według tych samych reguł co endpointy w panelu. Przy rejestracji sprawdzamy go bez zapytania do DNS (chwilowy problem z DNS nie odrzuci płatności), a pełne sprawdzenie przechodzi przed każdą wysyłką.
  • webhook.events - opcjonalna lista typów. Bez niej adres dostaje wszystkie zdarzenia płatności, jej zwrotów i płatności cyklicznej (payment.*, refund.*, recurring_payment.*) - zdarzeń konta (payout.*) tu nie ma.
  • Zwrot dziedziczy adres płatności, chyba że żądanie zwrotu ma własny obiekt webhook (patrz Zwroty). Obciążenia płatności cyklicznej dziedziczą adres płatności rejestrującej, a pobranie z preautoryzacji karty (payment.captured) - adres płatności, chyba że żądanie capture ma własny obiekt webhook (patrz Karty - pre-autoryzacja i capture).
  • Zdarzenia podpisuje sekret webhooków serwisu. Wygenerujesz go w panelu: Punkty Płatności > Serwisy > wybrany serwis > Sekret webhooków > Wygeneruj sekret. Pokazujemy go tylko raz, a po ponownym wygenerowaniu poprzedni podpisuje jeszcze przez 24 godziny. Bez sekretu rejestracja z obiektem webhook kończy się błędem WEBHOOK_SECRET_MISSING.
  • Błędny adres odrzuca rejestrację z kodem w errors["webhook.url"]: https_required, invalid_url, credentials_in_url, port_not_allowed, own_domain, private_address albo url_too_long.
  • Jeśli to samo zdarzenie idzie też na endpoint z panelu, oba żądania mają ten sam webhook-id - deduplikuj po nim.
  • Próby dostarczenia na adres z żądania i przycisk ponowienia są w szczegółach transakcji w historii, na karcie Webhooki.

Pole reference (do 64 znaków, bez znaków sterujących) to Twój numer zamówienia - wraca w references.merchant obiektu płatności. Nie musi być unikalny. Pola webhook i reference nie wchodzą do sumy kontrolnej.

Historia zdarzeń​

Po awarii endpointu albo do uzgodnienia stanu pobierzesz zdarzenia z API - od najnowszych, stronami, z filtrem po typach i czasie. To te same koperty, które poszły do endpointów, więc obsłużysz je tym samym kodem (zdarzenie pojawia się na liście kilka sekund po wystąpieniu).

POST/api/v1_0/eventsLista zdarzeń serwisu i konta: parametry, suma kontrolna i odpowiedź.Pełny kontrakt w API Reference
PoleWymaganeOpis
servicetakNazwa serwisu.
timestamptakBieżący czas Unix w sekundach - musi być w oknie 300 sekund od czasu serwera.
checksumtaksha256({service}|{hash}|{timestamp}), gdzie hash to klucz Hash serwisu.
typesnieLista typów zdarzeń, np. ["payment.failed"].
created_from, created_tonieZakres czasu zdarzeń (ISO 8601).
starting_afternieKursor: next_starting_after z poprzedniej strony.
limitnieLiczba zdarzeń na stronie, od 1 do 100, domyślnie 20.
{
"status": "success",
"data": [
{ "id": "evt_01k6a8q2m4pz7h8c3v5n9t2x6y", "type": "payment.succeeded", "api_version": "2026-10-01", "created": "2026-09-27T10:05:00Z", "livemode": true, "service": "abc123", "data": { "object": { "object": "payment", "id": "8F2A11C4-5B6D-4E7F-8A9B-0C1D2E3F4A5B", "status": "succeeded" } } }
],
"has_more": true,
"next_starting_after": "evt_01k6a8q2m4pz7h8c3v5n9t2x6y"
}

W przykładzie obiekt jest skrócony - lista zwraca pełne koperty. Obejmuje zdarzenia serwisu i zdarzenia konta (wypłaty), bez zdarzeń testowych endpointów. Suma kontrolna zawiera znacznik czasu, więc przechwycona raz nie daje trwałego dostępu do historii. API przyjmuje do 60 żądań na minutę.

Dobre praktyki​

  • Weryfikuj podpis każdego żądania i odrzucaj zbyt stare znaczniki czasu.
  • Odpowiadaj 2xx od razu, a zdarzenie przetwarzaj w tle.
  • Deduplikuj po id zdarzenia i nie zakładaj kolejności.
  • Pomijaj nieznane typy zdarzeń i nieznane pola.
  • Trzymaj osobne endpointy dla trybu testowego i produkcyjnego.
  • Reaguj na odmowę według failure.category i failure.retryable, a nie według tekstu message (patrz Kody odmów).