Skip to main content

Message send request lifecycle

Every sibling page in the outbound-status cluster owns one slice of a send — Delivery lifecycle owns the meanings, Message-status transition rules owns the merge contract, Message-envelope model owns the wire shape, and From 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.

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; the replay contract the idempotency guard enforces is on 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, and the queue’s drain is owned by the worker fleet you read about on 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; the inbound side of the same construct lives on 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 page owns how the BYO binding is established. Whichever plane carries the send, the per-message audit of the handoff is the Route-trace timelineGET /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; the legal-transition graph, precedence weight, and the recoverable/floor rules your own merge logic must copy live on Message-status transition rules; the message.created vs message.sent plane split lives on 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; the wire-vs-receipt plane split the sentinel belongs to is on DLR model: two planes; the read-upsert correction rules that accept the late receipt are on Message-status transition rules. 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:
Then the row walks the pipeline:
And the slow-route case — a carrier whose DLR never lands inside the grace window:
The second delivered supersedes submitted_no_receipt — exactly the precedence rule on the transition DAG. Replay this pair against your merge logic before you take your integration to production.

See also