Sign inGet Started

Webhooks & Events

Wenn in Ihrem Workspace etwas passiert (eine E-Mail wird zugestellt, ein Empfänger bounct, eine WhatsApp-Nachricht wird gelesen), sendet Bird per POST ein signiertes JSON-Event an jeden Webhook-Endpoint, der diesen Event-Typ abonniert hat. Bird folgt der Standard Webhooks-Spezifikation für Header, Signierung und Payload-Struktur. Wenn Sie also bereits Webhooks einer anderen Standard-Webhooks-Plattform verifizieren, funktioniert derselbe Verifizierungscode hier unverändert.
Einen Überblick über Webhook-Endpoints und Zustellung finden Sie unter Was ist ein Webhook?.

Endpoint erstellen

Registrieren Sie einen Endpoint im Dashboard unter Developers > Webhooks oder über das Terminal mit der bird CLI:
Codebeispiel
bird webhooks create https://example.com/webhooks/bird \
  --events email.delivered,email.bounced,email.complained \
  --description "Production delivery + bounce notifications"
Die Endpoint-Verwaltung erfordert den Scope webhooks. Dashboard-Sitzungen und der Login der CLI übernehmen ihn über Ihre Benutzerrolle, und API-Schlüssel können ihn ebenfalls enthalten: Vergeben Sie webhooks:read, um Endpoints und Zustellversuche einzusehen, oder webhooks:write, um sie zu verwalten. Die zugrunde liegenden Operationen beginnen bei POST /v1/webhooks.
Die Webhooks-Seite im Bird-Dashboard mit einem aktiven Endpoint und seinen abonnierten Events
Endpoint-URLs müssen HTTPS sein, höchstens 2048 Zeichen lang und öffentlich erreichbar. URLs auf privaten, Loopback-, Link-Local- oder anderweitig internen Adressen werden beim Erstellen oder Aktualisieren des Endpoints mit einem 422 abgelehnt. Zustellungen erfolgen über die Zustellinfrastruktur von Bird außerhalb Ihres Netzwerks.
Das Array events enthält bis zu 100 Typen aus dem Event-Katalog. Ein Endpoint empfängt nur die Typen, die er auflistet. Verwenden Sie PATCH /v1/webhooks/{webhook_id}, um die vollständige Liste für zukünftige Zustellungen zu ersetzen. Um jedes Event zu empfangen, abonnieren Sie jeden Typ: Ein Typ außerhalb des Katalogs wird mit einem 422 abgelehnt, das gilt auch für einen Wildcard wie sms.*. Bestehende Abonnements erweitern sich nicht, wenn neue Typen verfügbar werden.
Die Antwort auf die Erstellung enthält den Signierschlüssel secret des Endpoints (mit Präfix whsec_) genau einmal. Speichern Sie ihn sofort in Ihrem Secret-Manager; er kann nicht erneut abgerufen werden. Falls Sie ihn verlieren, rotieren Sie ihn.
Codebeispiel
{
  "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"
}
Endpoints unterstützen vollständiges CRUD: Auflisten, Abrufen, Aktualisieren und Löschen. Das Löschen eines Endpoints stoppt alle Zustellungen an ihn, einschließlich Wiederholungsversuche früherer fehlgeschlagener Zustellungen, und kann nicht rückgängig gemacht werden. Um Zustellungen vorübergehend zu stoppen, setzen Sie stattdessen status auf paused. Ein Workspace kann mehrere Endpoints registrieren, jeweils mit eigener URL, eigenem Event-Filter und eigenem Secret.

Signaturen verifizieren

Jede Zustellung enthält drei Header:
HeaderWert
webhook-idIdentifiziert die Event-Zustellung. Wiederholungsversuche und Replays verwenden denselben Wert.
webhook-timestampUnix-Zeitstempel (Sekunden) dieses Zustellversuchs
webhook-signaturev1,<base64 HMAC-SHA256>, möglicherweise mehrere Signaturen durch Leerzeichen getrennt
Die Signatur ist ein HMAC-SHA256 über den String {webhook-id}.{webhook-timestamp}.{raw request body}, mit dem Secret Ihres Endpoints als Schlüssel (entfernen Sie das Präfix whsec_ und dekodieren Sie den Rest per Base64, um die Schlüsselbytes zu erhalten). Ihr Handler sollte die Signatur verifizieren, Zustellungen ablehnen, deren webhook-timestamp älter als 5 Minuten ist, und anhand von webhook-id deduplizieren: Bird liefert at-least-once, dieselbe Zustellung kann also mehrfach eintreffen.
Mit der Bird SDK sind Signatur- und Zeitstempelprüfung ein einziger Aufruf; die Deduplizierung bleibt in Ihrem Handler:
// 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
Das Ablehnen einer Zustellung mit 400, wie in den Beispielen oben, verwirft das Event nicht: Wir wiederholen es nach dem Zeitplan unten. Das ist Absicht, und genau das wollen Sie. Die häufigste Ursache einer fehlgeschlagenen Verifizierung ist ein Secret, das Ihr Handler noch nicht hat – während einer Rotation oder eines fehlerhaften Deploys. Das Wiederholungsfenster gibt Ihnen die Chance, das Secret zu korrigieren und das Event dennoch zu empfangen. Geben Sie 2xx nur zurück, wenn Sie die Zustellung endgültig verwerfen wollen.
Jede Standard-Webhooks-Referenzbibliothek funktioniert ebenfalls. Wenn Sie manuell verifizieren, ist das Vorgehen:
Codebeispiel
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"))
    );
  });
}
Berechnen Sie den HMAC immer über die rohen Request-Body-Bytes. Parsen und erneutes Serialisieren des JSON verändert Leerzeichen oder Schlüsselreihenfolge und bricht die Signatur.

Zustellsemantik

Jede Zustellung ist ein Event pro HTTP-POST mit Content-Type: application/json, kein Batching. Ihr Endpoint hat 15 Sekunden Zeit zu antworten; jeder 2xx-Status gilt als Erfolg, alles andere (einschließlich 3xx-Redirects und Timeouts) gilt als Fehler. Jeder Fehler folgt demselben Wiederholungszeitplan. Der zurückgegebene Status ändert, was Sie im Zustellversuch-Log sehen, nicht ob wir es erneut versuchen: Es gibt keinen Statuscode, der die Zustellung vorzeitig stoppt. Antworten Sie schnell und verarbeiten Sie asynchron: Stellen Sie das Event in eine Queue und geben Sie 200 zurück, bevor Sie die eigentliche Arbeit erledigen.
Nach dem ersten Versuch werden fehlgeschlagene Zustellungen nach diesem Zeitplan wiederholt, mit ±20 % Jitter, damit sich Wiederholungsversuche nicht synchronisieren:
WiederholungVerzögerung nach dem vorherigen Versuch
15 Sekunden
25 Minuten
330 Minuten
42 Stunden
55 Stunden
610 Stunden
710 Stunden
Das sind acht Versuche über etwa 27,5 Stunden. Ein 429 oder Connection-Timeout wartet mindestens 60 Sekunden bis zum nächsten Versuch, und ein Retry-After-Header in Ihrer Antwort wird berücksichtigt. Jede Wiederholung trägt dieselbe webhook-id, was die Deduplizierung ermöglicht. Nach dem letzten Wiederholungsversuch ist die Zustellung endgültig fehlgeschlagen; Replay stellt sie wieder her.
Zustellungen sind nicht geordnet. Ein email.delivered kann vor dem email.accepted für dieselbe Nachricht eintreffen, besonders wenn Wiederholungsversuche beteiligt sind. Sortieren Sie nach dem Feld timestamp im Event-Payload, niemals nach der Eingangsreihenfolge.

Endpoints betreiben

Testsendungen

POST /v1/webhooks/{webhook_id}/test sendet ein signiertes synthetisches Event an Ihren Endpoint und gibt das Ergebnis synchron zurück: ob Ihr Endpoint es akzeptiert hat, den zurückgegebenen HTTP-Status und die Round-Trip-Latenz. Der Test-Body ist ein minimaler JSON-Stub, der nur den Event-type enthält und genau wie eine echte Zustellung signiert ist; er bildet kein reales Event-Payload nach. Übergeben Sie {"event_type": "email.delivered"}, um einen beliebigen Typ aus dem Katalog auszuwählen, abonniert oder nicht, oder lassen Sie den Body weg, um den ersten abonnierten Event-Typ des Endpoints zu verwenden.
Ihr Endpoint hat 10 Sekunden Zeit zu antworten. Ein nicht erreichbarer Endpoint erzeugt status: failed im Response-Body, während der Request selbst erfolgreich ist. Verwenden Sie dieses Ergebnis, um die Konnektivität zu debuggen. Testsendungen gehen direkt an Ihren Endpoint: Sie funktionieren bei einem pausierten Endpoint und werden nicht im Zustellversuch-Log aufgezeichnet. Ein 412 bedeutet, dass der Endpoint noch nicht getestet werden kann, weil ihm ein gültiges Signier-Secret oder ein abonnierter Event-Typ fehlt.
Für End-to-End-Tests mit echten Event-Flows senden Sie an die Sandbox-Adressen: Sandbox-Sendungen erzeugen echte Webhook-Events über den normalen Zustellpfad, was der beste Weg ist, Ihren Handler vor dem Livebetrieb zu testen.

Fehlgeschlagene Zustellungen erneut abspielen

POST /v1/webhooks/{webhook_id}/replay reiht die erneute Zustellung fehlgeschlagener Zustellungen ein. Events, die der Endpoint bereits erfolgreich empfangen hat, werden übersprungen, sodass ein Replay niemals doppelt zustellt; ein erneut zugestelltes Event trägt seine ursprüngliche webhook-id, sodass Ihre Deduplizierungsprüfung auch Replays abdeckt. Nur fehlgeschlagene Versuche werden erneut abgespielt: Ein Event, das Ihrem Endpoint nie gesendet wurde, hat keinen fehlgeschlagenen Versuch, und ein Replay stellt es daher nicht wieder her.
Übergeben Sie since/until-Zeitstempel, um das Zeitfenster einzugrenzen (Standard: die letzten 24 Stunden bis zum Zeitpunkt der Anfrage). Beide Grenzen sind inklusiv und beziehen sich auf den Zeitpunkt des Zustellversuchs, nicht auf den Zeitpunkt des Ereignisses. Ein Wiederholungsversuch, der seinem Ereignis um einen Tag hinterherhinkt, fällt also anhand der Stunde seines Versuchs in das Fenster. Replay liest das Protokoll der Zustellversuche, das drei Tage aufbewahrt wird – das ist die älteste erreichbare Historie: ein früherer since-Wert erweitert das Fenster, ohne ältere Einträge wiederherzustellen. Ein Replay umfasst höchstens die ältesten 10.000 Ereignisse im Fenster.
Der Request gibt 202 zurück und Events werden asynchron erneut zugestellt. Eine erneute Zustellung erhält einen Versuch, nicht den Wiederholungszeitplan oben. Der Versuch wird aufgezeichnet und der Job ist abgeschlossen, unabhängig davon, ob Ihr Endpoint ihn akzeptiert hat. Ein Replay an einen weiterhin defekten Endpoint kostet also einen Request pro Event statt acht; reparieren Sie den Endpoint und spielen Sie erneut ab. Diese Fehler beeinflussen den Endpoint-Zustand nicht: Ein Replay kann einen Endpoint nicht auf degraded setzen oder automatisch pausieren. Eine erneute Zustellung, die Ihr Endpoint akzeptiert, löscht beides.
Spielen Sie einen paused-Endpoint erneut ab, und der Request gibt weiterhin 202 zurück, aber nichts wird erneut zugestellt. Aktivieren Sie ihn zuerst erneut, wie unter Auto-Pause und Reaktivierung beschrieben.
Replays sind auf 20 pro Organisation pro UTC-Tag begrenzt; darüber hinaus gibt der Request einen 429 (WebhookReplayQuotaExceeded) zurück. Die Antwort enthält weder einen Zähler noch eine Task-ID. Verfolgen Sie Ergebnisse mit GET /v1/webhooks/{webhook_id}/attempts, das die letzten Zustellversuche vom neuesten zum ältesten mit Statuscodes und Latenz auflistet. Jeder HTTP-Request hat seinen eigenen Eintrag, sodass ein wiederholtes Event einmal pro Versuch erscheint und eine erneute Zustellung als weiterer Eintrag.

Signier-Secret rotieren

POST /v1/webhooks/{webhook_id}/rotate-secret generiert ein neues Secret und gibt es einmalig zurück. Für die nächsten 24 Stunden signiert Bird jede Zustellung mit beiden Secrets. Der webhook-signature-Header enthält die durch Leerzeichen getrennten Signaturen (v1,<old> v1,<new>), sodass Sie das neue Secret während der Überlappung deployen können. Standard-Webhooks-Bibliotheken probieren alle Signaturen automatisch. Nach 24 Stunden signiert das alte Secret nicht mehr. Ein Endpoint hält höchstens 5 gleichzeitig gültige Secrets, sodass wiederholtes Rotieren innerhalb des Überlappungsfensters mit WebhookTooManySecrets fehlschlägt, bis ein älteres Secret abläuft.

Auto-Pause und Reaktivierung

Der status eines Endpoints ist active, degraded oder paused. Aktuelle Zustellungsfehler markieren einen Endpoint als degraded als Zustandswarnung; wir liefern und wiederholen weiterhin. Ein Endpoint, der etwa fünf Tage lang durchgehend fehlschlägt, wird automatisch paused und alle Zustellungen stoppen; eine erfolgreiche Zustellung in diesem Zeitraum setzt den Zähler zurück. Ein pausierter Endpoint nimmt nie von selbst den Betrieb wieder auf. Reaktivieren Sie ihn mit PATCH /v1/webhooks/{webhook_id} und {"status": "active"} (oder über die Webhooks-Seite im Dashboard) und spielen Sie dann per Replay die Versuche erneut ab, die vor der Pausierung fehlgeschlagen sind. Reaktivieren Sie zuerst: Ein Replay, das angefordert wird, während der Endpoint noch pausiert ist, stellt nichts erneut zu. Events, die während der Pausierung eintrafen, wurden nie gesendet, sodass ein Replay diese nicht wiederherstellt.
Jede der folgenden Aktionen setzt einen degraded-Endpoint auf active zurück:
Was ihn zurücksetztWarum
Eine Zustellung ist erfolgreichDer Endpoint hat erneut ein Event akzeptiert.
Änderung der url des EndpointsDie aufgezeichneten Fehler beschreiben ein Ziel, das Sie nicht mehr verwenden.
Reaktivierung eines paused-EndpointsEr geht wieder in Betrieb, daher gelten seine alten Fehler nicht mehr.
Eine Testsendung, die 2xx zurückgibtSie haben gezeigt, dass der Endpoint erreichbar ist.
Das Bearbeiten der Beschreibung eines Endpoints oder seiner abonnierten Event-Typen sagt nichts über die Erreichbarkeit aus, sodass degraded bestehen bleibt – ebenso bei einer fehlgeschlagenen Testsendung.
Wir benachrichtigen die Inhaber der Organisation per E-Mail, wenn ein Endpoint erstmals degraded wird – einmal pro Episode, nicht einmal pro fehlgeschlagener Zustellung. Eine erneute Verschlechterung nach einer Erholung löst eine weitere E-Mail aus.

Event-Katalog

Event-Payloads enthalten kompakte, empfängerbezogene Fakten zur Korrelation mit Ihrem System. Sie enthalten nicht die vollständige Ressource. Wenn Sie mehr Kontext benötigen, rufen Sie die Ressource über ihre ID ab. Event-Typen folgen der resource.action-Benennung und sind nach Produkt gruppiert; die Event-Seite jedes Produkts enthält die Payload-Felder pro Event:
  • E-Mail-Events: der Zustelllebenszyklus (email.accepted bis email.delivered oder email.bounced), Engagement (email.opened, email.clicked), Abmeldungen und eingehende E-Mails
  • SMS-Events: der Nachrichtenlebenszyklus von sms.accepted bis zu einem Endstatus
  • WhatsApp-Events: whatsapp.accepted bis whatsapp.delivered, whatsapp.read, whatsapp.failed, whatsapp.rejected, whatsapp.received für eine eingehende Nachricht und whatsapp.reacted, wenn ein Nutzer auf eine Ihrer Nachrichten reagiert
  • Verify-Events: der Verifizierungslebenszyklus (verify.verification.created, verify.verification.verified) und die Zustellung jedes Bestätigungscode-Versuchs (verify.attempt.sent, verify.attempt.delivered, verify.attempt.undelivered)
  • Präferenz-Events: der kanalübergreifende Einwilligungsdatensatz: preference.granted, preference.revoked und preference.deleted
Jeder Zustellungs-Body ist der verschachtelte Standard-Webhooks-Envelope mit type, timestamp und einem typspezifischen data-Objekt. Der webhook-id-Header trägt die Event-Identität. Der Envelope-Wert timestamp zeichnet auf, wann das Event eingetreten ist. Der webhook-timestamp-Header zeichnet den aktuellen Zustellversuch auf und ändert sich bei jedem Wiederholungsversuch.
Codebeispiel
{
  "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
  }
}
Der data jedes E-Mail-Events enthält email_id, recipient_id, workspace_id, die recipient-Adresse und dessen Envelope-recipient_role. Er enthält außerdem tags und metadata aus dem Send-Request, oder null, wenn nicht angegeben. Er trägt auch broadcast_id, das den Broadcast benennt, in dessen Rahmen die Sendung erfolgte, oder null, wenn kein Broadcast dahinterstand. Bei email.unsubscribed und email.list_unsubscribed schließt null einen Broadcast nicht aus; E-Mail-Events erklärt warum. Event-Typen fügen dieser Basis eigene Felder hinzu. Jede Variante hat ein stabiles Feld-Set: Felder sind standardmäßig erforderlich, und ihre Präsenz hängt nur vom Event-Typ ab.
Event-Namen werden nie umbenannt, und neue Typen werden hinzugefügt, wenn Produkte ausgeliefert werden. Schreiben Sie Ihren Handler daher so, dass er unbekannte Typen ignoriert.

Präferenz-Events

Erklärte Präferenzen (die Einwilligungen und Opt-outs, die im jeweiligen Kanal-Leitfaden beschrieben sind: E-Mail, SMS, WhatsApp) erstrecken sich über Kanäle hinweg, daher benennen ihre Events den Kanal im Payload statt im Typ. preference.granted wird ausgelöst, wenn eine Einwilligung wirksam wird, preference.revoked, wenn ein Opt-out wirksam wird, und preference.deleted, wenn ein gespeicherter Eintrag entfernt wird und sein Schlüssel wieder keinen Eintrag hat. Ein Event bedeutet, dass sich der aktuelle Eintrag des Schlüssels geändert hat: Ein Statement, das den aktuellen Eintrag wiederholt, löst nichts aus, und eines, das als nicht in der richtigen Reihenfolge abgelehnt wird, ebenfalls nicht. Der Envelope-Wert timestamp gibt an, wann das Statement wirksam wurde – bei einem rückdatierten Statement ist das der Zeitpunkt der Erstellung, nicht der Zeitpunkt, zu dem es Bird erreicht hat.
Jeder Payload trägt den vollständigen Präferenzschlüssel: channel, handle, sender_scope und topic_id, wobei die Scoping-Felder present-with-null sind, wenn sie nicht einschränken. Neben dem Schlüssel stehen der coverage der Erklärung, der preference_id, die transition_id des Historieneintrags, den der Schreibvorgang angehängt hat, und der contact_id, dessen Handle bei der Aufzeichnung der Erklärung übereinstimmte, oder null:
Codebeispiel
{
  "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
  }
}

Nächste Schritte