Skip to main content

The single message-status map

Three concept pages describe the same outbound-message state machine from different angles, and until now nothing said which one to open for a given question. This page is that index. Read it as a hub: resolve a status here, then jump straight to the page that owns the answer you actually need — without reading three pages to find the one rule you came for. The four owners: Two sibling lifecycle pages are deliberately separate: email delivery lifecycle runs the same machine on a different provider and adds two outcomes; delivery lifecycles other than outbound is the ingress/egress split. Neither replaces the outbound story above.

The glossary of state rows, with one owner each

Resolve a status you saw in a webhook payload or a GET /messages/:id response here. Each row points at its one owning page; the first owner is the primary, the second is the edge note. bounced and complained are the two email-only outcomes; both live on the email page. Everything else is the shared outbound machine.

The DAG in one table

Take event payloads and reconciles them against the platform rules. This is the transition table as a reader uses it — grouped by what dictates the next move, not alphabetically sorted. Cross-check it against the full table on the Message status transition rules page before you hard-code a merge. Everything from any row to deleted is also legal on operator request. Any transition not listed, or a backward move not named under Recoverable corrections, is ignored as a duplicate. The full per-from table with verbatim allowed sets is on the Message status transition rules page — treat that page as the merge contract and this table as the reader digest.

When the same status means different things per channel

Three channel-specific flips change what a status tells you; none of them change the status itself.
  • Meta DM (Instagram, Messenger) has no DLR. Meta never emits a delivery receipt. sent flips to submitted_no_receipt after 5 minutes, and on an opted-in recipient that sentinel is a functional delivery signal, not genuine ambiguity. Branch on metadata.no_dlr_channel to tell the Meta case from the SMPP case. Owner: Delivery lifecycle.
  • SMPP grace window. On SMS, MMS, voice, fax, and RCS, the receipt window is 30 minutes. A row stuck sent past that window is promoted submitted_no_receipt (intermediate, not terminal). Owner: Delivery lifecycle; the late-arrival expiry rule and the carrier-correction allowance live on Message status transition rules.
  • Email adds a bounce outcome. A receiving mail server can reject after acceptance, producing bounced — an email-only arm the shared lifecycle treats as one bullet and the email page treats as the structural story. Owner: Email delivery lifecycle.
SMPP encoding of this vocabulary — which stat/err token a bind receives for each canonical state — is its own surface on the Send-side DLR model page.

Worked reconciliation: apply the merge to a report table

Suppose a day’s export of your own status mirror shows these rows, sorted by message id then by arrival time. Apply the merge contract in order.
The three rules this walkthrough applied, restated: (1) only the recoverable correction transitions move a row backward; (2) no provider callback moves a row off deleted or cancelled; (3) dedupe first — a redelivered event must not re-apply a transition you already recorded. The full correctness proof and a minimal JavaScript merge implementation live on the Message status transition rules page.

See also