Skip to main content

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

StatusMeaningWho writes it
pendingRow created, not yet enqueued for send.Send-path persistence layer.
queuedEnqueued onto BullMQ, awaiting worker pickup.messages.service.ts#sendMessage.
scheduledPersisted with scheduled_at in the future; not yet eligible to send.Send-path; promoted by scheduler at fire time.
sendingWorker has dequeued the row and is currently dispatching to the provider.Webhook-worker / send-path.
acceptedProvider responded with a synchronous 2xx but no wire-level ACK yet.Provider-specific paths (e.g. Meta Cloud API).
sentProvider 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

StatusMeaningWho writes it
deliveredCarrier-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.
readWhatsApp / RCS / email read-receipt — implies the row was delivered first.DLR handlers when the carrier emits a read event.
submitted_no_receiptTwo 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:
  1. 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.”
  2. 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

StatusMeaning
failedProvider returned a terminal error on send OR the DLR reported MESSAGE_STATE=UNDELIVERABLE.
rejectedProvider rejected the request synchronously (invalid recipient, throttled, policy violation).
undeliveredCarrier confirmed via DLR that the message did not reach the recipient (e.g. unreachable).
bouncedEmail-only — receiving mail server returned a bounce.
expiredDLR arrived after DEVOTEL_DLR_LATE_ARRIVAL_MAX_AGE_DAYS — treated as functionally expired.
unknownDefensive 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.
StatusWebhook event
sentmessage.sent
deliveredmessage.delivered
readmessage.read
failedmessage.failed
rejectedmessage.failed
undeliveredmessage.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"
  }
}