Webhook payload cookbook: one payload per event family
For each of the nine event families: the exact JSON your endpoint receives, the signature header shape, the dedupe key, and one receiver example. Mechanics (retry schedule, dead-letter, rotation) are deferred to the durable webhook consumer guide; the event catalog page lists all ~300 event types and the events reference carries field-level detail.Request shape (all families)
- Signature header:
t=<epoch-seconds>,v1=<hmac-sha256-hex>over${t}.${raw-body}with yourwhsec_...secret. Verify with the polyglot signature recipe. During a rotation window the header carries multiplev1candidates — accept if any matches. - Envelope:
{ id, type, created_at, data }—idis the dedupe key (stable across retries of the same event, at-least-once delivery). Persist seen ids; a hit is a duplicate, ack200and stop.
1. Message lifecycle (message.*)
message.expired and message.submitted_no_receipt never dispatch as their own type — they ride on message.failed, branch on data.status. Engagement events (email.opened, email.clicked, short_link.click) are siblings, not lifecycle transitions.
2. Voice / call lifecycle (call.*, recording.*, voicemail.*)
3. Porting (porting.request.*)
porting.request.cancelled flips cancelled_from_status; porting.request.manual_review_required adds manual_reason for routing by failure mode.
4. Verify (verification.*)
channel is sms / voice / silent. verification.checked fires on every persisted attempt — including non-terminal pending — check data.outcome before acting.
5. CDP events (cdp.*, contact.*)
contact.opted_in / contact.opted_out with { channel, phone, keyword, timestamp }; segment flips as contact.segment_changed with { contact_id, previous_label, new_label, computed_at }.
6. Delivery receipts (submitted_no_receipt / expired)
The carrier accepted the submission but never confirmed the handset; both statuses fan out on message.failed.
submitted_no_receipt is intermediate — do not mark yourself delivered. expired is terminal. Dedupe stays on envelope id even when one message_id re-reports.
7. Campaign (campaign.*)
8. Flows (flow.*)
status is completed, failed, or timeout. Sibling flow.execution.started / .completed / .failed add error and duration_ms.
9. Number lifecycle (number.*)
number.purchased / number.released carry { number_id }; a mid-flight amendment arrives as porting.request.supplement_submitted, never as number.ported with a half-status.
Order: verification before the parser
An unauthenticated body is wire noise — parser-first hands attacker bytes to your JSON stack.- Read the raw body bytes (before any JSON parser runs).
- Verify
X-Orbit-Signature— the one-function-call recipe in seven languages is in Verify webhook signatures in every language; during rotation accept anyv1candidate. - Parse the JSON envelope.
- Dedupe on envelope
id— a seen id gets a200ack and stops. - Route
typeto the family handler (the receiver examples above).
See also
- Build a durable webhook consumer — retries, DLQ, secret rotation, ack-fast pattern
- Explore webhook event schemas from the catalog — machine-readable schemas + CI fingerprint pins
- Webhook events reference — field-level detail for every event type