> ## 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.

# Fax lifecycle in Devotel Orbit: states, timestamps, retries, and inbound routing

> The unifying reference for what happens between POST /messages/fax and the terminal webhook: the lifecycle state model, the timestamp fields on every fax row, the retry posture, per-DID inbound routing destinations, and a reconciliation recipe.

# Fax lifecycle reference

The [Outbound fax workflow](/guides/fax-send-workflow), [Inbound fax receive-side workflow](/guides/fax-receive-workflow), [Inbound fax routing](/guides/inbound-fax-workflow), and [Fax lifecycle orchestration](/guides/fax-lifecycle-orchestration) guides each cover one leg of fax in depth. This page is the reference that ties them together: the state model a fax row moves through, the timestamp fields that record when it moved, the retry posture on both legs, the per-DID inbound routing configuration, and the webhook events your consumers see at each transition.

**You will:**

1. [Read the lifecycle state model](#1-the-lifecycle-state-model)
2. [Read the timestamp fields on a fax row](#2-timestamp-fields-on-a-fax-row)
3. [Follow the outbound path end to end](#3-outbound-orchestration-request-to-terminal-state)
4. [Configure inbound routing destinations](#4-inbound-routing-where-a-received-fax-goes)
5. [Consume the webhook events](#5-webhook-event-reference)
6. [Use the audit trail for correlation](#6-tenant-scoped-audit-trail)
7. [Treat numbers as your fax address book](#7-numbers-as-fax-address-books)
8. [Reconcile sends against terminal events](#8-reconciliation-recipe)
9. [Know the limits and where errors surface](#9-limits-and-errors)

## Prerequisites

* A fax-capable number on your account (see [Buy numbers](/guides/buy-numbers)).
* An API key from **Settings → API Keys** (a `dv_live_sk_…` live key).
* Familiarity with the [Fax channel](/channels/fax) page for the field reference and limits.

## 1. The lifecycle state model

Every fax row on your tenant moves through a small state machine. Outbound rows start at `queued`; inbound rows land directly in their terminal state because the carrier has already completed the transmission before Orbit sees the fax.

| State | Meaning | Tenant webhook event | `GET /api/v1/fax/:id` row |
| - | - | - | - |
| `queued` | Orbit accepted the send (`202`) and queued the transmission. Nothing is billed and the carrier has not been handed the document yet. | none yet | `status: "queued"`, `external_id: null` |
| `sending` | The carrier dispatched the transmission and is negotiating T.38 with the destination. | none yet (poll or wait) | `status: "sending"`, `external_id` populated |
| `delivered` | The carrier confirmed the transmission. The wallet is charged exactly once, on this transition. | `message.delivered` with `channel: "fax"` | `status: "delivered"`, `delivered_at` stamped |
| `failed` | The carrier reported a terminal failure (busy, no answer, no fax tone, line quality). No charge. | `message.failed` with `channel: "fax"` and the carrier diagnostic in `data.metadata` | `status: "failed"`, `failed_at` stamped, `error_code` / `error_message` populated |
| `received` (inbound only) | A sender faxed your DID; the carrier assembled the PDF and Orbit stored the row. Inbound receipts are never billed. | `message.received` with `channel: "fax"` | `direction: "inbound"`, `status: "received"` |

Two properties of this model matter for anything you build on top of it:

* **`failed` is terminal and there is no platform retry state.** Orbit never re-attempts a failed fax on its own. If you want a retry policy, you own it: consume `message.failed` and re-send (a re-send is a new message row with its own `message_id`). Section 3 gives a recommended cadence.
* **`sent` is not terminal for fax.** It means the carrier completed its side of the handshake; the billing and terminal verdict is `delivered` or `failed`. Wait for the terminal event before you settle anything.

On the inbound leg the carrier may re-post its `fax.received` webhook (its own delivery retry to Orbit). Orbit deduplicates on the carrier `fax_id`, so a carrier retry never creates a duplicate message row and never re-fires `message.received`.

## 2. Timestamp fields on a fax row

Each fax row carries a timestamp per transition. A field is set once, the first time the row enters the matching state, and never rewritten afterwards; `null` means the row has not reached that state.

| Field | Set when |
| - | - |
| `created_at` | The row is created. For outbound this is the `queued` moment (the `202` response); for inbound it is the receive moment. |
| `sent_at` | The carrier dispatches the transmission (outbound only). |
| `delivered_at` | The carrier confirms delivery (outbound only). The billing moment. |
| `failed_at` | The carrier reports a terminal failure (outbound only). |
| `updated_at` | The most recent write to the row, whatever it was. |
| `external_id` | Not a timestamp, but the correlation field: the carrier-side fax ID, `null` until dispatch. |

A fully-settled outbound row as returned by `GET /api/v1/fax/:id`:

```json theme={null}
{
  "data": {
    "id": "msg_a1b2c3d4e5f6g7h8",
    "channel": "fax",
    "direction": "outbound",
    "from": "+18005551234",
    "to": "+14155552671",
    "status": "delivered",
    "external_id": "fax_telnyx_9f2c1a8e",
    "provider": "telnyx",
    "price": "0.0500",
    "currency": "USD",
    "error_code": null,
    "error_message": null,
    "created_at": "2026-05-09T12:00:00Z",
    "sent_at": "2026-05-09T12:00:04Z",
    "delivered_at": "2026-05-09T12:02:11Z",
    "failed_at": null,
    "updated_at": "2026-05-09T12:02:11Z"
  },
  "meta": {
    "request_id": "req_xyz789",
    "timestamp": "2026-05-09T12:03:00Z"
  }
}
```

From these fields you can derive the durations that matter operationally: `sent_at - created_at` is queue wait, `delivered_at - sent_at` is transmission time, and a `failed` row's `failed_at - sent_at` tells you how far into the handshake the attempt got. The list endpoint `GET /api/v1/fax` returns the same columns, cursor-paginated, so a dashboard refresh never needs the per-row call.

## 3. Outbound orchestration: request to terminal state

The outbound path runs six steps; only two of them are yours to trigger.

1. **Dispatch.** `POST /api/v1/messages/fax` validates the document URL, the `from`/`to` numbers, and the quality setting, then returns `202` with `status: "queued"`. Nothing is billed.
2. **Media fetch and render.** The worker fetches your `media_url` and prepares the document for transmission. A document that fails to fetch or render fails the send before the carrier is involved.
3. **Carrier handoff.** Orbit hands the document to the fax connection (the shared platform connection, or your dedicated one when `fax_connection_id` is set). `external_id` populates at this point.
4. **T.38 transmission.** The carrier negotiates with the destination and transmits page by page. The row is `sending`.
5. **Verdict.** The carrier confirms `delivered` (wallet charged once, idempotency keyed per send so a worker re-dispatch cannot double-bill) or `failed` (no charge, carrier diagnostic in `error_code`/`error_message`).
6. **Event emission.** `message.delivered` or `message.failed` fires on your tenant event bus and, when you set `status_callback` on the send, POSTs to that URL too.

### Retry policy

Orbit does not retry outbound faxes. A `failed` row stays `failed`; there is no background re-attempt and no state between `failed` and a new send. This is deliberate: fax destinations fail for reasons that resolve on human timescales (a busy machine, an office that closed), and an automatic immediate retry mostly re-fails and re-consumes line time.

When you implement your own retry, a cadence that matches how fax lines actually free up:

| Attempt | Wait before it | Rationale |
| - | - | - |
| 1 | (the original send) | |
| 2 | 5 minutes | A busy line or a machine mid-receive usually frees up in minutes. |
| 3 | 30 minutes | Crosses a human activity boundary (a meeting ends, a queue clears). |
| Give up | after 3 | Persistent failure across this spread is a directory or line problem, not a transient one; escalate per [Error handling runbook](/guides/error-handling-runbook). |

Two refinements before you re-send:

* **Downgrade quality on `poor_line_quality`, do not just retry.** `very_high` → `high` → `normal` shortens the T.38 handshake and survives noisier lines. The trade-offs are documented in the [Outbound fax workflow](/guides/fax-send-workflow#5-pdf-vs-tiff-and-picking-a-quality) guide.
* **Do not retry `no_fax_tone`.** The destination answered as a voice line; re-sending fax to a voice line never succeeds. Fix the directory entry instead.

Each re-send is a fresh message row with its own `message_id` and `created_at`, so your reconciliation (section 8) should group attempts by your business document ID, not by `message_id`.

## 4. Inbound routing: where a received fax goes

When a sender faxes one of your DIDs, the inbound path runs without any request from you:

1. **Carrier receive.** The sender's machine negotiates T.38 with the fax connection bound to the DID; the carrier assembles the received pages into a single PDF.
2. **Carrier webhook.** The carrier posts `fax.received` to Orbit with the sender (`from`), your DID (`to`), the carrier `fax_id`, and a `media_url` for the PDF. Orbit verifies the carrier signature; a rejected signature returns `401` and stores nothing.
3. **Tenant resolution and dedup.** Orbit resolves your tenant from the `to` DID and dedups on `fax_id`, so a carrier re-post creates exactly one message row.
4. **Ledger write.** The row is stored with `channel: "fax"`, `direction: "inbound"`, and the carrier media URL. This happens whether or not routing is configured; the ledger is independent of the human surface.
5. **Routing fan-out.** Orbit looks up the per-DID routing configuration and delivers the PDF to the configured destinations (below).
6. **Event emission.** `message.received` fires on your tenant event bus regardless of routing, so a webhook consumer sees every inbound fax even with routing disabled.

Routing is configured per DID with `GET` and `PUT /api/v1/numbers/:id/fax-routing`, where `:id` is the number's ID in your tenant inventory. The configuration:

```bash theme={null}
curl -X PUT https://api.orbit.devotel.io/api/v1/numbers/<phone-number-id>/fax-routing \
  -H "X-API-Key: dv_live_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "forward_emails": ["ap@acme.example"],
    "deliver_to_inbox": true,
    "archive_path": "inbound-faxes"
  }'
```

The destinations are additive; enable any combination:

| Destination | Recipient-side experience | Storage |
| - | - | - |
| `forward_emails` | Each recipient gets the PDF as an email attachment. If the carrier media URL cannot be fetched at delivery time, the email carries the download link instead and delivery still counts. | Up to 20 recipients per DID; validated at write time (a bad address or a 21st recipient is a `422` on the `PUT`, not a silent drop at receive time). |
| `deliver_to_inbox` | The fax opens as a ticket in the shared inbox, alongside your other channels. | The inbox copy is a durable artifact. |
| Event-bus webhook | Your consumer receives `message.received` with the media URL in `data.metadata`. | The carrier `media_url` is presigned and temporary: fetch promptly or rely on the email/inbox copy of record. |
| `archive_path` | No recipient surface; a storage location for the archived copy. | Use it as your system of record when compliance requires every inbound fax kept. |

`GET /api/v1/numbers/:id/fax-routing` returns the current config, or the disabled default for a DID that has never been configured, so you can render the "set up forwarding" empty state from it. A `404` means the number is not fax-reachable on your tenant; an `enabled: false` row means the ledger still persists inbound faxes but no fan-out runs. The full per-destination walkthrough, including the inbox ticket shape, is in the [Inbound fax routing](/guides/inbound-fax-workflow) guide.

## 5. Webhook event reference

Three tenant events cover the fax lifecycle, all in the standard envelope with `channel: "fax"`. Verify the `X-Orbit-Signature` header on every delivery per the [polyglot signature recipes](/guides/webhook-signature-verify-polyglot); the event catalogue and signature mechanics live under [Webhooks](/webhooks/events) and [Webhook security](/webhooks/security).

### `message.delivered`

Fires when an outbound fax settles as delivered. This is the billing moment.

```json theme={null}
{
  "id": "evt_d3l1v3ryd",
  "type": "message.delivered",
  "created_at": "2026-05-09T12:02:14Z",
  "data": {
    "message_id": "msg_a1b2c3d4e5f6g7h8",
    "status": "delivered",
    "state_class": "terminal",
    "is_terminal": true,
    "channel": "fax",
    "timestamp": "2026-05-09T12:02:14Z",
    "metadata": {
      "fax_id": "fax_telnyx_9f2c1a8e",
      "completed_at": "2026-05-09T12:02:11Z"
    }
  }
}
```

### `message.failed`

Fires when an outbound fax fails terminally. The carrier diagnostic is in `data.metadata`; no charge is applied.

```json theme={null}
{
  "id": "evt_f41l3d",
  "type": "message.failed",
  "created_at": "2026-05-09T12:02:20Z",
  "data": {
    "message_id": "msg_a1b2c3d4e5f6g7h8",
    "status": "failed",
    "state_class": "terminal",
    "is_terminal": true,
    "channel": "fax",
    "timestamp": "2026-05-09T12:02:20Z",
    "metadata": {
      "error_code": "poor_line_quality",
      "error_message": "T.38 handshake completed but line noise corrupted the transmission",
      "fax_id": "fax_telnyx_9f2c1a8e"
    }
  }
}
```

### `message.received`

Fires for every inbound fax, whether or not per-DID routing is enabled.

```json theme={null}
{
  "id": "evt_9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d",
  "type": "message.received",
  "created_at": "2026-09-25T12:00:00Z",
  "data": {
    "message_id": "msg_a1b2c3d4e5f6g7h8",
    "channel": "fax",
    "from": "+14155552671",
    "to": "+18005551234",
    "metadata": {
      "media_url": "https://media.carrier.example/fax.pdf",
      "fax_id": "fax_telnyx_3d7e2c1b"
    }
  }
}
```

When you set `status_callback` on an outbound send, the same delivered/failed event also POSTs to that per-send URL; the signature recipe applies there too.

## 6. Tenant-scoped audit trail

Fax leaves two audit surfaces, both tenant-scoped:

* **The message ledger is the per-fax audit record.** Every transition is recorded on the row itself via the immutable timestamp fields (section 2), so `GET /api/v1/fax/:id` at any later point reconstructs when the fax was queued, dispatched, and settled. There is no separate event log to join; the row is the log.
* **Configuration changes land in your organization audit log.** A `PUT /api/v1/numbers/:id/fax-routing` writes an audit entry recording the new `enabled` flag, recipient count, inbox flag, and whether an archive path is set, attributed to the acting user. Your Settings [audit log](/guides/audit-log) (and its export/webhook, where enabled) is where you answer "who turned fax forwarding on, and when."

Correlation fields across the two surfaces:

| Field | What it keys |
| - | - |
| `message_id` (`msg_…`) | One send or one receipt. Every webhook event for that fax carries it; the ledger rows and your reconciliation key on it. |
| `fax_id` / `external_id` | The carrier-side transmission ID. Use it when a support case needs the carrier's view of a specific attempt; it is `null` until dispatch. |
| Your business document ID | Not an Orbit field: put it in the send `metadata` you attach, and use it to group re-send attempts of the same document (section 3). |

Because a re-send is a new `message_id`, "across retries" queries are queries across rows sharing your business ID, each row self-describing via its timestamps and terminal `error_code`.

## 7. Numbers as fax address books

A fax reaches you through a fax-capable DID, exactly as a call reaches you through a voice-capable one. The number is the address: senders dial it, the carrier binds it to your tenant, and the per-DID routing configuration (section 4) decides what happens on receipt. Outbound, the `from` on a send must be one of your fax-capable DIDs, and the number inventory shows fax capability per number.

Practical posture for most organizations is to treat fax DIDs as a small, published address book: one number per receiving function (contracts, pharmacy, billing), each with its own routing configuration, rather than one shared number with one fan-out. Provisioning and capability flags are covered in [Buy numbers](/guides/buy-numbers); the channel-level model in [Fax channel](/channels/fax) and [Fax delivery model](/concepts/fax-delivery-model).

## 8. Reconciliation recipe

To confirm every send reached a terminal state, tail your webhook stream into a map keyed by `message_id`, then cross-check against the ledger. Billing only ever attaches to `delivered`, so the same reconciliation proves the charge set.

```python theme={null}
import json

terminal = {}  # message_id -> event

def on_webhook(event: dict):
    data = event["data"]
    if data.get("channel") != "fax":
        return
    if event["type"] in ("message.delivered", "message.failed"):
        terminal[data["message_id"]] = event  # state_class == "terminal"

# After the window closes, cross-check sends that never settled:
def unsettled(sent_ids: list[str]) -> list[str]:
    return [m for m in sent_ids if m not in terminal]
```

For anything `unsettled` returns, poll `GET /api/v1/fax/:id` before escalating: the record is readable from the moment the `202` returned, and a `null` `external_id` means the send is still queued rather than lost. The status variant `GET /api/v1/fax/:id/status` (cached about 10 seconds) cross-checks against the carrier once `external_id` populates. The billing-side reconciliation, including how fax charges appear in usage export, is in [CDR, usage export, and billing reconciliation](/guides/cdr-usage-export-billing-reconciliation).

## 9. Limits and errors

| Limit or error | Value / behaviour | Where it surfaces |
| - | - | - |
| Document size | 50 MB per document, no separate page-count cap. Split very long documents that time out in transmission into multiple sends. | `message.failed` if the carrier rejects the fetch. |
| Colour and raster fidelity | T.38 transmits raster pages; fax is a monochrome medium in practice. Do not promise colour fidelity or fine vector detail; submit print-ready documents with readable body sizes. | Degrades silently; verify with a test fax to yourself. |
| Quality | `normal`, `high`, `very_high` (default `normal`). Higher quality needs a cleaner line. | `poor_line_quality` failures on too-ambitious settings. |
| `busy` / `no_answer` | Destination line occupied or silent. | `message.failed` `data.metadata.error_code`; re-send on the section 3 cadence. |
| `no_fax_tone` | The destination answered as a voice line. | Same; do not re-send, fix the directory entry. |
| `poor_line_quality` | Handshake completed but line noise corrupted the transmission. | Same; downgrade quality, then re-send. |
| Retry exhaustion | There is no platform retry to exhaust: after your own retries give up, the last row is `failed` with the carrier diagnostic, and no charge was applied on any failed attempt. | `error_code` / `error_message` on `GET /api/v1/fax/:id` and in `message.failed`. |
| Routing config `422` | `forward_emails` rejects bad addresses and more than 20 recipients at write time. | The `PUT` response; fix before traffic lands. |
| Inbound media fetch failure | The carrier media URL could not be fetched at fan-out time. | Email recipients get the download link instead of the attachment; delivery still counts. |

Failure diagnosis beyond this table: the [Fax channel](/channels/fax) error catalogue, [Message failed troubleshooting](/troubleshooting/message-undelivered-failed), and the [Troubleshooting hub](/reference/troubleshooting-hub).

## Next steps

* [Outbound fax workflow](/guides/fax-send-workflow) — the send-side walkthrough with billing and the on-success-charge model
* [Inbound fax receive-side workflow](/guides/fax-receive-workflow) — the receive-side ledger, events, and failure diagnostics
* [Inbound fax routing](/guides/inbound-fax-workflow) — per-DID routing to email, inbox, and webhooks
* [Fax lifecycle orchestration](/guides/fax-lifecycle-orchestration) — the two-leg comparison: billing contract, failure vocabulary, retry posture
* [Fax delivery model](/concepts/fax-delivery-model) — the channel-level concept
* [Fax channel](/channels/fax) — field reference, status lifecycle, error codes, limits
* [Error handling runbook](/guides/error-handling-runbook) — terminal-state handling across channels
* [CDR, usage export, and billing reconciliation](/guides/cdr-usage-export-billing-reconciliation) — fax charges in usage export
* [Webhook signature verification (polyglot)](/guides/webhook-signature-verify-polyglot) — verify every event above


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.