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

# Message send request lifecycle: one send from POST to provider

> The one end-to-end composition of an outbound send — request ingress through the guard pipeline, enqueue into the send queue, sender resolution, provider handoff to the terminating softswitch or your BYO carrier, the pending → queued → sending → sent writeback arc, and the no-DLR receipt arm that closes a silent route — with a worked SMS send you can replay.

# Message send request lifecycle

Every sibling page in the outbound-status cluster owns one slice of a send — [Delivery lifecycle](/concepts/delivery-lifecycle) owns the meanings, [Message-status transition rules](/concepts/message-status-dag) owns the merge contract, [Message-envelope model](/concepts/message-envelope-model) owns the wire shape, and [From queued to sent: the four-owner map](/concepts/queued-to-sent-the-four-owner-map) owns the page index. None of them answers the question you actually have after your first send: *what happens to my `POST /api/v1/messages` call, step by step?* This page composes the whole walk — API guard to provider handoff to receipt — and links each stage to its owning page instead of redefining it. Read this once to learn the walk; read the owning pages when a stage needs depth.

```mermaid theme={null}
sequenceDiagram
    participant You as Your service
    participant API as API (Fastify)
    participant Worker as Worker (webhook-worker)
    participant Provider as Provider (softswitch / BYO)
    participant Carrier as Carrier (DLR)

    You->>API: POST /api/v1/messages (+ Idempotency-Key)
    API-->>API: guard pipeline (rate limit, auth, policy, idempotency)
    API-->>You: 202 + message id (status queued)
    Note over Worker: a drain picks the row
    Worker->>API: dequeue → status sending
    Worker->>API: sender resolution (six-source chain)
    Worker->>Provider: submit payload
    Provider-->>Worker: accept → status sent
    Carrier-->>Provider: DELIVRD DLR
    Provider-->>API: status delivered
    Note over Worker: no DLR in the grace window →
    Worker->>API: promote sent → submitted_no_receipt
    Worker-->>You: message.* webhook events per transition
```

## 1. Request ingress — the guard stack

Your outbound POST passes through the ordered per-request guards that every Orbit API call traverses: retired-host gate, IP denylist, load-shed, rate-limit buckets, policy scan, auth, and the idempotency guard that replays a stored response for a re-sent `Idempotency-Key` header. The full ordered table — which guard fires first, the status code and error envelope each returns when it fires — is on [API request guard pipeline](/concepts/api-request-guard-pipeline); the replay contract the idempotency guard enforces is on [Idempotency and safe retries](/concepts/idempotency-and-safe-retries). Failing a guard here means nothing was enqueued and no row exists, so a 422/429/401 answer is your signal the send never entered the pipeline.

## 2. Enqueue — the row lands in the send queue

On acceptance the row is INSERTed with `status: "queued"` and the response returns **202** with the message id; from that moment the row is visible `GET /api/v1/messages/:id`. The durable enqueue — the queue the API produces to and the worker consumes from instead of answering inline — follows the producer/consumer split on [Async processing model](/concepts/async-processing-model), and the queue's drain is owned by the worker fleet you read about on [Scheduler fleet model](/concepts/scheduler-fleet-model). Do not poll the row to advance it; the pipeline advances itself, and polling a queued row tells you nothing the webhook event stream would not.

## 3. Sender resolution — the six-source chain picks the sender

Before the dispatching worker can hand the payload to a provider it must resolve *which sender* the message goes out from. For the unified `POST /api/v1/messages` (channel: `"sms"`) or the native `POST /api/v1/messages/sms` shape, resolution walks the six source types in order — an inline `sender`, a sender pool, a messaging service, a routable number, a BYO carrier binding, and the tenant default. The authoritative walk — every pool strategy with its fail-safes, per-country `country_sender_pools` overrides, health-tier swaps, and the fallback chain — is on [Sender resolution](/concepts/sender-resolution); the inbound side of the same construct lives on [Sender and routing](/concepts/sender-and-routing). When you supplied no sender hint, resolution picks the tenant-level default and the row records which source resolved it.

## 4. Provider handoff — the softswitch or your BYO carrier

Outbound exits the platform on **one of two planes**, both of them part of the tenant's own configuration. The default is the terminating softswitch; when you have brought your own carrier, the BYO-carrier binding from resolution hands the payload to your carrier connection instead. Which one fired is recorded on the row as the `provider` column plus the destination `mccmnc`, and the [BYO carrier lifecycle](/concepts/byo-carrier-lifecycle) page owns how the BYO binding is established. Whichever plane carries the send, the per-message audit of the handoff is the [Route-trace timeline](/concepts/message-route-trace) — `GET /api/v1/messages/:id/trace` assembles the timed events (accepted → routed → sent → terminal + webhook fan-out) from rows the platform already persists, so "which provider did this send take" is a one-call answer, not a forum question.

## 5. Status writeback — pending → queued → sending → sent

As the drain dispatches, the row advances through the forward arc — `pending → queued → sending → sent` — and each transition fires the matching `message.*` webhook event to every subscribed endpoint, so your subscribers track the send without polling. The per-status meaning of each transition and who writes it lives on [Delivery lifecycle](/concepts/delivery-lifecycle); the legal-transition graph, precedence weight, and the recoverable/floor rules your own merge logic must copy live on [Message-status transition rules](/concepts/message-status-dag); the `message.created` vs `message.sent` plane split lives on [Message-status map](/concepts/message-status-map). Your own receiver treats any status the row can reach through *those* pages, not just the ones this arc lists.

## 6. DLR arming — closing the grace window when the carrier says nothing

A wire-level `sent` is a submission acknowledgement, not a delivery claim; the terminal success hop is a carrier-confirmed `delivered`. When the provider accepted the payload but the carrier never reports back, the no-DLR sweep promotes `sent` to **`submitted_no_receipt`** after a per-channel grace window — 30 minutes on SMPP-backed channels (SMS, MMS, voice, fax, RCS), and the per-channel terminal otherwise — and tags that event `message.failed` with `state_class: "intermediate"`, `is_terminal: false`, so a later genuine `delivered`, `read`, or failure still overwrites it. The grace-window rules are on [Delivery lifecycle](/concepts/delivery-lifecycle); the wire-vs-receipt plane split the sentinel belongs to is on [DLR model: two planes](/concepts/dlr-model-two-planes); the read-upsert correction rules that accept the late receipt are on [Message-status transition rules](/concepts/message-status-dag). If you freeze your mirror on the first `submitted_no_receipt` event, you close the message on a sentinel that was never a terminal answer.

## Worked example — one SMS, five events

This is the exact sequence one well-formed SMS send produces. Your receiver must see the same statuses in the same order for the same message id, whatever the provider does on the wire:

```json theme={null}
POST /api/v1/messages
{"channel":"sms","to":"+14155552671","sender":"+14155550100","body":"Hi"}
→ 202 {"message_id":"msg_9f2"}
```

Then the row walks the pipeline:

```json theme={null}
{"type":"message.created","data":{"message_id":"msg_9f2","status":"queued"}}
{"type":"message.sent","data":{"message_id":"msg_9f2","status":"sent","state_class":"intermediate","is_terminal":false}}
{"type":"message.delivered","data":{"message_id":"msg_9f2","status":"delivered","state_class":"terminal","is_terminal":true}}
```

And the slow-route case — a carrier whose DLR never lands inside the grace window:

```json theme={null}
{"type":"message.created","data":{"message_id":"msg_9f2","status":"queued"}}
{"type":"message.sent","data":{"message_id":"msg_9f2","status":"sent","state_class":"intermediate","is_terminal":false}}
{"type":"message.failed","data":{"message_id":"msg_9f2","status":"submitted_no_receipt","state_class":"intermediate","is_terminal":false}}
{"type":"message.delivered","data":{"message_id":"msg_9f2","status":"delivered","state_class":"terminal","is_terminal":true}}
```

The second `delivered` supersedes `submitted_no_receipt` — exactly the precedence rule on the [transition DAG](/concepts/message-status-dag). Replay this pair against your merge logic before you take your integration to production.

## See also

* [Delivery lifecycle](/concepts/delivery-lifecycle) — what each status means and who writes it
* [Message-status transition rules](/concepts/message-status-dag) — the merge contract your receiver must implement
* [Message-status map](/concepts/message-status-map) — the four owners in one index
* [Route-trace timeline](/concepts/message-route-trace) — per-message diagnostic timeline
* [Message-envelope model](/concepts/message-envelope-model) — wire-shape semantics per channel
* [Sender resolution](/concepts/sender-resolution) — the full outbound chain
* [Async processing model](/concepts/async-processing-model) — queue producer/consumer split
* [API request guard pipeline](/concepts/api-request-guard-pipeline) — the ordered ingress guards
