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

# Outbound fax: send a document end to end

> Send a PDF or TIFF fax through the Messaging API, track it through delivery, consume status callbacks, and handle the failure modes that actually matter.

# Outbound fax workflow

The [Fax channel page](/channels/fax) covers the channel's request fields, error codes, and limits. This guide is the send-side walkthrough: from a document URL to a delivered fax, including how billing, receipts, and retries behave along the way. The receive side is covered separately in [Inbound fax routing](/guides/inbound-fax-workflow).

**You will:**

1. [Understand how the channel terminates](#1-how-outbound-fax-terminates)
2. [Understand the on-success-charge model](#2-the-on-success-charge-model)
3. [Send a fax](#3-send-a-fax)
4. [Consume delivery receipts and handle failures](#4-delivery-receipts-and-failure-handling)
5. [Choose the right document format and quality](#5-pdf-vs-tiff-and-picking-a-quality)

## Prerequisites

* A fax-capable number on your account (see [Buy numbers](/guides/buy-numbers)). The `from` field on every send must be one of your numbers.
* An API key from **Settings → API Keys** (a `dv_live_sk_…` live key).
* A publicly reachable HTTPS URL for the document you are sending. The carrier fetches it directly — Orbit does not re-host it — so the URL must resolve from the public internet, and `http://` is rejected at send time.
* A public HTTPS endpoint if you want per-send delivery callbacks (`status_callback`). Without one, outcome events reach your tenant-wide event bus subscribers anyway.

## 1. How outbound fax terminates

Outreach channels on Orbit are terminated deliberately. Voice calls and SMS exit only via the Devotel wholesale softswitch. Fax (T.38/G.711) and MMS are the two named exceptions: fax transmissions terminate on Telnyx Programmable Fax. When you send a fax, Orbit dispatches your document to the Telnyx fax connection resolved for your org — a tenant-assigned connection if you attached one in tenant settings, otherwise the platform default.

That split matters operationally in two places:

* **Billing** is on Telnyx carrier confirmations (section 2), not on the softswitch call detail records your voice traffic generates.
* **Failure diagnostics** come from the carrier's own vocabulary — busy, no answer, no fax tone, poor line quality — and Orbit passes them through unchanged (section 4).

If your org needs a dedicated Telnyx Fax application (compliance, data residency), set the tenant's `fax_connection_id` in tenant settings; all subsequent sends resolve that connection before the platform default.

## 2. The on-success-charge model

A fax is only billed when the carrier confirms successful transmission. If the attempt fails — busy, no answer, no fax tone, poor line quality, handshake rejection, or an internal error before dispatch — your wallet is not charged for it.

The mechanics:

1. **Accepted** — `POST /api/v1/messages/fax` validates and records the message. Nothing is billed at this point.
2. **Transmitting** — the carrier runs the T.38 handshake and transmits pages. Still nothing billed.
3. **Carrier verdict** — when the carrier confirms the transmission succeeded, the charge is applied and the message moves to `delivered`. Any path ending in `failed` is free.

A side effect worth knowing: the balance ledger for a fax settles asynchronously. The send response shows no charge yet; the wallet deduction appears when the carrier's success confirmation lands, alongside the `delivered` event.

The per-send idempotency key (`orbit-fax:<message-id>`) dedups at the carrier, so a client retry or a queue-worker re-dispatch of the same message record can never double-bill you.

## 3. Send a fax

Sending goes through the unified Messaging API at `POST /api/v1/messages/fax`:

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.orbit.devotel.io/api/v1/messages/fax \
    -H "X-API-Key: dv_live_sk_YOUR_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "to": "+14155552671",
      "from": "+18005551234",
      "media_url": "https://files.example.com/contracts/acme-4821.pdf",
      "quality": "high",
      "status_callback": "https://www.example.com/webhooks/fax",
      "status_callback_secret": "my-per-send-signing-secret",
      "metadata": { "contract_id": "ct_4821", "customer_ref": "cust_9" }
    }'
  ```

  ```typescript Node.js theme={null}
  import { Orbit } from '@devotel-orbit/node';

  const orbit = new Orbit({ apiKey: process.env.ORBIT_API_KEY });

  // No typed fax helper in the Node SDK — reach the channel endpoint
  // through the generic request escape hatch.
  const message = await orbit.request<{
    data: { id: string; status: string };
  }>('POST', '/messages/fax', {
    to: '+14155552671',
    from: '+18005551234',
    media_url: 'https://files.example.com/contracts/acme-4821.pdf',
    quality: 'high',
    status_callback: 'https://www.example.com/webhooks/fax',
    status_callback_secret: 'my-per-send-signing-secret',
    metadata: { contract_id: 'ct_4821', customer_ref: 'cust_9' },
  });

  console.log(message.data.id);      // msg_a1b2c3d4e5f6g7h8
  console.log(message.data.status);  // 'queued'
  ```

  ```python Python theme={null}
  from orbit_sdk import OrbitClient

  client = OrbitClient.from_env()  # reads ORBIT_API_KEY

  # No typed fax helper in the Python SDK — the generic request escape
  # hatch carries the full field set.
  message = client.request(
      "POST",
      "/messages/fax",
      json_body={
          "to": "+14155552671",
          "from": "+18005551234",
          "media_url": "https://files.example.com/contracts/acme-4821.pdf",
          "quality": "high",
          "status_callback": "https://www.example.com/webhooks/fax",
          "status_callback_secret": "my-per-send-signing-secret",
          "metadata": {"contract_id": "ct_4821", "customer_ref": "cust_9"},
      },
  )

  print(message["data"]["id"])       # msg_a1b2c3d4e5f6g7h8
  print(message["data"]["status"])   # 'queued'
  ```
</CodeGroup>

The response:

```json theme={null}
{
  "data": {
    "id": "msg_a1b2c3d4e5f6g7h8",
    "status": "queued",
    "channel": "fax"
  },
  "meta": {
    "request_id": "req_xyz789",
    "timestamp": "2026-05-09T00:00:00Z"
  }
}
```

The returned `data.id` is the Orbit message ID you correlate on — webhook events carry the same `message_id`, so no provider-side mapping is needed. The provider fax ID attaches asynchronously as `external_id` and is readable via `GET /api/v1/fax/:id`.

Validation errors surface at send time, not after: an invalid E.164 `to` returns `422 INVALID_PHONE_NUMBER`, a missing `from` or `media_url` returns `422 MISSING_REQUIRED_FIELD`, and an orphaned `status_callback_secret` (set with no `status_callback` URL) returns `422 VALIDATION_ERROR`. The full field table and error catalogue are on the [Fax channel page](/channels/fax).

<Note>
  `status_callback` is a per-send override. When you set it, delivery events for this message POST to that URL instead of only the tenant event bus. Pair it with `status_callback_secret` — the secret signs every callback to the URL, and [Webhook security](/webhooks/security) shows the verification recipe.
</Note>

## 4. Delivery receipts and failure handling

An outbound fax moves `queued` → `sending` → `sent` → `delivered` (or `failed`). Each transition emits a channel-agnostic event — `message.delivered` or `message.failed` with `channel: "fax"` (there is no separate `fax.*` event stream). The event payload carries `message_id`, `status`, `state_class`, and `is_terminal`. Subscribe per the [events catalogue](/webhooks/events).

Two terminal-read differences from SMS semantics:

* **`sent` is non-terminal for fax.** The carrier has completed transmission to the end station, but the receiving fax machine may still be confirming the final pages. Treat `sent` as "carrier-completed" and wait for `delivered` before telling a customer "they have it."
* **`failed` is single-attempt.** Orbit never retries a fax that has reached `failed`; re-sending is your call. The queue worker only re-dispatches a message that is still `queued` (e.g. a transient provider 408), and the carrier idempotency key prevents any duplicate transmission from that re-dispatch.

### Failure-handling recipe

Match `error_code` on `message.failed` events — the values are the carrier's own diagnostics, passed through unchanged:

```javascript theme={null}
async function onMessageFailed(event) {
  const { message_id, channel, error_code } = event.data;
  if (channel !== 'fax') return;

  switch (error_code) {
    case 'busy':
    case 'no_answer':
      // Destination line occupied or unanswered — schedule a re-send
      // after a delay; the recipient's fax line may be busy.
      await scheduleResend(message_id, { afterMinutes: 15 });
      break;
    case 'no_fax_tone':
      // The number answered as a voice line — confirm the destination
      // actually terminates a fax machine before re-sending.
      await flagDestinationForReview(message_id);
      break;
    case 'poor_line_quality':
      // Resend at a lower quality setting — a shorter T.38 handshake
      // survives noisier lines.
      await scheduleResend(message_id, { quality: 'normal' });
      break;
    default:
      // Carrier gave no diagnostic, or an internal error — surface to
      // reconciliation; the attempt was not billed.
      await recordTerminalFailure(message_id, error_code);
  }
}
```

Whichever consumer you build:

* Verify the `X-Orbit-Signature` header and reject deliveries older than five minutes ([Webhook security](/webhooks/security)).
* Return `2xx` quickly; only 2xx counts as delivered, and retries continue for anything else.
* Deduplicate on the event `data.id` — delivery is at-least-once.

If webhooks are unavailable, poll `GET /api/v1/fax/:id/status` (cached about 10 seconds) — note it returns `404` until the transmission has been dispatched, so poll `GET /api/v1/fax/:id` for the record first if you just got the send response.

## 5. PDF vs TIFF and picking a quality

The carrier accepts **PDF or TIFF**, single file, up to 50 MB. Multi-page documents are supported with no separate page-count cap.

* **PDF** is the right default for everything business-generated: contracts, invoices, forms, letterheads. Text stays crisp at every resolution and file sizes are small.
* **TIFF** is for faxes composed of scanned image pages — a scanned wet-signature or a photographed document. If your source is a scan, TIFF avoids a lossy image-through-PDF round trip, but a generated PDF still beats TIFF on size and handshaking stability.

The `quality` field trades resolution against handshake length:

| Value              | Behavior                                                                                                                                                          |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `normal` (default) | Shortest T.38 handshake; most tolerant of lossy lines. Fine for plain text documents.                                                                             |
| `high`             | Better legibility for small text and fine graphics. The right default for documents with tables or signatures.                                                    |
| `very_high`        | Artwork-grade resolution. Lengthens the handshake and can tip a long fax into `poor_line_quality` on bad lines — use it only when the detail is genuinely needed. |

Practical rule: start at `high`, and on a `poor_line_quality` failure re-send at `normal` — the workflow in section 4 already encodes that fallback.

## Next steps

* [Fax channel](/channels/fax) — field reference, status lifecycle, error codes, and limits
* [Inbound fax routing](/guides/inbound-fax-workflow) — the receive side: per-DID routing to email and the inbox
* [Webhook consumer](/guides/webhook-consumer) — reliable endpoint design
* [Webhook security](/webhooks/security) — signature verification recipes (org and per-send secrets)
* [Message failed troubleshooting](/troubleshooting/message-undelivered-failed) — general failure diagnosis
