Queued to sent: the outbound status primer
Read this one page and you own the whole vocabulary — the five deeper pages linked under See also become optional detail, not required reading. When you need the full legal-transition/merge graph rather than the arc, that lives on the message-status transition rules (DAG) page, and this primer references it rather than re-deriving it.The canonical vocabulary, stated inline
An outbound message moves through a forward arc of wire states, and it can end in one of the outcome classes. The two groups are different kinds of states: the forward arc describes where the row sits in the pipeline; the outcome classes describe how the row closed. The forward arc (in order):pending— the API accepted your send request; the row is not yet on the delivery queue. This state exists between acceptance and queueing and is usually sub-second.queued— the row is on the delivery queue, waiting to be claimed by the dispatcher. A row parked withsend_atsits in the hold statescheduledinstead and joinsqueuedwhen its fire time arrives.sending— the dispatcher claimed the batch and is handing it to the routed provider. Another sub-second state in a healthy pipeline; a persistentsendingcluster means the provider endpoint is stalling.sent— the provider acknowledged the payload on the wire. This is a wire state, not an outcome: on a receipt-capable channel it means “the carrier may still report a delivery receipt”; on a no-DLR channel it means “accepted, and no receipt will ever come.”delivered/read— the terminal success outcomes, resolved from the provider’s delivery receipt (DLR).readimplies delivered and outranks it.failed/rejected/undelivered— the terminal failure outcomes.failedis a send-time or classified failure;rejectedis a carrier policy refusal;undeliveredis “the carrier tried and the handset could not take it” — or, on a receipt-capable channel whose receipt never arrived within its grace window, “sent, never confirmed.” A genuinely-no-DLR row ages there; it does not masquerade as delivered.expired— a receipt arrived past the late-arrival window, or a scheduled row outlived its validity window before fire time. Terminal “too late to count,” never a fabricated outcome.submitted_no_receipt— the provisional sentinel: accepted by the provider, receipt not (or never will be) expected. It is deliberately not terminal — a later genuine receipt overwrites it. A section below is devoted to it.
cancelled— you called the cancel endpoint while the row was still unsent.expired, above.deleted— operator redaction; the terminal floor every status can exit to.test_sent— sandbox/test sends resolved before any provider dispatch.
accepted (a synonym for admission used by some providers) and unknown (a defensive placeholder for an unmapped provider status string — it never overwrites a real state and any real state overwrites it).
The four actors that move a row
Every status change is written by exactly one of four logical actors. When a row is stuck, the actor is what you debug; the status is what you read.- The send pipeline (API → queue → dispatcher). Owns
pending → queued → sending → sent, and writes the provider id, the provider’s message reference, and the carrier hop onto the row at handoff. A pre-send gate failure (validation, consent, quota) closes the row asfailedhere, before any provider touch. - The DLR pipeline (incoming provider webhooks). Owns every real receipt:
sent → delivered / read, and the carrier rejectionssent → failed / rejected / undelivered. It also enforces the legal-transition/merge rules — precedence weight, floors, the carrier-correction allowances — so out-of-order receipts resolve to the same answer the DAG page publishes. - The no-DLR scheduler (a background safety net). Sweeps rows stuck in
sentpast their channel’s grace window and closes them honestly: infersdelivered/readfrom stamped receipt markers on the row, promotes no-DLR-channel rows tosubmitted_no_receipt, ages genuinely-silent reportable channels toundelivered, and heals mislabeled sentinels back to their true receipt. Everything the scheduler writes is provisional — a later genuine DLR still wins. - You, the operator (API calls). Owns
cancelagainst a still-pending row (which lands it oncancelled, a floor no provider event can reopen) anddeletefor redaction (deleted— the absolute floor every status can exit to and nothing exits from).
Wire states vs terminal states — and the exit-edge table
A wire state (pending, queued, sending, sent, and accepted where it appears) describes progress, not outcome. A terminal state describes outcome. The sixth message-log column (sent / delivered / failed / pending, depending on the surface) is a projection, so two rows in the same column can sit in different dispute states — “Sent” can be a healthy SMS awaiting its DLR, or a Meta DM message that will never get one.
An exit edge is a legal transition out of a state. This table is the arc’s own edges; the full legal-transition graph (weights, floors, carrier corrections) is on the message-status DAG page — it is referenced here, not re-derived.
The forward arc plus the outcome classes above is the entire vocabulary. Everything else in your status stream is one of the alias states or a provider-specific string the platform maps into this list.
No-DLR channels and their grace windows
Some channels never emit a delivery receipt at all. Meta DM channels (Instagram, Messenger) are the canonical case: the Send API acknowledges submission and guarantees delivery while the recipient stays opted in, but it provides no carrier-style DLR — sosent is the best answer the provider will ever give.
The send path marks those rows with a channel-level “no receipt expected” flag, and the no-DLR scheduler resolves them after a short grace — 5 minutes for Instagram/Messenger — into the submitted_no_receipt sentinel. Email gets a longer, 60-minute window to absorb the provider webhook’s retry envelope before the same sentinel applies.
Receipt-capable channels behave differently by design. SMS/MMS/voice/fax/RCS rows are supposed to be confirmed by a carrier DLR; when none arrives within their 30-minute grace, the scheduler ages them to the honest terminal undelivered (“sent, never confirmed”) rather than crediting them toward delivered. Viber/WhatsApp rows get a 24-hour window before the same undelivered verdict.
So the single scheduled closing actor reads each channel’s grace off the row itself: 5 minutes for Meta DM, 60 minutes for email, 30 minutes for the SMPP-backed family, 24 hours for viber/whatsapp. The messages log shows these four as the same column, but the rates landing in your analytics differ on purpose — the sentinel counts differently from an aged failure. Split your own numbers by that grace class and the two sentences of reconciliation (“route never reports receipts” vs “route aged its window”) resolve themselves.
submitted_no_receipt, precisely
submitted_no_receipt means: the provider accepted the send; no receipt ever said what happened next. It is a statement about evidence, not about the handset.
Three things follow from that definition:
- It is provisional by design. The status deliberately reports as non-terminal (
is_terminal: false) on your webhook stream, and the legal-transition graph lets a later genuinedelivered,read, or failure DLR overwrite it. Freeze your integration on the first terminal-looking event and you misrecord exactly these rows. - It is KPI-counted as delivered in the platform’s delivery-rate math, because on the channels that produce it (Meta DM especially) acceptance is the strongest guarantee available. That is a reporting decision, not an assertion about the handset.
- It is not a failure. Your own webhooks receive the sentinel as a status-classification event (the platform maps it onto the failure-event type for subscription parity), so treat it in your merge logic as an intermediate state, and in your analytics as a delivered-equivalent — and never as a reason to retry.
undelivered, the scheduler heals a mislabeled sentinel back to a real receipt whenever the receipt markers exist on the row, and any genuine receipt that arrives late still moves the row forward.
A worked walk: POST to submitted_no_receipt
Walk one Meta Messenger send end to end and watch each actor write its slice:
- POST
/api/v1/messages/messengeraccepts the send; the actor-1 send pipeline writes the row aspending, thenqueued— amessage.createdevent fans out to your webhooks. - The dispatcher claims it:
queued → sendingas the pipeline resolves the route. - Meta acknowledges on the wire:
sending → sent, with the provider’s message reference stamped on the row. Your subscribers see amessage.sentevent marked intermediate. - Meta DM emits no DLR, so the DLR pipeline never touches the row. After the 5-minute grace on
sent, the no-DLR scheduler promotes it:sent → submitted_no_receipt, and fans out the terminal-style event withis_terminal: false. - If a read webhook arrives later, the DLR pipeline writes
read(implying delivered), weighting higher than the sentinel — the row has converged. - If a read webhook never arrives, the row rests at the sentinel: accepted, resolved, honestly labelled “no receipt expected.”
delivered”; when no receipt arrives within 30 minutes, step 4 becomes the scheduler’s aging to undelivered (terminal, with refund handling where a charge applied); a genuine late DELIVRD still flips the row to delivered at any point. Swap the request for a scheduled send and the row starts in scheduled and joins queued only at fire time.
For one specific message, the same arc is assembled on read as an ordered timeline — the accept, route, sent, terminal, and webhook fan-out hops a GET /api/v1/messages/:id/trace returns — so “where did this row actually go” never needs a carrier ticket.
See also
Resolve the vocabulary above and you rarely need these; they stay authoritative for the slices they own:- Delivery lifecycle — the fuller per-status semantics and which owner writes each.
- Message status map — the row-per-status owner index across the cluster.
- Message status transition rules (DAG) — the full legal-transition graph, precedence weights, floors, and merge contract (referenced above, never re-derived).
- Send-at scheduled sends — the
scheduledpre-arc hold and its three exits. - Message route trace — the per-message timeline endpoint that reconstructs the arc for one row.