Sign inGet Started

Webhooki i zdarzenia

Gdy coś się dzieje w Twoim obszarze roboczym (e-mail zostaje dostarczony, odbiorca odrzuca wiadomość, wiadomość WhatsApp zostaje odczytana), Bird wysyła metodą POST podpisane zdarzenie JSON do każdego endpointu webhooka subskrybującego dany typ zdarzenia. Bird stosuje specyfikację Standard Webhooks dla nagłówków, podpisywania i struktury payloadu, więc jeśli już weryfikujesz webhooki z innej platformy Standard Webhooks, ten sam kod weryfikacji działa tu bez zmian.
Przegląd endpointów webhooków i dostarczania znajdziesz w artykule Czym jest webhook?.

Tworzenie endpointu

Zarejestruj endpoint w dashboardzie w sekcji Developers > Webhooks lub z terminala za pomocą bird CLI:
Przykład kodu
bird webhooks create https://example.com/webhooks/bird \
  --events email.delivered,email.bounced,email.complained \
  --description "Production delivery + bounce notifications"
Zarządzanie endpointami wymaga zakresu webhooks. Sesje dashboardu i logowanie CLI zapewniają go przez Twoją rolę użytkownika, a klucze API również mogą go zawierać: przyznaj webhooks:read, aby przeglądać endpointy i próby dostarczenia, lub webhooks:write, aby nimi zarządzać. Operacje bazowe zaczynają się od POST /v1/webhooks.
Strona Webhooks w dashboardzie Bird, z listą aktywnego endpointu i jego subskrybowanych zdarzeń
Adresy URL endpointów muszą być HTTPS, mieć maksymalnie 2048 znaków i być publicznie dostępne. Adresy URL na adresach prywatnych, loopback, link-local lub w inny sposób wewnętrznych są odrzucane z błędem 422 podczas tworzenia lub aktualizacji endpointu. Dostarczenia pochodzą z infrastruktury dostarczania Bird poza Twoją siecią.
Tablica events zawiera do 100 typów z katalogu zdarzeń. Endpoint odbiera tylko typy, które wymienia. Użyj PATCH /v1/webhooks/{webhook_id}, aby zastąpić całą listę dla przyszłych dostarczeń. Aby odbierać każde zdarzenie, subskrybuj każdy typ: typ spoza katalogu jest odrzucany z błędem 422, dotyczy to też symbolu wieloznacznego takiego jak sms.*. Istniejące subskrypcje nie rozszerzają się, gdy pojawiają się nowe typy.
Odpowiedź na utworzenie zawiera klucz podpisujący secret endpointu (z prefiksem whsec_) dokładnie raz. Zapisz go natychmiast w swoim menedżerze sekretów; nie można go ponownie pobrać, a jeśli go utracisz, dokonaj rotacji.
Przykład kodu
{
  "id": "whk_01ky7q639cfh99xb7ysy2x2gzj",
  "url": "https://example.com/webhooks/bird",
  "events": ["email.delivered", "email.bounced", "email.complained"],
  "description": "Production delivery + bounce notifications",
  "status": "active",
  "secret": "whsec_c+dgRBsFELJ9mR4tu2cyhK0gTMMkqntv",
  "created_at": "2026-07-23T14:48:29.740Z",
  "updated_at": "2026-07-23T14:48:29.740Z"
}
Endpointy obsługują pełny CRUD: lista, pobranie, aktualizacja i usunięcie. Usunięcie endpointu zatrzymuje wszystkie dostarczenia do niego, w tym ponowne próby wcześniejszych nieudanych dostarczeń, i jest nieodwracalne; aby tymczasowo wstrzymać dostarczenia, ustaw status na paused. Obszar roboczy może rejestrować wiele endpointów, każdy z własnym URL, filtrem zdarzeń i sekretem.

Weryfikacja podpisów

Każde dostarczenie zawiera trzy nagłówki:
NagłówekWartość
webhook-idIdentyfikuje dostarczenie zdarzenia. Ponowne próby i odtworzenia używają tej samej wartości.
webhook-timestampZnacznik czasu Unix (sekundy) tej próby dostarczenia
webhook-signaturev1,<base64 HMAC-SHA256>, może zawierać kilka podpisów rozdzielonych spacjami
Podpis to HMAC-SHA256 z łańcucha {webhook-id}.{webhook-timestamp}.{raw request body}, z kluczem będącym sekretem endpointu (usuń prefiks whsec_ i zdekoduj resztę z base64, aby uzyskać bajty klucza). Twój handler powinien zweryfikować podpis, odrzucać dostarczenia, których webhook-timestamp jest starszy niż 5 minut, i deduplikować po webhook-id: Bird dostarcza w trybie at-least-once, więc to samo dostarczenie może nadejść więcej niż raz.
Z Bird SDK weryfikacja podpisu i znacznika czasu to jedno wywołanie; deduplikacja pozostaje w Twoim handlerze:
// Pass the RAW request body; set the secret via new BirdClient({ webhooks: { secret } }).
const event = bird.webhooks.unwrap(rawBody, headers);
console.log(event.type); // discriminated union: narrow on event.type
Odrzucenie dostarczenia z 400, jak robią to powyższe przykłady, nie powoduje porzucenia zdarzenia: ponawiamy je zgodnie z harmonogramem poniżej. To celowe i pożądane. Najczęstszą przyczyną nieudanej weryfikacji jest sekret, którego Twój handler jeszcze nie ma, w trakcie rotacji lub błędnego wdrożenia, więc okno ponownych prób to Twoja szansa na naprawienie sekretu i odebranie zdarzenia. Zwracaj 2xx tylko wtedy, gdy chcesz na stałe porzucić dostarczenie.
Dowolna biblioteka referencyjna Standard Webhooks również działa. Jeśli weryfikujesz ręcznie, procedura jest następująca:
Przykład kodu
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody: string, headers: Record<string, string>, secret: string): boolean {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // 5-minute tolerance

  const key = Buffer.from(secret.slice("whsec_".length), "base64");
  const expected = createHmac("sha256", key)
    .update(`${id}.${timestamp}.${rawBody}`)
    .digest("base64");

  // During secret rotation, the header can contain several signatures. Accept any match.
  return headers["webhook-signature"].split(" ").some((part) => {
    const sig = Buffer.from(part.replace(/^v1,/, ""), "base64");
    return (
      sig.length === Buffer.byteLength(expected, "base64") &&
      timingSafeEqual(sig, Buffer.from(expected, "base64"))
    );
  });
}
Zawsze obliczaj HMAC na podstawie surowych bajtów ciała żądania. Parsowanie i ponowna serializacja JSON zmienia białe znaki lub kolejność kluczy i psuje podpis.

Semantyka dostarczania

Każde dostarczenie to jedno zdarzenie na żądanie HTTP POST z Content-Type: application/json, bez grupowania. Twój endpoint ma 15 sekund na odpowiedź; każdy status 2xx liczy się jako sukces, a wszystko inne (w tym przekierowania 3xx i timeouty) jako błąd. Każdy błąd podlega temu samemu harmonogramowi ponownych prób. Zwracany status zmienia to, co widzisz w logu prób dostarczenia, a nie to, czy ponawiamy: nie istnieje kod statusu, który zatrzymuje dostarczanie wcześniej. Odpowiadaj szybko i przetwarzaj asynchronicznie: kolejkuj zdarzenie i zwracaj 200 przed właściwym przetwarzaniem.
Po pierwszej próbie nieudane dostarczenia są ponawiane według tego harmonogramu, z jitterem ±20%, aby ponowne próby się nie synchronizowały:
Ponowna próbaOpóźnienie od poprzedniej próby
15 sekund
25 minut
330 minut
42 godziny
55 godzin
610 godzin
710 godzin
To łącznie osiem prób w ciągu około 27,5 godziny. 429 lub timeout podnosi zaplanowane opóźnienie krótsze niż 60 sekund do 60 sekund, co w praktyce dotyczy tylko pierwszej ponownej próby: po dodaniu jittera dociera ona 48 do 72 sekund później. Nagłówek Retry-After w odpowiedzi z błędem może wydłużyć następny czas oczekiwania. Akceptujemy ten nagłówek jako liczbę sekund opóźnienia lub datę HTTP. Żądane opóźnienie dłuższe od zaplanowanego zastępuje je, z limitem dwukrotności zaplanowanego opóźnienia (po ewentualnym podniesieniu do 60 sekund); krótsze jest ignorowane, więc nagłówek nigdy nie przyspiesza ponownej próby. Jitter nakładany jest dodatkowo. Każda ponowna próba niesie ten sam webhook-id, dzięki czemu deduplikacja działa. Po ostatniej ponownej próbie dostarczenie jest trwale oznaczone jako nieudane; odzyskasz je przez ponowne odtworzenie.
Dostarczenia nie są uporządkowane. email.delivered może nadejść przed email.accepted dla tej samej wiadomości, szczególnie gdy w grę wchodzą ponowne próby. Sortuj po polu timestamp w payloadzie zdarzenia, nigdy po kolejności nadejścia.

Obsługa endpointów

Wysyłki testowe

POST /v1/webhooks/{webhook_id}/test wysyła podpisane syntetyczne zdarzenie do Twojego endpointu i zwraca wynik synchronicznie: czy endpoint je zaakceptował, status HTTP, który zwrócił, oraz opóźnienie w obie strony. Ciało testowe to minimalny stub JSON zawierający tylko type zdarzenia, podpisany dokładnie jak prawdziwe dostarczenie; nie odzwierciedla prawdziwego payloadu zdarzenia. Przekaż {"event_type": "email.delivered"}, aby wybrać dowolny typ z katalogu, subskrybowany lub nie, albo pomiń ciało, aby użyć pierwszego subskrybowanego typu zdarzenia endpointu.
Twój endpoint ma 10 sekund na odpowiedź. Nieosiągalny endpoint generuje status: failed w ciele odpowiedzi, podczas gdy samo żądanie kończy się sukcesem. Użyj tego wyniku do debugowania połączenia. Wysyłki testowe trafiają bezpośrednio do endpointu: działają na wstrzymanym endpoincie i nie są zapisywane w logu prób dostarczenia. 412 oznacza, że endpoint nie może być jeszcze przetestowany, ponieważ brakuje mu prawidłowego sekretu podpisującego lub subskrybowanego typu zdarzenia.
Do testowania end-to-end z prawdziwymi przepływami zdarzeń wysyłaj na adresy sandboxa: wysyłki sandboxowe emitują prawdziwe zdarzenia webhookowe przez normalną ścieżkę dostarczania, co jest najlepszym sposobem na przetestowanie handlera przed uruchomieniem produkcyjnym.

Odtwarzanie nieudanych dostarczeń

POST /v1/webhooks/{webhook_id}/replay kolejkuje ponowne dostarczenie nieudanych dostarczeń. Zdarzenia, które endpoint już odebrał pomyślnie, są pomijane, więc odtwarzanie nigdy nie dostarcza podwójnie; ponownie dostarczone zdarzenie niesie oryginalny webhook-id, więc Twoje sprawdzanie deduplikacji obejmuje też odtworzenia. Odtwarzane są tylko nieudane próby: zdarzenie, które nigdy nie zostało wysłane do endpointu, nie ma nieudanej próby, więc odtwarzanie go nie odzyska.
Przekaż znaczniki czasu since/until, aby ograniczyć okno (domyślnie: ostatnie 24 godziny do momentu wysłania żądania). Obie granice są włączające i odnoszą się do momentu próby dostarczenia, a nie do momentu wystąpienia zdarzenia, więc ponowna próba, która nastąpiła dzień po zdarzeniu, trafia do okna według godziny tej próby. Replay odczytuje dziennik prób dostarczenia, który przechowuje dane przez trzy dni, więc tyle sięga najstarsza dostępna historia: wcześniejszy since poszerza okno, ale nie odzyskuje niczego starszego. Jedno odtworzenie obejmuje najwyżej 10 000 najstarszych zdarzeń w oknie.
Żądanie zwraca 202, a zdarzenia są ponownie dostarczane asynchronicznie. Ponowne dostarczenie ma jedną próbę, a nie harmonogram ponownych prób opisany wyżej. Próba jest zapisywana i zadanie jest zakończone niezależnie od tego, czy endpoint je zaakceptował, więc odtwarzanie do endpointu, który nadal nie działa, kosztuje jedno żądanie na zdarzenie zamiast ośmiu; napraw endpoint i odtwórz ponownie. Te niepowodzenia nie wpływają na kondycję endpointu: odtwarzanie nie może przesunąć endpointu do degraded ani go automatycznie wstrzymać. Ponowne dostarczenie zaakceptowane przez endpoint resetuje oba stany.
Odtwórz paused endpoint, a żądanie nadal zwraca 202, ale nic nie jest ponownie dostarczane. Najpierw go włącz ponownie, jak opisuje sekcja Automatyczne wstrzymywanie i ponowne włączanie.
Odtworzenia są ograniczone do 20 na organizację na dzień UTC; powyżej tego żądanie zwraca 429 (WebhookReplayQuotaExceeded). Odpowiedź nie zawiera licznika ani identyfikatora zadania. Śledź wyniki za pomocą GET /v1/webhooks/{webhook_id}/attempts, które wyświetla ostatnie próby dostarczenia od najnowszych do najstarszych z kodami statusu i opóźnieniem. Każde żądanie HTTP ma własny wpis, więc ponawiane zdarzenie pojawia się raz na próbę, a ponowne dostarczenie pojawia się jako kolejny wpis.

Rotacja sekretu podpisującego

POST /v1/webhooks/{webhook_id}/rotate-secret generuje nowy sekret i zwraca go raz. Przez następne 24 godziny Bird podpisuje każde dostarczenie oboma sekretami. Nagłówek webhook-signature zawiera podpisy rozdzielone spacjami (v1,<old> v1,<new>), co pozwala wdrożyć nowy sekret w okresie nakładania się. Biblioteki Standard Webhooks automatycznie sprawdzają wszystkie podpisy. Po 24 godzinach stary sekret przestaje podpisywać. Endpoint może mieć najwyżej 5 jednocześnie ważnych sekretów, więc wielokrotna rotacja w oknie nakładania się kończy się błędem WebhookTooManySecrets, dopóki starszy sekret nie wygaśnie.

Automatyczne wstrzymywanie i ponowne włączanie

status endpointu to active, degraded lub paused. Ostatnie niepowodzenia dostarczeń oznaczają endpoint jako degraded w ramach ostrzeżenia o kondycji; kontynuujemy dostarczanie i ponawianie. Endpoint, który nieprzerwanie zawodzi przez około pięć dni, jest automatycznie paused i wszystkie dostarczenia są wstrzymywane; jedno pomyślne dostarczenie w tym okresie resetuje licznik. Wstrzymany endpoint nigdy nie wznawia się sam. Włącz go ponownie za pomocą PATCH /v1/webhooks/{webhook_id} i {"status": "active"} (lub ze strony Webhooks w dashboardzie), a następnie użyj odtwarzania, aby ponownie dostarczyć próby, które nie powiodły się przed wstrzymaniem. Najpierw włącz ponownie: odtwarzanie żądane, gdy endpoint jest nadal wstrzymany, nie dostarcza niczego. Zdarzenia, które nadeszły, gdy endpoint był wstrzymany, nigdy nie zostały wysłane, więc odtwarzanie ich nie odzyska.
Dowolna z poniższych czynności przywraca endpoint degraded do active:
Co resetuje stanDlaczego
Dostarczenie się powiodłoEndpoint ponownie zaakceptował zdarzenie.
Zmiana url endpointuZarejestrowane niepowodzenia dotyczą adresu, którego już nie używasz.
Ponowne włączenie endpointu pausedWraca do użytku, więc jego dawne niepowodzenia nie mają już znaczenia.
Wysyłka testowa zwracająca 2xxWykazano, że endpoint jest osiągalny.
Edycja opisu endpointu lub jego subskrybowanych typów zdarzeń nie mówi nic o osiągalności, więc pozostawia degraded bez zmian, tak samo jak nieudana wysyłka testowa.
Wysyłamy e-mail do właścicieli organizacji, gdy endpoint po raz pierwszy przechodzi w stan degraded, raz na epizod, a nie raz na każde nieudane dostarczenie. Kolejna degradacja po odzyskaniu sprawności powoduje ponowne wysłanie e-maila, z zachowaniem 24-godzinnego okresu oczekiwania: wysyłamy co najwyżej jeden e-mail o degradacji na endpoint co 24 godziny, więc endpoint przeskakujący między active a degraded nie zalewa skrzynki odbiorczej. Zmiana url endpointu resetuje ten okres, więc pierwsza degradacja pod nowym adresem URL może wygenerować e-mail nawet w ciągu 24 godzin od poprzedniego.

Katalog zdarzeń

Payloady zdarzeń zawierają zwięzłe fakty w zakresie odbiorcy do korelacji z Twoim systemem. Nie zawierają pełnego zasobu. Jeśli potrzebujesz więcej kontekstu, pobierz zasób po jego ID. Typy zdarzeń stosują nazewnictwo resource.action i są grupowane według produktu; strona zdarzeń każdego produktu zawiera pola payloadu poszczególnych zdarzeń:
  • Zdarzenia e-mail: cykl życia dostarczenia (email.accepted przez email.delivered lub email.bounced), zaangażowanie (email.opened, email.clicked), wypisania i e-mail przychodzący
  • Zdarzenia SMS: cykl życia wiadomości od sms.accepted do statusu końcowego
  • Webhooki WhatsApp: od whatsapp.accepted do whatsapp.delivered, whatsapp.read, whatsapp.failed, whatsapp.rejected, whatsapp.received dla wiadomości przychodzącej, whatsapp.reacted gdy użytkownik zareaguje na jedną z Twoich wiadomości, oraz whatsapp.group.join_request_created i whatsapp.group.join_request_revoked gdy ktoś prosi o dołączenie do grupy wymagającej zatwierdzenia lub wycofuje prośbę
  • Zdarzenia Verify: cykl życia weryfikacji (verify.verification.created, verify.verification.verified) i dostarczenie każdej próby kodu weryfikacyjnego (verify.attempt.sent, verify.attempt.delivered, verify.attempt.undelivered)
  • Zdarzenia preferencji: międzykanałowy zapis zgody: preference.granted, preference.revoked i preference.deleted
Każde ciało dostarczenia to zagnieżdżona koperta Standard Webhooks z type, timestamp i obiektem data specyficznym dla typu. Nagłówek webhook-id niesie tożsamość zdarzenia. Pole timestamp w kopercie zapisuje, kiedy zdarzenie wystąpiło. Nagłówek webhook-timestamp zapisuje bieżącą próbę dostarczenia i zmienia się przy każdej ponownej próbie.
Przykład kodu
{
  "type": "email.delivered",
  "timestamp": "2026-06-10T14:30:00Z",
  "data": {
    "email_id": "em_01krdgeqcxet5s7t44vh8rt9mg",
    "recipient_id": "er_01krdgeqcxet5s7t44vh8rt9mg",
    "workspace_id": "ws_01krdgeqcxet5s7t44vh8rt9mg",
    "recipient": "user@example.com",
    "recipient_role": "to",
    "tags": [{ "name": "category", "value": "welcome" }],
    "metadata": { "order_id": "ord_123" },
    "broadcast_id": null
  }
}
data każdego zdarzenia e-mail zawiera email_id, recipient_id, workspace_id, adres recipient i jego recipient_role z koperty. Zawiera również tags i metadata z żądania wysyłki lub null, gdy nie zostały podane. Zawiera też broadcast_id, wskazujące broadcast, w ramach którego wysyłka została wykonana, lub null, gdy za wysyłką nie stał żaden broadcast. W przypadku email.unsubscribed i email.list_unsubscribed, null nie wyklucza broadcastu; zdarzenia e-mail wyjaśniają dlaczego. Typy zdarzeń dodają własne pola do tej bazy. Każdy wariant ma stały zestaw pól: pola są domyślnie wymagane, a ich obecność zależy wyłącznie od typu zdarzenia.
Nazwy zdarzeń nigdy nie są zmieniane, a nowe typy są dodawane wraz z wydaniami produktów, więc pisz handler tak, aby ignorował typy, których nie rozpoznaje.

Zdarzenia preferencji

Zadeklarowane preferencje (zgody i rezygnacje opisane w przewodniku każdego kanału: e-mail, SMS, WhatsApp) obejmują wiele kanałów, więc ich zdarzenia wskazują kanał w treści payloadu, a nie w typie. preference.granted jest emitowane, gdy zgoda wchodzi w życie, preference.revoked gdy rezygnacja, a preference.deleted gdy zapisana deklaracja zostaje usunięta i klucz wraca do stanu bez rekordu. Zdarzenie oznacza, że bieżący rekord klucza się zmienił: deklaracja powtarzająca obecny stan nie emituje niczego, podobnie jak deklaracja odrzucona z powodu nieprawidłowej kolejności. Pole timestamp w kopercie to moment wejścia deklaracji w życie, czyli w przypadku deklaracji z datą wsteczną chwila jej złożenia, a nie dotarcia do Bird.
Każdy payload zawiera pełny klucz preferencji: channel, handle, sender_scope i topic_id, z polami zakresu obecnymi z wartością null, gdy nie zawężają klucza. Obok klucza znajdują się: coverage oświadczenia, preference_id, transition_id wpisu historii dopisanego przez zapis oraz contact_id, którego uchwyt pasował w momencie zarejestrowania oświadczenia, lub null:
Przykład kodu
{
  "type": "preference.revoked",
  "timestamp": "2026-08-25T14:52:03.192524705Z",
  "data": {
    "preference_id": "prf_01krdgeqcxet5s7t44vh8rt9mg",
    "transition_id": "prt_01krdgeqcxet5s7t44vh8rt9mg",
    "channel": "sms",
    "handle": "+15550001234",
    "sender_scope": null,
    "topic_id": null,
    "coverage": "non_transactional",
    "contact_id": null
  }
}

Następne kroki