Documentation Index
Fetch the complete documentation index at: https://orbit-docs.devotel.io/llms.txt
Use this file to discover all available pages before exploring further.
Message Status Lifecycle
Every outbound message in Orbit advances through a sequence of statuses
on its messages.status column. This page is the authoritative
reference for what each status means, who writes it, and how the row
transitions between them.
The canonical set of statuses lives in
packages/shared/src/constants.ts
under MESSAGE_STATUS and is mirrored in the Drizzle schema, every
provider’s getStatus() return type, and the FE
MessageStatusBadge.
In-flight statuses
| Status | Meaning | Who writes it |
|---|
pending | Row created, not yet enqueued for send. | Send-path persistence layer. |
queued | Enqueued onto BullMQ, awaiting worker pickup. | messages.service.ts#sendMessage. |
scheduled | Persisted with scheduled_at in the future; not yet eligible to send. | Send-path; promoted by scheduler at fire time. |
sending | Worker has dequeued the row and is currently dispatching to the provider. | Webhook-worker / send-path. |
accepted | Provider responded with a synchronous 2xx but no wire-level ACK yet. | Provider-specific paths (e.g. Meta Cloud API). |
sent | Provider returned a wire-level acknowledgement (SMPP submit_sm_resp ESME_ROK, Telnyx message_id, Meta wamid, etc.). | Send-path on synchronous ACK. |
In-flight rows do NOT count toward the delivery-rate denominator.
Terminal-positive statuses
| Status | Meaning | Who writes it |
|---|
delivered | Carrier-confirmed delivery via a wire-level DLR (SMPP deliver_sm MESSAGE_STATE=DELIVERED, Telnyx message.delivered webhook, Meta messages.statuses.status=delivered). | DLR handlers in apps/api/src/routes/webhooks/dlr*.ts. |
read | WhatsApp / RCS / email read-receipt — implies the row was delivered first. | DLR handlers when the carrier emits a read event. |
submitted_no_receipt | Two semantically different cases collapsed onto one sentinel — see below. | apps/webhook-worker/src/scheduler-no-dlr-transition.ts after the per-channel grace window elapses. |
submitted_no_receipt — what it actually means
History: This status was named delivered_no_dlr before
2026-05-14 (W1-023). Operators read the prior name as a success state
when (for SMPP channels) it actually means “submission ACK’d but no
delivery receipt received within the grace window — outcome unknown”.
The rename surfaces the honest semantics. A backfill migration
(208_messages_status_rename_delivered_no_dlr_to_submitted_no_receipt)
renames every existing row on every tenant schema.
Two distinct upstream conditions both promote the row to
submitted_no_receipt. Analytics and observability code that needs to
distinguish them should branch on channel:
-
Meta DM channels (
instagram, messenger). Meta’s Send API for
Instagram and Messenger never emits a wire-level delivery receipt.
The send path persists the row as sent with no_dlr_channel=TRUE,
and the no-DLR transition scheduler flips it to
submitted_no_receipt after 5 minutes. For this case Meta
guarantees delivery on accept when the recipient is still opted in
— analytics treat this as functionally delivered, and the FE
tooltip reads “Meta does not emit delivery receipts for Instagram
or Messenger. The message was accepted; delivery is guaranteed when
the recipient is still opted in.”
-
SMPP channels (
sms, mms, voice, fax, rcs). The SMSC /
carrier returned an ESME_ROK on submit_sm (submission accepted)
but no deliver_sm DLR landed within the 30-minute grace window.
This can happen because:
- The carrier never sent a DLR (some destination countries / routes
are non-cooperating).
- The DLR was dropped in transit (Jasmin restart, SMPP route flap,
misconfigured webhook).
- The handset received the SMS but the carrier never reported it.
For this case the outcome is genuinely unknown — the message
may have reached the handset, or it may have been silently dropped.
The FE tooltip reads:
“SMPP submission ACK’d but no delivery receipt received within
the 30-min grace window — outcome unknown. Don’t confuse with
delivered (carrier-confirmed).”
Operators investigating delivery quality on a route should treat
the submitted_no_receipt rate as a separate KPI from the
delivered rate. A high submitted_no_receipt rate signals
either a non-cooperating destination or a broken DLR path on our
side — both warrant investigation.
The MESSAGE_DELIVERED_STATUSES set in
packages/shared/src/constants.ts includes submitted_no_receipt
alongside delivered and read for backward compatibility with
existing analytics dashboards. Use that constant when computing a
delivered numerator; if you need to exclude the unknown-outcome SMPP
variant, filter on channel NOT IN ('sms','mms','voice','fax','rcs')
in addition.
Terminal-failure statuses
| Status | Meaning |
|---|
failed | Provider returned a terminal error on send OR the DLR reported MESSAGE_STATE=UNDELIVERABLE. |
rejected | Provider rejected the request synchronously (invalid recipient, throttled, policy violation). |
undelivered | Carrier confirmed via DLR that the message did not reach the recipient (e.g. unreachable). |
bounced | Email-only — receiving mail server returned a bounce. |
expired | DLR arrived after DEVOTEL_DLR_LATE_ARRIVAL_MAX_AGE_DAYS — treated as functionally expired. |
unknown | Defensive fallback only — should never persist for long; investigate if seen in dashboards. |
Transition rules
Valid status transitions are enforced by
packages/messaging/src/message-status-dag.ts.
Highlights:
- Linear happy path:
pending → queued → sending → sent → delivered → read.
sent → submitted_no_receipt is performed by the no-DLR transition
scheduler, NOT by a provider DLR.
- Carrier-correction (
delivered → undelivered or delivered → failed)
is allowed but logged loudly. Some Indian and Brazilian carriers emit
a DELIVERED then re-emit UNDELIV minutes later.
<terminal> → deleted lets operators retire stuck rows; once deleted
nothing else can touch the row.
Webhook events
Every status transition fires a corresponding webhook event (when the
tenant has subscribed). See Webhook event types.
| Status | Webhook event |
|---|
sent | message.sent |
delivered | message.delivered |
read | message.read |
failed | message.failed |
rejected | message.failed |
undelivered | message.failed |
submitted_no_receipt does NOT fire its own webhook event — by design,
because the upstream condition (timeout waiting for DLR) is not a
carrier event. Subscribe to the per-channel quality alerts if you need
to react programmatically to a tenant accumulating
submitted_no_receipt rows.
Reading the status from the API
Every GET /messages/:id response includes status plus
metadata.classified_error_code for terminal failures and
metadata.no_dlr_channel for Meta-DM rows. Consumers should never
parse the operator-facing label strings — always branch on the
machine-readable status field.
curl https://api.orbit.devotel.io/api/v1/messages/msg_01ABCDEF \
-H "X-API-Key: dv_live_sk_your_key_here"
{
"id": "msg_01ABCDEF",
"channel": "sms",
"status": "submitted_no_receipt",
"sent_at": "2026-05-14T10:23:04.501Z",
"metadata": {
"no_dlr_channel": false,
"submitted_at": "2026-05-14T10:23:04.501Z"
}
}