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

# The per-message route-trace timeline

> How a route trace assembles one ordered timeline per message — accepted through routing, provider submission, the terminal receipt, and every webhook fan-out hop — from rows the platform already persists, and how to read one for the delivered-but-never-received symptom.

# The per-message route-trace timeline

A flat status on the message row answers *what* a message is. It does not answer *where it is* or *how long it spent getting there* — and those are the questions an operator asks the moment a recipient says "I never got it." The route trace is the per-message answer: one ordered timeline of every routing, delivery, and webhook fan-out event the platform knows about for a single message ID, fetched in one API call.

The [Delivery lifecycle](/concepts/delivery-lifecycle) page defines the state machine a row moves through, and the [DLR model: two planes](/concepts/dlr-model-two-planes) page defines which plane a receipt belongs to. This page defines the diagnostic surface you open when an individual row's story needs to be reconstructed end to end.

## Section 1 — What a route trace is

A route trace is a per-message, ordered event timeline assembled **on read** from rows the platform already persists. When you call `GET /api/v1/messages/:id/trace`, Orbit reads two things it stores for every message anyway — the message row itself, and the per-message webhook delivery log (one row per delivery attempt to any of your webhook endpoints) — and returns them as one sorted event list plus an aggregate latency summary. Nothing is written on this path, and no new storage exists for it: the endpoint is a pure read-path assembly over the same rows the Delivery Log renders.

Two constraints follow from that design, and they shape how you should use the endpoint:

* **A trace reflects history, not a live carrier query.** Calling it re-reads the persisted rows; it never re-interrogates the provider or the destination carrier. If a carrier receipt has not arrived yet, the trace shows the state at the last persisted transition — call it again in a minute and it changes only if a new receipt actually landed in the meantime.
* **Do not confuse the trace with per-status polling.** The trace assembles from the same persisted row state that `GET /api/v1/messages/:id` already returns, so polling it buys you nothing over polling the row. The live event path is your webhook subscription; the trace is the diagnostic you open when a specific row needs explaining.

## Section 2 — The canonical event vocabulary

Every event in a trace is one of these names. Each carries `at` (the ISO-8601 timestamp the event was observed, `null` for stage markers with no timestamp), a `hop` label naming who performed it (`platform`, `provider:<name>`, `carrier:<MCCMNC>`, or `webhook:<endpoint-id>`), and a `detail` payload.

| Event                                             | What it proves                                                                                                                                                                                                                                                                            | Where the timestamp comes from                                                              |
| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `accepted`                                        | Orbit ingested the send. Always present — every message row carries a creation timestamp.                                                                                                                                                                                                 | The message row's creation time.                                                            |
| `validation`                                      | The pre-send gate stack (recipient opt-out, quota, consent, sender-ownership, compliance) passed. Outbound only; omitted when the row failed **at** validation so the failure marker is not double-marked.                                                                                | Same acceptance time — the gates run in-line with ingestion.                                |
| `queued`                                          | The send was accepted onto the delivery queue. Emitted for every outbound row that cleared validation, so the lifecycle reads validation → queueing → routing → submission rather than jumping stages.                                                                                    | Same acceptance time — there is no dedicated queue timestamp on the row.                    |
| `routed`                                          | A provider plus destination carrier was resolved. The `hop` names them: `provider:<name>:carrier:<MCCMNC>`.                                                                                                                                                                               | Same acceptance time — routing resolves on the same ingestion write.                        |
| `sent`                                            | The provider acknowledged the payload. `detail.external_id` carries the provider's wire-level reference; `detail.latency_ms` measures accept-to-submit lag. Some channels never emit one — the row flips straight to a terminal receipt and the stage still renders (untimed) as reached. | The row's provider-acknowledgement time.                                                    |
| `delivered`                                       | Carrier-confirmed receipt — the handset acknowledged. The terminal success hop.                                                                                                                                                                                                           | The row's delivery time.                                                                    |
| `read`                                            | The recipient opened the message. Implies delivered; on absorbed traffic a later read is recorded but produces no second terminal receipt.                                                                                                                                                | The row's read time (falling back to the delivery time).                                    |
| `failed` / `undelivered` / `rejected` / `expired` | A terminal failure. `detail` carries `error_code` and `error_message` as the carrier returned them.                                                                                                                                                                                       | The row's failure time (falling back to the row's last update time on legacy rows).         |
| `submitted_no_receipt`                            | The per-channel grace window closed with no receipt. On no-DLR channels (Meta DM) it is the terminal line by policy; on SMPP channels a late receipt can still supersede it.                                                                                                              | The row's last update time — the no-receipt scheduler stamps that when it flips the status. |
| `webhook_fanout`                                  | One delivery attempt to one of your webhook endpoints. `detail` carries `endpoint_id`, `event_type`, `attempts`, `http_code`, `latency_ms`, and `next_retry_at`.                                                                                                                          | The webhook delivery log row's time.                                                        |

On a **failure** the response also carries a `failed_stage` field on the message header — `validation` (a pre-send gate refused the send), `submission` (routing was resolved but the provider never accepted), or `carrier` (the provider accepted and the carrier reported the failure back) — so you read where it stopped from one field instead of decoding timestamps. Outbound failures additionally render the stages the message never reached as skeleton rows: `at: null` with `detail.stage_state: "skipped"`, so the whole expected lifecycle is visible and the stop point is unambiguous. Untimed-but-reached stages (a provider that emits one terminal DLR and never a `sent` acknowledgement) render the same way with `stage_state: "reached"` and no fabricated timestamp.

The `summary` block closes the "where did the time go" question in one round trip:

* `accept_to_sent_ms` — ingestion to provider acknowledgement.
* `sent_to_delivered_ms` — provider submission to carrier-confirmed delivery (the number you quote when a receipt is late).
* `accept_to_terminal_ms` — ingestion to the terminal state, whatever it was.

## Section 3 — Reading a trace for "delivered-but-never-received"

This is the canonical symptom the endpoint exists for: the recipient swears nothing arrived, and the old industry answer — raise a support ticket, wait for a carrier-side trace — took days. Read the trace bottom-up:

1. **If the terminal event is `delivered`:** the carrier reported handset receipt. The destination-side issue (handset filter, SMS firewall, anti-spam on the destination carrier) now has a name: take the `provider` and `mccmnc` from the `routed` event and the `external_id` from the `sent` event — those three fields are exactly what a carrier-side trace request asks for, and you now hand them over without a support round-trip.
2. **If the row holds at `sent` or `submitted_no_receipt`:** no carrier answered at all. The same `routed` + `sent` hop names the provider and MCC/MNC carrier to trace against; compare the destination's history (per the [sent-but-no-receipt troubleshooting page](/troubleshooting/submitted-no-receipt)) to tell a slow route from a broken one.
3. **If the row is a failure:** `failed_stage` tells you whose side to fix — a `validation` failure is your gate to adjust, a `submission` failure is the provider's accept side, a `carrier` failure came back from the destination network.

In all three cases the two fields that replaced the ticket loop are the provider name and the carrier code on the `routed` event — the hop the carrier-side trace request needs to name.

## Section 4 — The webhook fan-out section

The same endpoint proves the other half of a "nothing happened" report: whether Orbit notified **your** endpoints about the row. Every `webhook_fanout` event is one delivery to one subscriber endpoint, and its tells are direct:

* **Rising `attempts` with a non-2xx `http_code` and a queued `next_retry_at`** — your downstream subscriber is the stuck party. Fix the endpoint (timeouts, 5xx, TLS) and re-check; the receipt path to the carrier is fine.
* **A single 2xx fan-out with no carrier-side terminal event on the row** — the subscriber is healthy; the carrier leg is the one to escalate.
* **Latency outliers** — `latency_ms` per attempt distinguishes a slow subscriber from a failing one; a 200 with multi-second latency still points at your endpoint.

Fan-out hops interleave with lifecycle events at their true timestamps, so a trace reads as one timeline, not two.

## Section 5 — Access

`GET /api/v1/messages/:id/trace` is gated by the **same scope as the message-detail endpoint**: an API key with `messages:read` or `messages:write` works — read-only keys keep working, write-only keys work too. A message ID that does not exist returns **404**; a malformed ID returns **422** with an actionable error instead of an opaque schema rejection. Full request shape is in the [messaging endpoint reference](/api-reference/endpoints/messaging).

## Section 6 — Worked example

One stuck SMPP send, traced end to end. The row accepted validation, routed to a provider on a US carrier, was acknowledged in under a second, and then no receipt ever arrived — the `submitted_no_receipt` sentinel closed the grace window, and one subscriber endpoint has been failing with 503s since the first attempt:

```json theme={null}
{
  "message_id": "msg_9c1d2e3f4a5b67c8d9e0f1a2b3c4d5e6",
  "channel": "sms",
  "direction": "outbound",
  "provider": "smpp-upstream-1",
  "external_id": "smpp-upstream-1:77a9be",
  "mccmnc": "310260",
  "status": "submitted_no_receipt",
  "error_code": null,
  "error_message": null,
  "failed_stage": null,
  "events": [
    { "event": "accepted",  "at": "2026-08-26T09:14:02.411Z", "hop": "platform",   "detail": { "stage_state": "reached" } },
    { "event": "validation","at": "2026-08-26T09:14:02.411Z", "hop": "platform",   "detail": { "stage_state": "reached" } },
    { "event": "queued",    "at": "2026-08-26T09:14:02.411Z", "hop": "platform",   "detail": { "stage_state": "reached" } },
    { "event": "routed",    "at": "2026-08-26T09:14:02.411Z", "hop": "smpp-upstream-1:carrier:310260", "detail": { "stage_state": "reached" } },
    { "event": "sent",      "at": "2026-08-26T09:14:03.223Z", "hop": "provider:smpp-upstream-1", "detail": { "external_id": "smpp-upstream-1:77a9be", "latency_ms": 812, "stage_state": "reached" } },
    { "event": "submitted_no_receipt", "at": "2026-08-26T09:44:03.223Z", "hop": "provider:smpp-upstream-1:carrier:310260", "detail": { "status": "submitted_no_receipt", "stage_state": "failed" } },
    { "event": "webhook_fanout", "at": "2026-08-26T09:44:04.015Z", "hop": "webhook:whep_4f2a9c", "detail": { "event_type": "message.failed", "attempts": 3, "http_code": 503, "latency_ms": 4120, "next_retry_at": "2026-08-26T09:52:04.015Z", "stage_state": "reached" } }
  ],
  "summary": { "accept_to_sent_ms": 812, "sent_to_delivered_ms": null, "accept_to_terminal_ms": null }
}
```

Read it: the carrier leg is the stuck party (single acknowledge, no receipt), *and* one subscriber endpoint is stuck (three attempts, 503, retry queued). Two different owners, visible in one call.

The Meta no-DLR variant is shorter by policy. Meta's Send API never emits delivery receipts, so after the 5-minute no-DLR grace the trace terminates on `submitted_no_receipt` — and that is the terminal line, not a stuck row:

```json theme={null}
{
  "message_id": "msg_4b3a2c1d0e9f8a7b6c5d4e3f2a1b0c9d",
  "channel": "messenger",
  "direction": "outbound",
  "provider": "meta",
  "external_id": "wamid.HBfG9s3xL…",
  "mccmnc": null,
  "status": "submitted_no_receipt",
  "failed_stage": null,
  "events": [
    { "event": "accepted",  "at": "2026-08-26T14:02:10.901Z", "hop": "platform", "detail": { "stage_state": "reached" } },
    { "event": "routed",    "at": "2026-08-26T14:02:10.901Z", "hop": "meta",     "detail": { "stage_state": "reached" } },
    { "event": "submitted_no_receipt", "at": "2026-08-26T14:07:10.442Z", "hop": "provider:meta", "detail": { "status": "submitted_no_receipt", "stage_state": "failed" } }
  ],
  "summary": { "accept_to_sent_ms": null, "sent_to_delivered_ms": null, "accept_to_terminal_ms": null }
}
```

On an opted-in recipient, Meta guarantees delivery on accept, so this terminal line is a functional delivery signal — confirm the `sent`/`routed` hops exist, and treat the carrier as delivered. The same rule the [sent-but-no-receipt troubleshooting page](/troubleshooting/submitted-no-receipt) documents for Meta DM channels, confirmed by the trace rather than assumed.

## Section 7 — Cross-links

* [Delivery lifecycle](/concepts/delivery-lifecycle) — the state machine every event above belongs to.
* [The DLR model: two planes](/concepts/dlr-model-two-planes) — which plane each receipt crossed on.
* [Troubleshooting: message sent but no delivery receipt](/troubleshooting/submitted-no-receipt) — the intermediate-state decision tree the trace accelerates.
* [Troubleshooting: message undelivered or failed](/troubleshooting/message-undelivered-failed) — reading terminal failures, with the trace as the end-to-end view.
* [Messaging endpoint reference](/api-reference/endpoints/messaging) — the route's full request/response shape.
