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

# Read the inbound event catalog for carrier-folded webhook normalization

> Answer 'the carrier says delivered — which Orbit event fires on my endpoint' before subscribing — the inbound catalog folds Telnyx, Devotel softswitch, DIDWW, and Meta statuses onto canonical events, re-pin the mapping in CI, and reconcile receipts that arrive without a normalized block.

# Read the inbound event catalog for carrier-folded webhook normalization

## When to use this catalog

Two dashboard catalogs answer two different questions about webhooks:

* **Event catalog** (schema-level) — "what does the payload of each event type look like, and how do I validate it?" Use it when you are designing a receiver and need the JSON Schema, an example envelope, and a fingerprint to pin.
* **Inbound event catalog** (carrier-folded) — "my carrier reports `UNDLV` / `message.delivered` / `read` / `opt_out`; which canonical Orbit event arrives on my endpoint, and what does `data.status` say?" Use it when you are reading inbound statuses, not designing receiver schemas.

This page walks the second one. The carriers Orbit delivers inbound statuses from — Telnyx Messaging, the Devotel wholesale softswitch (SMPP), DIDWW, and Meta (WhatsApp, Messenger, Instagram) — each report their own status vocabulary. The inbound catalog folds every one of those vocabularies onto the same canonical webhook events so your receiver branches once, not once per carrier.

<Note>
  The catalog is static platform metadata with no tenant data. It answers
  the mapping question for everyone the same way.
</Note>

## The four fold-in carriers

Each carrier supplies inbound message lifecycle signals in its own vocabulary. The fold maps each raw status onto one canonical event, a closed `data.status` (`delivered` / `failed` / `sent` / `read` / `opted_out`), and a high-level `kind` (`delivery` / `failure` / `opt_out` / `received`):

| Carrier | Typical webhooks | Delivery folds into | Failure folds into |
| - | - | - | - |
| Telnyx | DLR webhooks (`message.sent`, `message.delivered`, `message.sending_failed`) | canonical `message.delivered`, `data.status="delivered"` | canonical `message.failed`, `data.status="failed"` |
| Devotel softswitch (SMPP) | MT delivery receipts (`DELIVRD`, `UNDLV`, `REJECTD`, `EXPIRED`) | canonical `message.delivered` | canonical `message.failed` |
| DIDWW | MO plus DLR callbacks (`delivered`, `completed`, `undelivered`, `rejected`) | canonical `message.delivered` | canonical `message.failed` |
| Meta (WhatsApp, Messenger, Instagram) | `statuses[]` entries (`sent`, `delivered`, `read`, `failed`) plus opt-out preference signals | canonical `message.delivered` and `message.read` | canonical `message.failed`, plus `contact.opted_out` for opt-outs |

On top of the per-carrier rows there is a universal inbound row: every carrier's inbound message (SMS MO, a WhatsApp text, a Messenger message, a DIDWW MO) folds onto `message.received`. A receiver listening for "a message arrived" needs exactly one branch.

Every mapping is a literal string match, case-insensitive and whitespace-trimmed. You never match a pattern — the catalog tells you exactly which wire strings resolve, and matching is first-hit-wins in a published order.

## Browse the dashboard view

In the dashboard, open **Developer → Webhooks → Inbound event catalog** at `/developer/webhooks/inbound-catalog`. The page is role-gated to owner, admin, and developer, and it renders the same table the API serves.

The page is a searchable list on the left and a detail pane on the right:

* **Search** matches canonical events, canonical statuses, kinds, and any raw upstream string — typing `undlv`, `delivered`, or `failure` all narrow to the same entries.
* **Selecting a canonical event** opens its detail: the canonical `data.status`, the kind, and the per-carrier vocabulary grouped into Telnyx, the Devotel softswitch, DIDWW, and Meta.
* **Copy event name** puts the canonical `type` value on your clipboard so you can paste it into your endpoint's event subscription.

Use the dashboard when you are eyeballing a mapping — a receipt landed and you want to know which canonical event fired. Use the API when you want CI or codegen to pin it.

## Fetch the mapping over the API

`GET /api/v1/webhooks/inbound-events` returns the same table in the standard `{ data, meta }` envelope. The static catalog needs no tenant scope; the read is authed the same way as the rest of the webhooks API.

```bash theme={null}
curl -s https://api.orbit.devotel.io/api/v1/webhooks/inbound-events \
  -H "X-API-Key: dv_live_sk_..." \
| jq '.data.events[] | {canonical_event, canonical_status, kind}'
```

Pull one canonical event's upstream vocabulary when you are wiring a receiver branch:

```bash theme={null}
curl -s https://api.orbit.devotel.io/api/v1/webhooks/inbound-events \
  -H "X-API-Key: dv_live_sk_..." \
| jq '.data.events[] | select(.canonical_event == "message.delivered")'
```

The entry's `equivalent_upstream` array is the literal-match table: every (provider, raw status) pair that resolves to this canonical event, returned in the order the normalizer tries them. Pin that array's checksum in CI the same way you pin event-schema fingerprints — a carrier vocabulary move shows up as a mapping-table diff, and you want that to fail your build before it silently skips a receiver branch.

## One receipt walked end to end

A Telnyx delivery status arrives on your endpoint after a one-step fold. Walk a concrete receipt through:

| Stage | Value |
| - | - |
| Raw carrier status (as it arrives on the wire) | `message.delivered` (a Telnyx DLR webhook event type) |
| Canonical event (the webhook `type` your endpoint receives) | `message.delivered` |
| Canonical `data.status` on the delivery envelope | `delivered` |
| Event kind | `delivery` |

The signed envelope your receiver gets:

```json theme={null}
{
  "id": "evt_b62mp11",
  "type": "message.delivered",
  "created_at": "2026-09-30T12:00:00Z",
  "data": {
    "message_id": "msg_x41",
    "channel": "sms",
    "status": "delivered",
    "provider_status": "message.delivered",
    "normalized": {
      "event": "message.delivered",
      "status": "delivered",
      "kind": "delivery",
      "provider": "telnyx",
      "raw_status": "message.delivered"
    }
  }
}
```

Two fields carry status. `data.status` is the closed canonical set you switch on. `data.provider_status` echoes the carrier's own term verbatim, so your per-carrier detail (an SMPP `EXPIRED` versus `REJECTD`, or a Telnyx `message.sending_failed` versus `delivery_failed`) survives the fold. The `normalized` block is the same mapping the catalog publishes — event, status, kind, provider, raw status — stamped at dispatch time so your receiver never has to repeat the lookup.

## Reconcile a receipt that arrives without a normalized block

The fold is a closed vocabulary. A carrier string the catalog does not cover resolves to no mapping, and the dispatch stamps the canonical `data.status` the emitter already carried with **no `normalized` block**. That is the difference between a mismapping (bad) and a non-mapping (expected for genuinely novel statuses): a non-mapping still delivers the canonical event, it just does not carry the catalog-verified normalization block.

Treat a missing `normalized` block as a reviewer alarm, not a drop:

```ts theme={null}
function handleInboundStatus(envelope) {
  const { status, provider_status, normalized } = envelope.data;

  if (!normalized) {
    // The emitter resolved the canonical event, but the raw status is
    // outside the published fold vocabulary. Log it and re-check the
    // catalog — if the raw status is now standard, add it to the fold;
    // if it is genuinely novel, keep your switch on the canonical pair.
    console.warn("unmapped inbound status", {
      status,
      provider_status,
      provider: envelope.data.provider,
    });
  }

  // Switch on the canonical status regardless — the event still delivers.
  switch (status) {
    case "delivered":
      markDelivered(envelope.data.message_id);
      break;
    case "failed":
      alertDeliverability(envelope.data.message_id, provider_status);
      break;
  }
}
```

Re-pin the fold before you silence the alarm. Fetch `GET /api/v1/webhooks/inbound-events` and check the raw string against the `equivalent_upstream` array the endpoint returns: if it appears, the mapping you are pinning in CI is stale; if it does not, the status is outside the published vocabulary and you decide whether to treat it as `delivered` / `failed` / `read` / `sent` / `opted_out` in your own handler. The catalog is the reference, not the unreachable side.

## See also

* [Event catalog (schema-level)](/guides/webhook-event-catalog) — the sibling design-time catalog this page pairs with
* [Inbound event catalog reference](/webhooks/inbound-event-catalog) — the index-style page this guide complements
* [Normalized inbound event envelope](/webhooks/normalized-inbound-envelope) — the `data.normalized` block anatomy
* [Build a durable webhook consumer](/guides/webhook-consumer) — the receiver this catalog feeds
