> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Operational sentinels: the status values that aren't deliveries

> How Orbit marks simulated sends, blocked recipients, deleted rows, unrecognized provider callbacks, and complaints — the settled-but-non-outcome statuses that sit beside the delivery lifecycle and that analytics must not count as successes or failures.

# Operational sentinels

Not every `status` value on a message row describes a delivery. Besides the delivery outcomes (`delivered`, `failed`, `undelivered`, `rejected`, `bounced`) that the [delivery lifecycle](/concepts/delivery-lifecycle) walks through, a message can settle at a sentinel that means **something acted on the message before or instead of a carrier outcome**: a sandbox send, a suppression, a deletion, an unparseable provider callback, or a complaint-side effect.

These sentinels are settled — the row will not move on its own — but they are **not delivery outcomes**. Integrations that treat "settled" as "delivered or failed" silently mis-bucket them. This page enumerates the sentinel states, what produces each one, and how to keep them out of your delivery-rate math.

## The sentinel set

A sentinel is one of:

| Status       | Reading                                                                                                                                                      | Who writes it                                |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------- |
| `test_sent`  | The send short-circuited in sandbox/test mode; no provider was ever called.                                                                                  | The send pipeline, when test mode is active. |
| `suppressed` | The send was blocked by a suppression policy (recipient blocked/deduped); no provider was called.                                                            | The suppression path before dispatch.        |
| `deleted`    | An operator soft-deleted the row. A hard terminal floor — nothing can move it afterward.                                                                     | `DELETE /messages/:id`.                      |
| `complaint`  | The recipient filed a complaint (spam report) with their provider; the row settles as a failure-class outcome and the address joins the suppression list.    | The email complaint webhook path.            |
| `unknown`    | A provider callback arrived whose status string did not map to any known state. Defensive, forward-enrichable — a later valid callback can still correct it. | The delivery-receipt (DLR) mapping layer.    |

Four qualities separate them from delivery outcomes:

1. **No carrier asserted them.** A sentinel is written by Orbit itself (or by an operator), never by a delivery receipt from the network.
2. **They settle outside the happy path.** A `test_sent` or `suppressed` row never passed `sending`; a `deleted` row can start from any terminal state.
3. **They must not count as failures.** `test_sent`, `suppressed`, and `deleted` are neutral — the message simply never went out — while `complaint` and genuine failures do count against failure rates.
4. **They must not stay in-flight.** Every sentinel is a terminal signal for reconciliation purposes: a batch whose remaining rows are all sentinel-marked is drained, not stuck.

## How sandbox and test mode resolve to `test_sent`

When test mode is active, the send pipeline returns success without ever calling a provider, and the row is written directly as `test_sent` — a dedicated terminal status, deliberately **not** `sent` followed by a simulated `delivered`. Test mode activates three ways: a `dv_test_sk_*` sandbox API key, an organization flagged as sandbox, or the dashboard's **Test mode** toggle (which sends the `X-Test-Mode: true` header on API calls from the panel).

Two flavors resolve differently:

* **Shared send pipeline** (`POST /messages/<channel>`). The row is persisted with `status: "test_sent"`, metadata stamped `test_mode: true`, no provider call, no balance deduction. Subscribers receive `message.sent` with `status: "test_sent"` and `metadata.test_mode: true`; `message.delivered` is intentionally **not** emitted — emitting it would lie at the webhook layer. The dashboard shows an amber **Test sent** badge so operators can tell a simulated send from a real one.
* **WhatsApp direct-send routes** (send-flow, interactive, location/address request, catalog send, call initiate, marketing send). These bypass the shared pipeline and would otherwise hit Meta directly, so they short-circuit the same way: a synthesized `test_…` message id, the same response envelope, `status: "test_sent"`, and a tamper-evident audit record that the simulated send never reached the provider. The response is not persisted to the messages table — it is purely the wire shape clients receive — and credits are never touched.

Sandbox delivery-receipt simulation (the [magic-numbers](/sandbox/magic-numbers) trailing-digit convention) fires webhook events for scenarios like `delivered` or `failed`, but those receipts appear **only in webhook payloads** — the stored row keeps its honest `test_sent` status. Branch your webhook handling on `metadata.test_mode` to keep sandbox traffic out of production paths.

## How suppression and complaint statuses are produced

Suppression produces two distinct signals depending on the layer that fires, and both end with the send never reaching a provider.

**Message-suppression policy (`suppressed`).** When a tenant enables the duplicate-content policy on the [message-suppression](/guides/message-suppression) surface, the send pipeline claims a content hash for `<channel, recipient, body>` inside the configured window. A second send with identical content inside that window is dropped before it burns a frequency-cap slot: the pipeline returns success with `status: "skipped"`, `reason: "duplicate_content"`, refunds any quota it had pre-claimed, and — on channels whose conversation UI renders the attempt (Telegram today) — persists a `status='suppressed'` row so operators can see the blocked attempt in the timeline. Test-mode sends bypass the check so staging can replay identical bodies.

**Opt-out suppression (channel-level).** Channel opt-out lists also block before dispatch (behavior varies by channel — the API skips the send; some channels persist the `suppressed` audit row). Manage the lists on the [opt-outs reference](/api-reference/optouts).

**Complaint-side suppression.** When a recipient reports an email as spam, the provider fires a complaint webhook. The row settles as `failed` (complaints are terminal failures for accounting purposes — no refund on a complaint), and the recipient's address is written to the platform suppression list with reason `complaint`, which short-circuits every future send to that address. The same list carries `hard_bounce`, `manual` (ops-curated additions), and `soft_bounce_exhausted` (an address that soft-bounces on every send past the exhaustion threshold); all four are terminal reasons, and only the soft-bounce-exhausted class is expected to be lifted manually. A complaint therefore has two faces: the message row closes as `failed`, and the recipient becomes un-sendable until the suppression entry is removed.

## `unknown`: the defensive callback sentinel

`unknown` is the one sentinel that is genuinely transient. The DLR pipeline maps each provider callback onto the status table; when a callback's status string matches nothing (a new provider-side value, an unexpected payload shape), the row is parked at `unknown` rather than dropped on the floor. It is forward-enrichable by design: a later callback with a recognizable status can still advance the row to a concrete outcome, so `unknown` never blocks the lifecycle. Treat it as *mapping gap, not message outcome* — an unusual spike in `unknown` rows on one channel points at a provider contract change worth telling support about, not at a delivery problem.

## Why analytics must key these off the right column

Reporting splits the status vocabulary three ways, and a sentinel that lands in the wrong bucket corrupts the number next to it:

* **Delivery-rate denominators** use the curated terminal set (`delivered`, `read`, `submitted_no_receipt`, `failed`, `undelivered`, `rejected`, `bounced`). The operational sentinels are deliberately absent — `test_sent` traffic and deleted rows are neither successes nor failures, so including them would deflate the rate with messages that never attempted delivery.
* **Silent-route / SLO filters** must classify a route as settled the same way the reconciliation scheduler does — keyed off the *full* settled set (the DLR-terminal statuses plus the operator sentinels), not the narrower delivery-rate set. Filtering on the delivery-rate set alone mis-classified routes with test-mode sends or deleted rows as permanently "silent", firing false SLO-breach alerts.
* **Drained/in-progress detection** uses the in-flight predicate (`scheduled`, `queued`, `sending`, `sent`), not the terminal set — so a row that settled at `test_sent`, `suppressed`, `deleted`, or `unknown` correctly reads as done rather than sticking a batch at "running" forever.

When you build your own reporting on top of the [analytics endpoints](/api-reference/analytics) or your warehouse, mirror the same discipline: exclude the operational sentinels from delivery-rate numerators *and* denominators, and treat them as settled for reconciliation.

## Handling sentinels in your integration

* Branch on `data.status` in webhook payloads, and read `metadata.test_mode` before treating a `sent` event as production traffic.
* `complaint` arrives as `message.failed`; detect it via the metadata, not the status, if you need to reconcile it against a recipient-level block.
* `test_sent`, `suppressed`, and `deleted` are operator-visible rows, not deliveries — exclude them when you compute your own delivery rates from `GET /messages` exports, and expect **no** webhook event for a delete.
* If a row sits at `unknown`, wait for a follow-up callback before alerting — it resolves on its own when the provider sends a recognizable status.
* Poll `GET /messages/:id` for the current sentinel when you expose operator delete/cancel in your own UI; these flows emit no webhook.

## See also

* [Delivery lifecycle](/concepts/delivery-lifecycle) — the carrier-driven happy path and failure outcomes
* [Message status lifecycle reference](/api-reference/messages-status-lifecycle) — per-status transition table and webhook-event map
* [Sandbox magic numbers](/sandbox/magic-numbers) — simulate DLR scenarios with trailing digits
* [Message suppression](/guides/message-suppression) — duplicate-content blocking policy
* [Opt-outs](/api-reference/optouts) — recipient-level blocking lists
* [Analytics reference](/api-reference/analytics) — the reporting endpoints these statuses feed
* [Webhook events reference](/reference/webhook-events) — the payload shapes that carry `test_mode` and `state_class`
