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

# Delivery lifecycle: queued to delivered, and the states in between

> How an outbound message moves from queued through sent to delivered (or a failure outcome), who advances each transition, how delivery receipts (DLRs) drive it, and the channel-specific exceptions you need before you handle your first webhook.

# Delivery lifecycle

Every outbound message you send through Orbit carries a `status` field that advances as the message moves from your API call toward the recipient's handset — or toward a failure outcome. This page explains that state machine at the concept level: what the states mean, what drives each transition, and where the carrier-specific edges are. Read it before you subscribe to your first webhook or branch your integration on message outcomes.

Per-status semantics, the full transition table, and the webhook-event map live in the [message status lifecycle reference](/api-reference/messages-status-lifecycle); the response schema for reading a message's current state lives in the [Messaging API reference](/api-reference/endpoints/messaging). This page ties the two together at a higher level.

## The happy path

A message that succeeds end-to-end passes through:

`pending → queued → sending → sent → delivered → read`

Each transition is advanced by a different actor — no single party sees the whole arc:

| Transition         | Meaning                                                                   | What advances it                                                                                                                                                  |
| ------------------ | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pending → queued` | The row exists and is placed on the send queue.                           | The send path, at `POST /messages/<channel>` acceptance. `pending` is a transient pre-queue state you will rarely observe.                                        |
| `queued → sending` | A worker has picked the message up and is dispatching it to the provider. | The send pipeline at dequeue.                                                                                                                                     |
| `sending → sent`   | The provider acknowledged the submission on the wire.                     | The send path, the moment the provider accepts (an SMPP submission ACK, a provider message id, a Meta hand-off id — whatever the channel's wire-level accept is). |
| `sent → delivered` | The carrier confirmed delivery to the recipient.                          | A delivery receipt (DLR) arriving back from the carrier, handled by the DLR pipeline.                                                                             |
| `delivered → read` | The recipient opened it.                                                  | A read-receipt DLR, on channels that emit them (WhatsApp, RCS, email). Implies the row was delivered first.                                                       |

Two actors sit beside this path and can move a row off it:

* **The no-DLR scheduler.** When a carrier never returns a delivery receipt, a scheduler promotes `sent` to `submitted_no_receipt` after a per-channel grace window — see [Carrier-confirmed vs. wire-intermediate](#carrier-confirmed-vs-wire-intermediate).
* **You (the operator).** Cancelling a still-unsent scheduled message moves it to `cancelled`, a terminal state no provider call and no DLR ever touches. Sandbox sends resolve to `test_sent`, a terminal state reached before any provider dispatch. Deleting a row moves any terminal state to `deleted`, after which nothing can touch it.

A message scheduled for the future parks in `scheduled` until its fire time, then joins the queue. It can only leave `scheduled` three ways: promoted to `queued` at fire time, `cancelled` by you, or `expired` if its validity window lapses before dispatch.

## Carrier-confirmed vs. wire-intermediate

The distinction that matters most for reporting and reconciliation is whether a state is a **carrier-confirmed outcome** or a **wire-intermediate sentinel**:

* `delivered` and `read` are carrier-confirmed. A real DLR arrived; the carrier itself asserted the outcome.
* `submitted_no_receipt` is wire-intermediate. It means "the submission was accepted, and no receipt came back inside the grace window." It is tagged intermediate (`state_class: "intermediate"`, `is_terminal: false`) on every webhook, because a genuine `delivered`, `read`, or failure DLR can still land afterward and overwrite it.

Treat `submitted_no_receipt` as *outcome unknown*, not as a delivery. Whether "unknown" leans positive or genuinely ambiguous depends on the channel — see [Per-channel caveats](#per-channel-caveats).

`expired` is the counterpart on the other side: a DLR arrived, but so late that the receipt window had already closed. The outcome is unknowable and the row is closed; `expired` is fanned out to subscribers as a `message.failed` event.

## Per-channel caveats

* **Meta DM channels (Instagram, Messenger).** Meta's Send API never emits delivery receipts. The send leaves Orbit as `sent`, is flagged `no_dlr_channel` on the message metadata, and flips to `submitted_no_receipt` after 5 minutes. For an opted-in recipient, Meta guarantees delivery on accept — so on these channels `submitted_no_receipt` behaves like a functional delivery signal, and the message metadata carries `no_dlr_channel: true` so you can tell that case apart.
* **SMPP-backed channels (SMS, MMS, voice, fax, RCS).** The grace window is 30 minutes. Here `submitted_no_receipt` is genuinely ambiguous: the handset may have received the message with no receipt reported, the carrier may never send receipts on that route, or the receipt may have been dropped in transit. Track this rate separately from your delivered rate — a rising `submitted_no_receipt` share on one destination points at a non-cooperating route or a broken receipt path, and is worth investigating either way.
* **Email.** Adds one failure outcome other channels lack: `bounced`, when the receiving mail server rejects the message. Bounced counts against your terminal-failure rate like `failed`, but it is a distinct status so you can separate recipient-side rejects from provider-side ones.
* **Operator cancel.** `cancelled` is reachable only by you — via `POST /messages/:id/cancel` on an unsent message. No carrier ever writes it, and as a consequence it fires **no webhook event**; nothing notifies a subscriber of a cancel. Poll `GET /messages/:id` if you expose cancel in your own UI and need to observe it.
* **Sandbox / test mode.** Test sends resolve to `test_sent` before any provider dispatch. They fire `message.sent` with `status: "test_sent"` and `metadata.test_mode: true`, so a subscriber must branch on `metadata.test_mode` to keep sandbox traffic out of production handling.

## What to branch on

Integrations should switch on machine-readable fields only, never on display labels:

* **`status`** — the message's current state. This is the primary branch point. Handle the full set: besides the happy-path and common failure states, do not forget `cancelled`, `test_sent`, `submitted_no_receipt`, and `bounced`.
* **`metadata.classified_error_code`** — present on terminal failures; the normalized, machine-readable failure category. Combine it with raw `error_code` / `error_message` on `message.failed` webhooks when you want the carrier's own wording.
* **`metadata.no_dlr_channel`** — present on Meta DM rows; tells you a `submitted_no_receipt` is the delivery-guaranteed Meta case, not the ambiguous SMPP case.
* **`state_class` / `is_terminal`** — on lifecycle webhooks. `is_terminal: true` (equivalently `state_class: "terminal"`) means the outcome is final; `submitted_no_receipt` reports `is_terminal: false` specifically so you do not close the book on it.

## Common pitfalls

1. **Treating `message.created` as acceptance.** `message.created` fires the moment the row is queued, before any provider call. A synchronous rejection (a 4xx on the send, or an immediate `rejected`/`failed`) still leaves that event delivered. Pair it with `message.sent` before concluding the message went out.
2. **Assuming `delivered` is unchangeable.** Carriers on some routes emit a delivery receipt, then a correction minutes later — some Indian and Brazilian carriers do this. Orbit honors it: the row can move `delivered → undelivered` or `delivered → failed`. If you mirror statuses into your own datastore, apply updates idempotently by message id rather than ignoring transitions for a message you already marked delivered.
3. **Waiting for a webhook on `cancelled`.** It will never come — cancel originates from your own API call, not a carrier callback, so the platform emits no event for it. The row simply sits at `cancelled` until you delete it.
4. **Dropping `submitted_no_receipt` into the failed bucket.** It arrives on the `message.failed` event type for transport reasons (there is no dedicated event), but its payload `status` is `submitted_no_receipt` with `is_terminal: false`. Branch on `data.status`, not the event type, and keep it out of your hard-failure metrics.
5. **Expecting one terminal event per message.** A message can emit `message.failed` with `status: "submitted_no_receipt"` and later `message.delivered`, when a slow carrier receipt finally lands inside the late-arrival window. Dedupe and reconcile by `message_id`, letting the later event supersede.

Once the state machine is clear, the per-status webhook payloads are in the [webhook events reference](/reference/webhook-events), and the endpoint-level status semantics are in the [message status lifecycle reference](/api-reference/messages-status-lifecycle).
