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 page defines the state machine a row moves through, and the 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 callGET /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/:idalready 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 carriesat (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.
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:- 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 theproviderandmccmncfrom theroutedevent and theexternal_idfrom thesentevent — those three fields are exactly what a carrier-side trace request asks for, and you now hand them over without a support round-trip. - If the row holds at
sentorsubmitted_no_receipt: no carrier answered at all. The samerouted+senthop names the provider and MCC/MNC carrier to trace against; compare the destination’s history (per the sent-but-no-receipt troubleshooting page) to tell a slow route from a broken one. - If the row is a failure:
failed_stagetells you whose side to fix — avalidationfailure is your gate to adjust, asubmissionfailure is the provider’s accept side, acarrierfailure came back from the destination network.
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. Everywebhook_fanout event is one delivery to one subscriber endpoint, and its tells are direct:
- Rising
attemptswith a non-2xxhttp_codeand a queuednext_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_msper attempt distinguishes a slow subscriber from a failing one; a 200 with multi-second latency still points at your endpoint.
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.
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 — thesubmitted_no_receipt sentinel closed the grace window, and one subscriber endpoint has been failing with 503s since the first attempt:
submitted_no_receipt — and that is the terminal line, not a stuck row:
sent/routed hops exist, and treat the carrier as delivered. The same rule the sent-but-no-receipt troubleshooting page documents for Meta DM channels, confirmed by the trace rather than assumed.
Section 7 — Cross-links
- Delivery lifecycle — the state machine every event above belongs to.
- The DLR model: two planes — which plane each receipt crossed on.
- Troubleshooting: message sent but no delivery receipt — the intermediate-state decision tree the trace accelerates.
- Troubleshooting: message undelivered or failed — reading terminal failures, with the trace as the end-to-end view.
- Messaging endpoint reference — the route’s full request/response shape.