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 billing gate to termination decision 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. Pricing and the billing gate — a send that cannot pay never reaches a provider

Before any provider handoff, the send is priced and then walked through the ordered billing gate. The quote comes first: the destination’s per-unit rate resolves through overrides and markups as Pricing and rate resolution describes, and every gate below checks the wallet against that priced row, never a $0 placeholder. Then the chain runs in its fixed order — the same four stages the outbound billing gate chain narrates in full:
  1. Pre-flight balance check — an exhausted wallet rejects with INSUFFICIENT_BALANCE (402) before the send reaches the provider.
  2. Pause/block flags gate — SENDING_PAUSED (soft) or SENDING_BLOCKED (hard), both 402, with the free-channel exemption for a soft pause.
  3. Per-channel fund path — voice escrows a hold of up to two minutes at dial; SMS debits per billable segment at send time.
  4. Atomic check-and-decrement — the wallet never overdraws, and the idempotency key collapses retries into one ledger row.
A send that clears one gate proceeds only to the next, so the refusal code names how far the chain got. For your integration the stage-1 shape is the one to build against: a pre-flight rejection answers the POST with 402 and no row exists — no message.created event, nothing to poll — so treat it as terminal, top up, and resend. The soft-fail shape is different: an inbound auto-reply that runs dry records the reply as failed with an insufficient-balance cause while the inbound pipeline keeps going. The zero-balance per-surface table on the billing gate chain is the full matrix; this page only places the gate in the walk.

5. The termination decision — the send may leave the channel it arrived on

Sender resolution picks who sends; the termination decision picks which channel carries the send. Between resolution and provider handoff sits the unified termination decision: every production send origin funnels through one decision point that runs before every provider attempt, and your termination rules — tenant-owned configuration — can absorb the message and deliver it on another channel, for example an SMS submit redirected onto a WhatsApp template. The rules vocabulary of deliver vs absorb hops, the shadow-to-enforce lifecycle, and the canonical failure modes live on Unified termination routing; this stage places the decision in the walk. The walk is evaluate → claim → deliver. In shadow mode the rule is evaluated and the would-be channel is recorded on the intent row while the send proceeds on its original path; flip a rule to enforce and the terminator claims the intent and performs exactly one egress on the chosen channel. An absorbed send still bills exactly once: the preflight debits the charge key charge:msg:<messageId> before the routing decision runs, the claim collapses the second send, and a second deduct on the same key resolves to a no-op — the one-billed-egress guarantee. And because rules name a channel, never a provider or trunk, a rule can redirect the hop but can never take the send outside the platform’s wholesale egress. Where termination decides the first hop, Least-cost routing ranks the carriers one hop downstream.

6. 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 timeline — 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.

Who carries the send: the provider chain inside a channel

Stage 6 names the two planes a send can exit on; this section names the ordering that picks the one connection inside the channel that actually carries it. Every send resolves to a single provider connection, ordered by four inputs:
  1. Your own connection for the channel, first. If your organization has connected its own asset — a WhatsApp WABA, a Viber Business account, a Telegram bot — that connection is placed at the head of the chain and tried before anything the platform registered. This is tenant-owned configuration; nothing about it is inferred.
  2. Sender-based preference. The resolved sender can dictate the connector: a number purchased on Telnyx exits via the Telnyx connection, and an alphanumeric sender ID rides the SMPP bind that registered it.
  3. Destination prefix routing. A longest-prefix match on the destination number against the BYO carrier connectors you configured, per BYO carrier lifecycle.
  4. Registration order. Whatever remains, in the order the platform registered it.
The chain is tried until one provider accepts the send. The connection that carried it is recorded on the row and in its trace, and when the chain has to skip a first choice to succeed, your organization is notified of the failover; the Route-trace timeline is what you read after the fact — which connection carried the send and which it skipped. Failover inside the chain is visible, never silent. The identity-bound rule: when a send must fail rather than fail over. Ordering the providers is only half of the machinery; the other half decides whether a failed provider may be skipped at all. On channels where the sender identity is the credential — a WhatsApp WABA, a Telegram bot — your connection is not merely first in the chain, it is the only provider attempted. A send it cannot carry surfaces to you as 422 CHANNEL_NOT_CONFIGURED or a provider error, and is never re-sent from a different identity. That is the general form of the rule each channel page states for its own surface: WhatsApp has no fallback that routes a tenant send onto the platform WABA, and Telegram dispatches under your own bot or the demo bot, never under an identity that is not yours. SMS is the contrast case that makes the rule legible: your number is yours whichever connector carries it, so connector failover under an unchanged sender is safe and is allowed — and precisely for that reason a send pinned to a provider-owned number locks to that connector rather than letting a different one answer with its own default sender. Viber is the one deliberate middle case: your Business account is tried first, and a send that it rejects — the recipient is not a Viber user, for example — degrades to the platform’s one-way Viber route instead of failing, a configured degradation where both tiers remain Viber egress rather than a substitution of identity. The test-mode exception. Platform demo assets exist for evaluation only. The shared Telegram demo bot serves test-mode tenants and never carries production traffic: a production send resolves to the tenant’s own connected bot through input 1, and a production tenant without one does not silently borrow the demo identity. The per-channel ladder of test assets is on the channel test-account model. Where this stage ends and upstream selection begins. The chain above picks a provider inside one channel; it neither picks the channel nor ranks the egress route the provider hands the message to downstream: Troubleshooting: three different “wrong carrier” shapes.
  • My send failed instead of failing over. The identity-bound rule did its job. The channel is identity-bound, your own connection was the only provider attempted, and it refused. Fix the connection — register the number, reconnect the bot — and resend; there is no platform-side fallback to wait on.
  • My send left on an unexpected carrier. The provider was yours; what moved was the upstream route. Least-cost routing or route quality picked a different egress than you assumed; read the trace against Least-cost routing.
  • My sender changed. Not this stage. Sender identity is fixed before the provider chain runs, on Sender resolution; a substituted sender means another source won the resolution walk, or an unlocked connector answered with its default.

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

8. 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, and one 402

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. And the refused case — the same POST against an exhausted wallet:
Stage 1 of the billing gate — the pre-flight balance check — ate this send before enqueue, so no row exists and no webhook event fires; §4’s terminal-402 rule applies: top up, then resend. Had the same wallet run dry on an inbound auto-reply instead, the reply would have failed soft at the per-channel fund path with the row recorded as failed — same wallet state, two refusal shapes, and the gate stage names which one you see.

See also