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

# Migrate from Telnyx Programmable Fax to mintfax

> Endpoint mapping, auth swap, webhook diff, retry-loop removal, and a pricing translation for engineers moving off Telnyx.

## Outcome

By the end of this guide your Telnyx Programmable Fax integration runs against the mintfax sandbox: requests go to `https://api.mintfax.com/v1`, the retry loop on your side is gone, webhook handlers verify Standard Webhooks signatures and consume the smaller mintfax event set, and you have a known-good cutover path back to Telnyx if you need it.

This is a dev-execution guide. For the marketing comparison see the [Telnyx compare page](https://mintfax.com/compare/telnyx-alternative). SIP-trunking voice topics and Telnyx phone-number rental are out of scope.

## Prerequisites

* A mintfax sandbox account with an API key (`mfx_test_...`) and a webhook endpoint with its signing secret.
* Your existing Telnyx integration in a state you can run end-to-end (so you can diff behavior).
* `curl` and Node.js 18+ on your machine.
* Familiarity with [verifying webhook signatures](/docs/webhooks/verify-requests), [idempotency keys](/docs/idempotency), the [error catalog](/docs/errors), the [event catalog](/docs/webhooks/events/overview), and the [sandbox magic numbers](/docs/sandbox).

## Step 1: Map the endpoints

The base URL changes. Most resource paths stay close to their Telnyx shape. Two operations have no mintfax equivalent because mintfax handles them internally.

| Telnyx                                   | mintfax                                                |
| ---------------------------------------- | ------------------------------------------------------ |
| `POST   https://api.telnyx.com/v2/faxes` | `POST   https://api.mintfax.com/v1/faxes` (multipart)  |
| `GET    /v2/faxes`                       | `GET    /v1/faxes` (cursor-paginated)                  |
| `GET    /v2/faxes/{id}`                  | `GET    /v1/faxes/{id}`                                |
| `POST   /v2/faxes/{id}/actions/cancel`   | No equivalent. mintfax runs faxes to a terminal state. |
| `POST   /v2/faxes/{id}/actions/refresh`  | No equivalent. mintfax pushes status via webhooks.     |
| `DELETE /v2/faxes/{id}`                  | `DELETE /v1/faxes/{id}` (terminal-state only)          |
| `GET    /v2/faxes/{id}/download`         | `GET    /v1/faxes/{id}/image`                          |
| `POST   /v2/fax_applications`            | No equivalent. mintfax has no `connection_id`.         |

The two missing operations are intentional. mintfax retries automatically and pushes status events as state changes, so cancel and refresh are not primitives a client needs.

**Verify** Run `curl https://api.mintfax.com/v1/faxes -H "Authorization: Bearer $MINTFAX_KEY"` and confirm a `200` with a `data` array (empty is fine on a fresh sandbox).

## Step 2: Swap authentication

Both APIs use `Authorization: Bearer ...`. The header is identical; the key shape changes.

| Aspect           | Telnyx                          | mintfax                                   |
| ---------------- | ------------------------------- | ----------------------------------------- |
| Header           | `Authorization: Bearer KEY`     | `Authorization: Bearer KEY`               |
| Key prefix       | None (opaque string)            | `mfx_test_` (sandbox), `mfx_live_` (live) |
| Scope            | Account-level                   | Per-environment within an account         |
| Rotation latency | Up to 60 minutes to take effect | Immediate                                 |

The self-identifying prefix is the one behavior change worth noting: an `mfx_test_` key sitting in your production config is now greppable.

<CodeGroup>
  ```bash curl theme={null}
  # Telnyx
  curl -X POST https://api.telnyx.com/v2/faxes \
    -H "Authorization: Bearer KEY_xxxxxxxx" \
    -H "Content-Type: application/json" \
    -d '{"connection_id":"1234","from":"+13125551234","to":"+12015550100","media_url":"https://example.com/doc.pdf"}'

  # mintfax
  curl -X POST https://api.mintfax.com/v1/faxes \
    -H "Authorization: Bearer mfx_test_4eC39HqLyjWDarjtT1zdp7dc" \
    -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
    -F "to=+12015550100" \
    -F "file=@document.pdf"
  ```

  ```javascript Node.js theme={null}
  // Drop-in replacement for a Telnyx send call.
  import fs from "node:fs";
  import { randomUUID } from "node:crypto";

  const form = new FormData();
  form.append("to", "+12015550100");
  form.append("file", new Blob([fs.readFileSync("document.pdf")]), "document.pdf");

  const res = await fetch("https://api.mintfax.com/v1/faxes", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.MINTFAX_KEY}`,
      "Idempotency-Key": randomUUID(),
    },
    body: form,
  });
  const fax = await res.json();
  console.log(fax.id, fax.status); // fax_..., queued
  ```
</CodeGroup>

**Verify** The response is `201 Created` with `status: "queued"` and a `fax_`-prefixed `id`.

## Step 3: Rewrite the send-fax body

The send body changes shape because mintfax does not use a Telnyx-style fax application or pull-from-URL flow. Drop these fields entirely: `connection_id`, `from`, `media_url`, `media_name`, `from_display_name`, `quality`, `t38_enabled`, `monochrome`, `black_threshold`, `store_media`, `store_preview`, `preview_format`. Replace `media_url` with a multipart `file` upload. Replace `client_state` with the [`Idempotency-Key`](/docs/idempotency) header for correlation, plus the `tags` map if you need echoed metadata on webhooks.

Two ceilings to plan for:

* File size cap drops from 50 MB on Telnyx to 10 MB on mintfax.
* mintfax assigns the originating number, so any code that loads a Telnyx-owned DID and passes it as `from` can be deleted.

T.38, G.711, baud rate, ECM, and resolution toggles are not exposed. mintfax does expose `csid`, `page_size`, `resolution` (`standard` or `fine`), and `optimize_for` (`text` or `photo`) per fax. See `POST /v1/faxes` in the OpenAPI spec for the full body.

**Verify** Submit a fax to the sandbox success magic number `+15005550001`. The response is `201` with `status: "queued"`. Within seconds, `GET /v1/faxes/{id}` reports `status: "delivered"`.

## Step 4: Delete the retry loop

This is the largest single change. Telnyx does not auto-retry, so your integration almost certainly has retry orchestration somewhere: a queue, exponential backoff, a per-error-code policy, dead-letter handling, and idempotency wrapping. On mintfax, all of that is unnecessary.

mintfax retries with exponential backoff, configured per fax via the `retries` field on the send request (default 3, max 10). Each attempt against the destination fires its own `fax.sending` event, so the count of `fax.sending` deliveries is your mid-flight progress signal without your needing to drive it. The terminal events arrive once: `fax.delivered` on success, `fax.failed` after retries exhaust.

This flips the meaning of `fax.failed`. Counter logic that increments on every Telnyx `fax.failed` will under-count by `retries` after migration. Rewrite handlers to treat `fax.failed` as terminal, not per-attempt.

```javascript Node.js theme={null}
// Before: Telnyx with custom retry orchestration
async function sendWithRetries(fax, attempt = 0) {
  try {
    return await telnyx.faxes.create(fax);
  } catch (e) {
    if (attempt >= 5 || !isRetryable(e)) throw e;
    await sleep(2 ** attempt * 1000);
    return sendWithRetries(fax, attempt + 1);
  }
}

// After: mintfax handles retries internally
const res = await fetch("https://api.mintfax.com/v1/faxes", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.MINTFAX_KEY}`,
    "Idempotency-Key": randomUUID(),
  },
  body: form, // includes optional `retries` field, default 3, max 10
});
```

**Verify** Send to the transient-failure magic number `+15005550004`. The fax fails on attempt 1, mintfax fires a second `fax.sending` for attempt 2, and attempt 2 delivers. Your handler should see one `fax.delivered`, not a terminal failure.

## Step 5: Update webhook signature verification

Telnyx signs with Ed25519 and a per-account public key. mintfax implements the [Standard Webhooks](https://www.standardwebhooks.com/) specification: HMAC-SHA256 with a per-endpoint shared secret, signed input is `{webhook-id}.{webhook-timestamp}.{body}`, signature is base64.

| Aspect           | Telnyx                              | mintfax                                                              |
| ---------------- | ----------------------------------- | -------------------------------------------------------------------- |
| Algorithm        | Ed25519                             | HMAC-SHA256 (Standard Webhooks)                                      |
| Signed content   | timestamp, pipe, raw JSON body      | `webhook-id`, dot, `webhook-timestamp`, dot, raw body                |
| Signature header | `telnyx-signature-ed25519` (base64) | `webhook-signature` (`v1,<base64>` tokens, space-separated)          |
| Timestamp header | `telnyx-timestamp`                  | `webhook-timestamp`                                                  |
| Event id header  | (none; in payload)                  | `webhook-id` - stable across retry attempts, ideal for deduplication |
| Key character    | One public key per account          | One signing secret per webhook endpoint                              |
| Replay window    | Implementation-defined              | 5 minutes, enforced by the Standard Webhooks libraries on your side  |

Two operational consequences. The Standard Webhooks reference libraries handle signature comparison, the 5-minute timestamp tolerance, and parsing multi-secret rotation tokens for you - you do not write the verification by hand. Multiple webhook endpoints now mean multiple secrets, so a leaked secret affects fewer endpoints, but you manage more secrets.

```javascript Node.js theme={null}
import express from 'express';
import { Webhook } from 'standardwebhooks';

const app = express();
const wh = new Webhook(process.env.MINTFAX_WEBHOOK_SECRET);

app.post(
  '/webhooks/mintfax',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    try {
      const event = wh.verify(req.body, {
        'webhook-id': req.headers['webhook-id'],
        'webhook-timestamp': req.headers['webhook-timestamp'],
        'webhook-signature': req.headers['webhook-signature'],
      });
      // dedupe on event.id, route on event.type
      res.sendStatus(200);
    } catch {
      res.status(401).send('Invalid signature');
    }
  },
);
```

The full multi-language reference and rotation guidance lives on the [verify webhook signatures](/docs/webhooks/verify-requests) page.

**Verify** Send a fax to `+15005550001` with a `webhook_url` pointing at your endpoint. Confirm your handler accepts the request and rejects a tampered copy with `401`.

## Step 6: Update webhook event handlers

mintfax fires a smaller event set with a flatter envelope.

| Telnyx                | mintfax         | Notes                                                             |
| --------------------- | --------------- | ----------------------------------------------------------------- |
| `fax.queued`          | `fax.queued`    | 1:1 semantically                                                  |
| `fax.media.processed` | (none)          | Folded into `fax.sending`'s `pages` field                         |
| `fax.sending.started` | `fax.sending`   | Fires once per delivery attempt; also the mid-flight retry signal |
| `fax.delivered`       | `fax.delivered` | Terminal; credit hold captured                                    |
| `fax.failed`          | `fax.failed`    | Fires once after retries exhaust, not per attempt                 |
| (none)                | `balance.low`   | Account-level                                                     |
| (none)                | `balance.topup` | Account-level                                                     |

The envelope changes. Telnyx wraps in `data: { event_type, id, occurred_at, payload: {...} }` plus a sibling `meta: { attempt, delivered_to }`. mintfax uses `{ id, type, created, data: { object: {...} } }` with the `id` (`evt_`-prefixed, also delivered as the `webhook-id` header) for deduplication and `type` for event routing. See the full payload shapes in the [event catalog](/docs/webhooks/events/overview).

Code that waits for `fax.media.processed` to know "the PDF is rendered" should switch to watching `fax.sending` for a non-null `data.object.pages` field. Code that bumps a "fax has failed" counter on every `fax.failed` should treat the event as terminal instead.

Failure reasons translate too. Telnyx `failure_reason` strings (`user_busy`, `receiver_no_response`, `fax_signaling_error`, and so on) become normalized codes on `data.object.error_code`. For example, Telnyx `user_busy` maps to mintfax `line_busy`. The full taxonomy is in the [error catalog](/docs/errors).

**Verify** Replay a previously-handled event `id` and confirm your handler treats it as a duplicate. Submit a fax that fails permanently (`+15005550005`) and confirm exactly one `fax.failed` event arrives.

## Step 7: Translate pricing for the rollout review

Telnyx publishes $0.007 per page plus SIP trunking minutes (US domestic outbound from $0.005/min). mintfax publishes per-page-only at $0.060 / $0.048 / \$0.036 depending on tier.

Worked example: a 3-page fax that takes 60 seconds to transmit.

| Cost component       | Telnyx                      | mintfax                        |
| -------------------- | --------------------------- | ------------------------------ |
| Per-page             | 3 x $0.007 = $0.021         | 3 x $0.060 = $0.180 (top tier) |
| SIP minutes          | 1 min x $0.005/min = $0.005 | \$0 (included)                 |
| **Direct API total** | **\$0.026**                 | **\$0.180**                    |

Sticker price is roughly 7x higher on mintfax. Your migration brief should also include the engineering hours that disappear with the retry loop, dead-letter, and per-error-code dispatch table you maintained for Telnyx, plus the Telnyx phone-number monthly rental that no longer applies. Build those rows for your specific volume.

## Verify

End-to-end check, all against the sandbox:

1. Send to `+15005550001`. Receive `fax.queued`, `fax.sending`, `fax.delivered`. The fax record reports `pages` populated.
2. Send to `+15005550004`. Receive `fax.queued`, `fax.sending` (attempt 1), `fax.sending` (attempt 2 after the transient failure), then `fax.delivered`. Confirm your code routes the repeated `fax.sending` as in-progress, not terminal.
3. Send to `+15005550005`. Receive `fax.queued`, then a `fax.sending` per attempt (count depends on your `retries` setting), then exactly one `fax.failed` with a normalized `data.object.error_code`.
4. Send a request without `Idempotency-Key`. Confirm the response includes `X-Idempotency-Key: not provided; recommended` and decide whether to add the header to your client.
5. Replay a previously-delivered webhook and confirm your handler skips it via the `webhook-id` header (same value as `event.id`).

If any step fails, the [error catalog](/docs/errors) lists each error code, the HTTP status, and the next action.

## What to do next

* [Verify webhook signatures](/docs/webhooks/verify-requests) - the full Standard Webhooks receiver recipe with raw-body capture and key rotation.
* [Event types](/docs/webhooks/events/overview) - every event type, payload shape, and delivery behavior.
* [Idempotency keys](/docs/idempotency) - retry safety on outbound API calls.
* [Sandbox](/docs/sandbox) - the magic-number matrix used in the verify steps above.
* [Errors](/docs/errors) - error codes, HTTP statuses, and deterministic next actions.
