> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook payload cookbook: one payload per event family

> Copy-pasteable JSON payloads for the nine webhook event families — message lifecycle, voice/call, porting, verify, CDP, delivery receipts, campaign, flows, and number lifecycle — with the signature header shape, dedupe key, and one receiver example per family.

# 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](/guides/webhook-consumer); the [event catalog](/webhooks/events) page lists all \~300 event types and the [events reference](/reference/webhook-events) carries field-level detail.

## Request shape (all families)

```
POST /webhooks/orbit HTTP/1.1
Content-Type: application/json
X-Orbit-Signature: t=1715357600,v1=4f9c2e6b...<64-hex>
X-Devotel-Signature: t=1715357600,v1=4f9c2e6b...   # back-compat twin

{"id":"evt_abc123","type":"message.delivered","created_at":"…","data":{…}}
```

* Signature header: `t=<epoch-seconds>,v1=<hmac-sha256-hex>` over `${t}.${raw-body}` with your `whsec_...` secret. Verify with the [polyglot signature recipe](/guides/webhook-signature-verify-polyglot). During a rotation window the header carries multiple `v1` candidates — accept if any matches.
* Envelope: `{ id, type, created_at, data }` — `id` is the **dedupe key** (stable across retries of the same event, at-least-once delivery). Persist seen ids; a hit is a duplicate, ack `200` and 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.

```json theme={null}
{
  "id": "evt_msq01",
  "type": "message.delivered",
  "created_at": "2026-09-24T12:00:03Z",
  "data": {
    "message_id": "msg_abc123",
    "channel": "sms",
    "to": "+14155552671",
    "status": "delivered",
    "state_class": "terminal",
    "is_terminal": true,
    "timestamp": "2026-09-24T12:00:03Z"
  }
}
```

```javascript theme={null}
export function handleMessageEvent(event) {
  const { message_id, status, is_terminal } = event.data;
  updateDeliveryState(message_id, status, is_terminal);
}
```

## 2. Voice / call lifecycle (`call.*`, `recording.*`, `voicemail.*`)

```json theme={null}
{
  "id": "evt_voc01",
  "type": "call.completed",
  "created_at": "2026-09-24T12:02:22Z",
  "data": {
    "call_id": "call_abc123",
    "provider": "jambonz",
    "provider_call_sid": "CA-4f9c...",
    "direction": "inbound",
    "from": "+14155552671",
    "to": "+18005551234",
    "status": "completed",
    "duration_seconds": 142,
    "hangup_reason": "normal_clearing",
    "sip_response_code": 200,
    "sip_reason": "OK",
    "timestamp": "2026-09-24T12:02:22Z"
  }
}
```

```python theme={null}
def handle_call_event(event):
    d = event["data"]
    if event["type"] == "call.completed":
        record_cdr(d["call_id"], d["duration_seconds"], d["hangup_reason"])
```

## 3. Porting (`porting.request.*`)

```json theme={null}
{
  "id": "evt_prt01",
  "type": "porting.request.loa_signed",
  "created_at": "2026-09-24T09:15:00Z",
  "data": {
    "porting_id": "port_001",
    "numbers": ["+14155550100"],
    "signer_name": "Jane Doe",
    "signer_email": "jane@example.com",
    "signed_at": "2026-09-24T09:15:00Z"
  }
}
```

`porting.request.cancelled` flips `cancelled_from_status`; `porting.request.manual_review_required` adds `manual_reason` for routing by failure mode.

## 4. Verify (`verification.*`)

```json theme={null}
{
  "id": "evt_vfy01",
  "type": "verification.approved",
  "created_at": "2026-09-24T10:00:11Z",
  "data": {
    "verification_id": "vrf_3f1c0b2a8e4d",
    "channel": "sms",
    "to": "+14155552671"
  }
}
```

`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.*`)

```json theme={null}
{
  "id": "evt_cdp01",
  "type": "cdp.event.ingested",
  "created_at": "2026-09-24T11:00:00Z",
  "data": {
    "event_id": "cdpevt_abc123",
    "type": "track",
    "name": "Order Completed",
    "user_id": "user_42",
    "anonymous_id": "anon_abc",
    "contact_id": "con_abc",
    "properties": { "value": 99.99, "currency": "USD" },
    "received_at": "2026-09-24T11:00:00Z"
  }
}
```

Opt-outs arrive as `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`.

```json theme={null}
{
  "id": "evt_dlr01",
  "type": "message.failed",
  "created_at": "2026-09-24T18:00:01Z",
  "data": {
    "message_id": "msg_abc123",
    "channel": "sms",
    "status": "submitted_no_receipt",
    "state_class": "intermediate",
    "is_terminal": false,
    "timestamp": "2026-09-24T18:00:01Z"
  }
}
```

`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.*`)

```json theme={null}
{
  "id": "evt_cmp01",
  "type": "campaign.drip_step.completed",
  "created_at": "2026-09-24T14:00:00Z",
  "data": {
    "campaign_id": "camp_abc123",
    "step_index": 2,
    "sent": 480,
    "failed": 12
  }
}
```

## 8. Flows (`flow.*`)

```json theme={null}
{
  "id": "evt_flw01",
  "type": "flow.executed",
  "created_at": "2026-09-24T15:00:01Z",
  "data": {
    "flow_id": "flow_abc123",
    "execution_id": "exec_001",
    "status": "completed",
    "step_count": 8
  }
}
```

`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.

```json theme={null}
{
  "id": "evt_num01",
  "type": "number.ported",
  "created_at": "2026-09-24T16:00:00Z",
  "data": {
    "porting_id": "port_001",
    "numbers": ["+14155550100", "+14155550101"],
    "carrier": "Verizon",
    "provider": "telnyx",
    "status": "completed"
  }
}
```

## Order: verification before the parser

An unauthenticated body is wire noise — parser-first hands attacker bytes to your JSON stack.

1. Read the **raw** body bytes (before any JSON parser runs).
2. Verify `X-Orbit-Signature` — the one-function-call recipe in seven languages is in [Verify webhook signatures in every language](/guides/webhook-signature-verify-polyglot); during rotation accept any `v1` candidate.
3. Parse the JSON envelope.
4. Dedupe on envelope `id` — a seen id gets a `200` ack and stops.
5. Route `type` to the family handler (the receiver examples above).

## See also

* [Build a durable webhook consumer](/guides/webhook-consumer) — retries, DLQ, secret rotation, ack-fast pattern
* [Explore webhook event schemas from the catalog](/guides/webhook-event-catalog) — machine-readable schemas + CI fingerprint pins
* [Webhook events reference](/reference/webhook-events) — field-level detail for every event type
