Webhook-Payloads
Der maßgebliche Wire-Contract für ausgehende Webhooks: Envelope, Signatur-Header, Datenstrukturen je Ereignis und Payload-Versionierung.
Dies ist der maßgebliche Wire-Contract für die ausgehenden Webhooks, die Owlat an Ihre Endpunkte sendet. Um Abonnements zu konfigurieren, Signaturen zu prüfen und das Retry-Verhalten auf höherer Ebene zu verstehen, beginnen Sie mit der Webhooks-Referenz; diese Seite dokumentiert die exakte JSON-Struktur jedes Ereignisses.
Jedes nachfolgende Payload ist zum Stand payloadVersion 1 eingefroren. Ein Feld umzubenennen, einen Typ zu ändern, ein Feld zu entfernen oder ein Pflichtfeld hinzuzufügen, ist für Empfänger ein Breaking Change und erfordert eine Anhebung der Payload-Version. Einen völlig neuen Ereignistyp hinzuzufügen ist kein Breaking Change, sofern Ihr Handler unbekannte Ereignisse ignoriert.
Envelope
Jede Zustellung ist die JSON-serialisierte Form dieses Envelopes:
{
"event": "<event-name>",
"timestamp": "<ISO-8601 UTC>",
"data": { /* event-specific, see below */ }
}
event— das Ereignis-Literal, z. B.email.delivered.timestamp— Zeitpunkt, zu dem der Envelope erzeugt wurde, als ISO-8601-UTC-String.data— das ereignisspezifische Payload. Der HTTP-Request-Body ist exakt die ZeichenketteJSON.stringify({ event, timestamp, data }), und die HMAC-Signatur wird über genau diese Zeichenkette berechnet.
Signatur-Header
Jede Zustellung trägt diese Header:
| Header | Wert |
|---|---|
X-Signature | HMAC-SHA256 des Request-Bodys, hex-kodiert, mit Ihrem Webhook-Secret als Schlüssel |
X-Timestamp | Unix-Sekunden zum Zeitpunkt des Zustellversuchs |
X-Webhook-Id | Convex-ID des Webhook-Abonnements |
User-Agent | Owlat-Webhooks/1.0 |
Content-Type | application/json |
Webhook-Secrets verwenden das Format whsec_... und werden beim Anlegen des Webhooks erzeugt. Prüfen Sie die Signatur, indem Sie den HMAC über den rohen Body neu berechnen und in konstanter Zeit vergleichen — siehe das Verifizierungsbeispiel in der Webhooks-Referenz.
X-Signature wird über die wörtlichen Request-Bytes berechnet. Wenn Ihr Framework das JSON parst und neu serialisiert, bevor Sie es hashen, können sich Schlüsselreihenfolge oder Leerzeichen ändern und die Signatur stimmt nicht überein. Lesen Sie zuerst den rohen Body-String, verifizieren Sie ihn und parsen Sie erst danach.
Strukturvorgaben für data
In Payload-Version 1 ist data eine flache Map aus Primitiven: string, number, boolean oder null. Es gibt keine verschachtelten Objekte oder Arrays. Alles Strukturierte wird JSON-kodiert in ein String-Feld gelegt — siehe topic.unsubscribed.listsRemoved weiter unten. Eine künftige Payload-Version kann diese Vorgabe lockern.
E-Mail-Ereignisse
Diese werden ausgelöst, während eine Kampagnen- oder transaktionale E-Mail Versand, Zustellung und Interaktion durchläuft.
email.sent
Wird ausgelöst, wenn eine Kampagnen- oder transaktionale E-Mail an den Versanddienst übergeben wird. Genau eines von campaignId / transactionalEmailId ist gesetzt; das jeweils andere ist null.
{
"email": "recipient@example.com",
"campaignId": "<Id<'campaigns'> | null>",
"transactionalEmailId": "<Id<'transactionalEmails'> | null>",
"timestamp": "<ISO-8601>"
}
email.delivered
Wird ausgelöst, wenn der empfangende Mailserver die Zustellung annimmt.
{
"email": "recipient@example.com",
"timestamp": "<ISO-8601>"
}
email.opened
Wird beim ersten Öffnen ausgelöst, erfasst über ein 1×1-Pixel-Beacon.
{
"email": "recipient@example.com",
"timestamp": "<ISO-8601>"
}
email.clicked
Wird bei jedem Linkklick ausgelöst, über die Weiterleitung der getrackten URL.
{
"email": "recipient@example.com",
"url": "https://www.example.com/landing",
"timestamp": "<ISO-8601>"
}
email.bounced
Wird bei einem harten oder weichen Bounce ausgelöst, den der Dienst meldet. message ist der vom Dienst gelieferte Grund oder ein leerer String "", wenn keiner angegeben wurde.
{
"email": "recipient@example.com",
"bounceType": "hard | soft",
"message": "<provider reason or ''>",
"timestamp": "<ISO-8601>"
}
email.complained
Wird ausgelöst, wenn der Empfänger die E-Mail als Spam markiert (Feedback-Loop des Dienstes).
{
"email": "recipient@example.com",
"timestamp": "<ISO-8601>"
}
Kontaktereignisse
contact.created
Wird ausgelöst, wenn ein neuer Kontakt hinzugefügt wird. source hält fest, wie der Kontakt ins System gelangt ist.
{
"contactId": "<Id<'contacts'>>",
"email": "person@example.com",
"source": "api | import | form | transactional | inbound",
"timestamp": "<ISO-8601>"
}
source | Bedeutung |
|---|---|
api | Über die Kontakte-API angelegt |
import | Über einen CSV-Import hinzugefügt |
form | Hat ein gehostetes Formular abgesendet |
transactional | Beim Versand einer transaktionalen E-Mail automatisch angelegt |
inbound | Aus einer eingehenden Nachricht erstellt |
Themen-Ereignisse
topic.unsubscribed
Wird ausgelöst, wenn sich ein Kontakt von einem oder mehreren Themen abmeldet.
{
"contactId": "<Id<'contacts'>>",
"email": "person@example.com",
"unsubscribedAt": 1710505200000,
"listsRemoved": "[{\"topicId\":\"...\",\"topicName\":\"...\"}]"
}
Da Payload-Version 1 nur flache Primitive erlaubt, ist listsRemoved ein String, der JSON enthält, und kein Array. Sie müssen ihn mit JSON.parse einlesen, um die Einträge zu lesen. Jeder Eintrag hat die Form { topicId, topicName }. Beachten Sie außerdem, dass unsubscribedAt Epoch-Millisekunden enthält (eine Zahl), anders als das ISO-8601-timestamp der E-Mail-Ereignisse.
const removed: Array<{ topicId: string; topicName: string }> =
JSON.parse(data.listsRemoved)
test
Nicht abonnierbar — wird nur gesendet, wenn Sie im Webhook-Dashboard den Testauslöser verwenden, um einen Empfänger-Endpunkt zu prüfen. Nutzen Sie ihn, um Erreichbarkeit und Signaturprüfung Ihres Endpunkts zu bestätigen, bevor Sie sich auf Live-Ereignisse verlassen.
{
"message": "This is a test webhook from Owlat",
"webhookId": "<Id<'webhooks'>>",
"webhookName": "<your label>"
}
Payload-Version und Vorwärtskompatibilität
Der Envelope trägt auf der Leitung kein Versionsfeld, doch jede Zeile im Zustellprotokoll speichert die payloadVersion, mit der sie erzeugt wurde. Schreiben Sie Ihren Empfänger defensiv:
- Unbekannte Ereignisse ignorieren. Neue Ereignis-Literale können ohne Versionsanhebung hinzukommen; Handler, die unbekannte
event-Werte strikt ablehnen, brechen beim Upgrade. - Unbekannte Felder ignorieren. Zusätzliche Felder innerhalb einer Struktur sind möglich; scheitern Sie nicht an Extras.
- Nach Typ verzweigen, nicht nach Position. Denken Sie daran:
timestampist bei E-Mail-/Kontaktereignissen ISO-8601,unsubscribedAtbeitopic.unsubscribedhingegen Epoch-Millisekunden.
Eine künftige Version 2 kann die Beschränkung auf flache Primitive lockern und verschachtelte Arrays/Objekte zulassen (womit die JSON-String-Kodierung von listsRemoved entfällt). Wenn es so weit ist, wird die Version angehoben und beide Strukturen werden dokumentiert.
Retry-Verhalten und Zustellprotokolle
Owlat behandelt jede Nicht-2xx-Antwort — ebenso wie einen Netzwerkfehler, ein Timeout oder eine URL, die auf ein privates/lokales Netzwerk auflöst — als fehlgeschlagene Zustellung und wiederholt sie mit Backoff auf insgesamt bis zu 3 Versuche:
Versuch 1
Wird sofort zugestellt, wenn das Ereignis ausgelöst wird.
Versuch 2
Wiederholung nach 1 Minute, falls Versuch 1 fehlgeschlagen ist.
Versuch 3
Wiederholung nach 5 Minuten, falls Versuch 2 fehlgeschlagen ist. Nach diesem letzten Versuch wird die Zustellung als fehlgeschlagen markiert und nicht erneut wiederholt.
Jede Anfrage hat ein 30-Sekunden-Timeout und folgt keinen Weiterleitungen. Jeder Versuch wird in einem Zustellprotokoll mit einem dieser Status festgehalten:
| Status | Bedeutung |
|---|---|
pending | Protokolliert, eingeplant, noch nicht versucht |
retrying | Ein Versuch ist fehlgeschlagen und ein weiterer ist eingeplant |
success | Der Endpunkt hat eine 2xx-Antwort geliefert |
failed | Alle Versuche aufgebraucht oder der Webhook war deaktiviert/nicht auffindbar |
Protokolle erfassen den HTTP-Statuscode, einen gekürzten Response-Body (die ersten 1000 Zeichen), im Fehlerfall eine Fehlermeldung sowie die Dauer je Versuch. Da Owlat bei jeder Nicht-2xx-Antwort wiederholt, sollte Ihr Endpunkt mit einem 2xx quittieren, sobald er das Ereignis dauerhaft entgegengenommen hat, und asynchron verarbeiten — und er sollte idempotent sein, denn ein langsames oder unzuverlässiges 2xx kann dennoch zu einer doppelten Zustellung führen.