Webhooks empfangen
Statt die API in einer Schleife abzufragen, hinterlegen Sie eine URL, und wir rufen Sie auf, wenn sich etwas ändert.
Ein Endpunkt wird für einen Modus registriert — live oder test — und für eine Liste von Ereignisnamen abonniert. Ein Test-Endpunkt erhält genau dieselbe Nutzlastform, gleich signiert, wie ein Live-Endpunkt.
Die Form eines Ereignisses
Abschnitt betitelt „Die Form eines Ereignisses“Das ist, bis auf die id Byte für Byte, der Rumpf, den ein Test-Endpunkt für
invoiceSent erhält — jede Test-Zustellung trägt dieselbe Beispielressource,
sodass Sie das Eintreffende mit dieser Seite vergleichen können:
{ "id": "evt_3f9c2b7d1e8a4c6f9b0d2e5a7c1f4b8d", "type": "invoiceSent", "createdAt": "2026-01-15T09:30:00.000Z", "mode": "test", "data": { "object": { "id": "inv_0000000000000001", "projectId": "prj_0000000000000001", "status": "sent", "refNumber": "F-2026-0042", "isFinalInvoice": true, "amount": 1850.5, "clientRef": { "id": "cli_0000000000000001", "name": "Acme AG", "email": "billing@example.com" }, "sentAt": "2026-01-15T09:30:00.000Z", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-01-15T09:30:00.000Z" } }}| Feld | Bedeutung |
|---|---|
id |
Die Kennung des Ereignisses, evt_ gefolgt von 32 Hexadezimalzeichen. Ihr Schlüssel zum Entduplizieren — siehe unten. |
type |
Der Ereignisname — siehe Ereignisnamen. |
createdAt |
Der Zeitpunkt, an dem das Geschäftsereignis eingetreten ist, als ISO-8601-Zeichenkette in UTC. Nicht der Zeitpunkt dieses Zustellversuchs: eine Wiederholung trägt denselben Wert. |
mode |
live oder test, der Modus des Endpunkts, an den das Ereignis zugestellt wurde. |
data.object |
Die Ressource, um die es in dem Ereignis geht. |
data.object wird aus denselben Schemata gebaut wie die Leseantworten der API.
Sie verarbeiten also eine Form pro Ressource, gleich ob Sie sie über ein GET
erhalten oder per Webhook zugestellt bekommen haben. Jeder Zeitstempel, auf dem
Umschlag wie in der Ressource, ist eine ISO-8601-Zeichenkette.
Lesen Sie die Ressource aus data.object, nicht aus data: Diese Verschachtelung
erlaubt es uns, später ein Geschwisterfeld hinzuzufügen — etwa die vorherigen
Werte der geänderten Felder —, ohne zu verschieben, was Sie bereits lesen.
Ereignisnamen
Abschnitt betitelt „Ereignisnamen“Ereignisnamen sind in camelCase geschrieben und verwenden dasselbe Vokabular
wie die Benachrichtigungen in der App: invoiceSent, quoteAccepted,
jobRescheduled. Ein Endpunkt kann jedes dieser Ereignisse abonnieren:
| Ressource | Ereignisse |
|---|---|
| Projekt | projectCreated, projectUpdated, projectShared, projectClientValidationRequested, projectAccepted, projectDeclined, projectCanceled, projectDone, projectRated, projectReminderCreated, projectReminderUpdated, projectReminderDeleted, projectReminderSent |
| Einsatz | jobCreated, jobUpdated, jobScheduled, jobRescheduled, jobDone, jobNoShow, jobCanceled |
| Nachricht | messageCreated |
| Offerte | quoteCreated, quoteUpdated, quoteDeleted, quoteSent, quoteAccepted, quoteDeclined |
| Rechnung | invoiceCreated, invoiceUpdated, invoiceDeleted, invoiceSent, invoiceAccepted, invoiceDeclined, invoiceAutoMarkPaidFailed, invoiceReminderCreated, invoiceReminderUpdated, invoiceReminderDeleted, invoiceReminderSent, invoiceReminderFailed |
Dieselbe Liste ist die event-Aufzählung von POST /test/webhooks/send in der
OpenAPI-Beschreibung der API, die massgeblich ist: Ein Name,
der dort nicht steht, wird mit einem 400 abgewiesen, das die gültigen Namen
aufführt.
Die Signatur prüfen
Abschnitt betitelt „Die Signatur prüfen“Jede Zustellung trägt einen Tay-Signature-Header:
Tay-Signature: t=1758186764,v1=5257a869e7b2cd0e2c...tist der Unix-Zeitstempel der Signatur.v1ist ein HMAC-SHA256 in Hexadezimal über die Zeichenkette${t}.${rawBody}, berechnet mit dem Signaturgeheimnis des Endpunkts.
Das ist Stripes Verfahren. Wer schon einmal einen Stripe-Webhook geprüft hat, hat diesen Code bereits geschrieben.
Die Prüfung besteht aus vier Schritten:
- Lesen Sie den Rohrumpf, bevor ein JSON-Parser ihn berührt.
- Lösen Sie
tundv1aus dem Header heraus. - Berechnen Sie den HMAC über
${t}.${rawBody}neu und vergleichen Sie ihn in konstanter Zeit mitv1. - Weisen Sie ab, wenn
tmehr als 5 Minuten von der aktuellen Zeit abweicht.
import { createHmac, timingSafeEqual } from 'node:crypto';
/** Das Toleranzfenster: fünf Minuten, in beide Richtungen. */const TOLERANCE_SECONDS = 300;
export function verify(rawBody, header, secret) { const parts = Object.fromEntries( header.split(',').map((part) => part.split('=')), );
const expected = createHmac('sha256', secret) .update(`${parts.t}.${rawBody}`) .digest('hex');
const received = Buffer.from(parts.v1, 'hex'); const computed = Buffer.from(expected, 'hex');
// `timingSafeEqual` verlangt zwei Puffer gleicher Länge. if (received.length !== computed.length) return false; if (!timingSafeEqual(received, computed)) return false;
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t)); return age <= TOLERANCE_SECONDS;}Der Zeitstempel ist gerade deshalb Teil der signierten Zeichenkette, damit Sie eine Wiedereinspielung erkennen können: Ohne ihn bliebe eine abgefangene Anfrage unbegrenzt gültig.
Anhand der Ereignis-ID entduplizieren
Abschnitt betitelt „Anhand der Ereignis-ID entduplizieren“Das ist der wichtigste Punkt dieser Seite.
Antwortet Ihr Endpunkt nicht mit 2xx — weil er langsam oder nicht erreichbar
ist oder einen Fehler geliefert hat — wird die Zustellung eine begrenzte Anzahl
Male wiederholt und danach als fehlgeschlagen markiert.
Ein Wiederholungsversuch trägt dieselbe id und dasselbe createdAt wie
der erste. Er ist kein neues Ereignis: Es ist dasselbe, erneut vorgelegt. Nur
das t im Signatur-Header ist neu, weil es den Versuch datiert, nicht das
Ereignis.
Sie müssen daher:
- die bereits verarbeiteten
ids in Ihrer eigenen Datenbank festhalten; - beim Empfang prüfen, ob die
iddort schon steht; - falls ja, mit
200antworten und sonst nichts tun.
Andernfalls verrechnet ein kurzzeitig langsamer Endpunkt zweimal, verschickt zwei E-Mails oder legt zwei Zeilen an. Kein Netz garantiert eine Genau-einmal-Zustellung, und wir tun es auch nicht: Wir garantieren, dass die ID stabil ist — damit können Sie die fehlende Garantie selbst herstellen.
Behandeln Sie auch die Reihenfolge als nicht garantiert. Zwei zeitlich nahe
Ereignisse können Sie in vertauschter Reihenfolge erreichen; verlassen Sie sich
auf createdAt, nicht auf die Ankunftsreihenfolge.
Was Ihr Endpunkt tun sollte
Abschnitt betitelt „Was Ihr Endpunkt tun sollte“- Schnell mit
2xxantworten. Empfang bestätigen, Arbeit einreihen, danach verarbeiten — langsame Verarbeitung führt zu einer Zeitüberschreitung, also zu einer Wiederholung, also zu einem Duplikat. - Auch bei einem unbekannten
typemit2xxantworten. Es können neue Ereignisse hinzukommen, und sie abzuweisen löst nur sinnlose Wiederholungen aus. - Niemals einer Nutzlast vertrauen, deren Signatur Sie nicht geprüft haben.