Skip to main content

The three success statuses: delivered vs submitted_no_receipt vs sent

Three outbound statuses can all describe a message that, in the reader’s mind, “went fine” — sent, submitted_no_receipt, and delivered — yet they encode three very different grades of success:
  • delivered — terminal, read-confirmed by the receiver (carrier DLR or read receipt).
  • submitted_no_receipt — an intermediate sentinel promoted by the no-DLR scheduler once the per-channel grace window closes with no receipt.
  • sent — the earliest wire acknowledgment; the message is in flight.
The delivery lifecycle page names all three, the status map ranks them on the transition DAG, and the DLR two-plane model page splits the carrier plane from the send-side plane. None of them collected the distinction per group: this page does. Read it before you wire a webhook handler, a KPI, or a reconciliation table that treats “success” as a single boolean.

The three buckets — terminal vs wire-intermediate on a promotion boundary

Every status you can read off an outbound row sits on one side of a promotion boundary: a terminal status the receiver (carrier or recipient) asserted, or a wire-intermediate status that names how far the submission travelled but not the outcome. The row is stamped each transition by the single writer messages.messages row, and reporting distills the three into two canonical ladders: numeric delivery rate and event state-class — see Failure modes for why reducing them blindly poisons both. Recasting the three buckets with the reader’s mental model:
  • Terminal success says “a receiver asserted the outcome.”
  • submitted_no_receipt says “the scheduler bounded the wait; the outcome is still an open variable.”
  • sent says “the wire ACK came back; the receiver has not spoken yet.”

The promotion rules — the no-DLR safety net, the per-channel grace windows, and the partial index

A background scheduler (the no-DLR transition net in the webhook worker) promotes sent rows through a two-step sweep per tenant every 5-minute tick: a bounded SELECT ... WHERE status = 'sent' AND <channel gate> AND sent_at < NOW() - <grace> (capped, FOR UPDATE SKIP LOCKED) followed by a second UPDATE ... WHERE id IN (...) RETURNING ... that flips the row. The relevant gates: The two-select promotion sweep is bounded by a partial index, idx_messages_no_dlr_channel(nodlr_channel) WHERE no_dlr_channel = TRUE, which keys only the TRUE rows (currently Meta DM traffic) so the sweep cost stays flat regardless of overall message volume. Every row entering the messages table carries the flag: the sole canonical message writer (MessagesRepository.create) derives no_dlr_channel = (channel IN ('instagram','messenger')) at the insert seam and binds it explicitly, so a row either joins the index cleanly or provably stays out of it. A row never runs from nothing: the sent → <grace> predicate reads sent_at (not created_at), so a retry that lands late restarts its window fairly. Per promotion, the random receiver of dashboard parity hooks (SSE, webhooks, cache-bust, fallback escalation) fans out exactly like a real DLR flip would. See the Scheduler section of the cross-check note below — the schedule is part of the longevity contract, not an implementation detail you timing-tune against.

Why submitted_no_receipt is a sibling-but-skipped status on every consumer path

Five waypoints read messages.status without advancing it, and each of them deliberately treats the sentinel differently:
  1. The canonical writer. The single writer that creates or merges a messages row at send time derives and binds no_dlr_channel, but never writes submitted_no_receipt; promotion is the scheduler’s job.
  2. The no-DLR scheduler. The five promotion UPDATEs transition sent → <sentinel or undelivered> per channel grace, and the reconciliation UPDATE heals a sentinel row back to a genuine receipt.
  3. The DLR receiver path. A genuine late receipt flips the sentinel back to delivered/read (weight 4/5 beats 2.5) — a forward-only correction, never a regression.
  4. The data-integrity probe. status='sent' AND no_dlr_channel=false AND sent_at < NOW()-25h rows flag the gap class; Meta-DM-flagged rows are excluded so a healthy safety net never ring-fences a false positive.
  5. The unified-inbox/receipt planes. The DLR chokepoint and the scheduler’s parity fan-out emit message.receipt/message.status_changed frames so the inbox thread and dashboard lists stop freezing at “Sent” or “Submitted (no receipt) once the row converges.
Across those waypoints, four anchors must never filter the sentinel: webhook events (the sentinel arrives on message.failed because no dedicated event exists — branch on data.status), route trace (GET /api/v1/messages/:id/trace renders the sentinel as its own event with stage_state intact), budgets (the wallet debit stays where the send path charged it except the scheduler’s explicit undelivered refund; the sentinel itself is not a refund trigger), and fallback decisions (the cross-channel cascade trigger relies on the scheduler’s aged-to- undelivered flip, not on the sent/ACK origin, which must not be skipped). Metadata gate — no_dlr_channel: true (Meta DM) means the sentinel is a functional delivery (Meta guarantees accept-delivery on opted-in recipients); false means ordinary honest ambiguity.

Failure modes a reader must not confuse

Two failure modes share the postcard-status sent surface and only one of them is a bug:
  • Low-DLR — a report-capable channel whose carrier should return receipts but does not (a non-cooperating route, a dropped Jasmin deliver_sm PDU, an endpoint drift on email). The scheduler ages the row to undelivered and, where a pre-send charge is recorded, frankly reverses it. Track it in your failure KPIs; it is a new-shipment ambiguity class.
  • No-DLR — a channel (currently Instagram / Messenger, both flagged no_dlr_channel=true) where by contract no receipt ever arrives. The scheduler promotes the row to submitted_no_receipt; reporting counts it on the delivered numerator for exactly those two channels (channel-aware status IN (MESSAGE_DELIVERED_STATUSES_UNCONDITIONAL) OR (status = 'submitted_no_receipt' AND channel IN (META_DM_CHANNELS))). Do not confuse the second class with the first: a no-DLR channel is not broken, and a low-DLR channel will not fix itself by being promoted to the sentinel.
Guard for both: annotate any “no DLR” tax with channel, and only then interpret the sentinel. The data-integrity probe’s no_dlr_channel=false predicate and the unified META_DM_CHANNELS set exist to keep the two classes apart on the same field.