> ## 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 Documo / mFax to mintfax

> Endpoint mapping, authentication swap, webhook signing change, and rollback plan for engineers porting an integration off Documo's v1 API.

## Outcome

After this guide you will have a mintfax sandbox integration that does what your Documo client does today: send a fax, list and inspect records, fetch the rendered image, resend, delete, and verify webhook payloads. You will also know which Documo features have no mintfax equivalent and how to keep both providers running side by side until you cut over. Samples are in curl and Node.

## Prerequisites

* A mintfax sandbox API key (prefix `mfx_test_`, issued at `POST /account/register/verify`). Sample: `mfx_test_4eC39HqLyjWDarjtT1zdp7dc`.
* Your existing Documo API key and a working call to `POST https://api.documo.com/v1/fax/send`.
* An HTTPS receiver URL for webhook code.
* Node 18+ for the Node samples.

## What changes at the API boundary

| Area              | Documo                               | mintfax                                                                                                         |
| ----------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| Base URL          | `https://api.documo.com`             | `https://api.mintfax.com/v1`                                                                                    |
| Authentication    | `Authorization: Basic <api_key>`     | `Authorization: Bearer mfx_test_...` or `Bearer mfx_live_...`                                                   |
| Send field        | `recipientFax` (bare digits)         | `to` (E.164 only, leading `+` and country code)                                                                 |
| Cover-page tokens | PascalCase: `RecipientName`, `Notes` | `{{snake_case}}`: `{{recipient_name}}`, `{{message}}`                                                           |
| Webhook security  | HTTP Basic auth on the receiver      | Standard Webhooks: HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{body}` in the `webhook-signature` header |

Each row gets its own step below, plus the behavioral differences that bite during cutover: idempotency, retry budgets, error codes, and the account model.

## Step 1: Swap authentication

Documo passes the API key directly after `Basic`. mintfax uses Bearer tokens with a `mfx_test_` or `mfx_live_` prefix that routes to sandbox or live. Replace the entire `-H 'Authorization: ...'` line, not just the value.

<CodeGroup>
  ```bash curl theme={null}
  # Documo (before)
  curl -X POST 'https://api.documo.com/v1/fax/send' \
    -H 'Authorization: Basic YOUR_DOCUMO_KEY' \
    -F 'recipientFax=12125551234' \
    -F 'file=@document.pdf'

  # mintfax (after)
  curl -X POST 'https://api.mintfax.com/v1/faxes' \
    -H 'Authorization: Bearer mfx_test_4eC39HqLyjWDarjtT1zdp7dc' \
    -F 'to=+12125551234' \
    -F 'file=@document.pdf'
  ```

  ```javascript Node theme={null}
  // Documo (before)
  await fetch('https://api.documo.com/v1/fax/send', {
    method: 'POST',
    headers: { Authorization: `Basic ${process.env.DOCUMO_KEY}` },
    body: form,
  });

  // mintfax (after)
  await fetch('https://api.mintfax.com/v1/faxes', {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.MINTFAX_KEY}` },
    body: form,
  });
  ```
</CodeGroup>

**Verify** A `GET /v1/account` with the new header returns 200 and the environment name. `401 unauthenticated` means the header value is malformed or the key prefix does not match the environment you expect.

## Step 2: Translate the send-fax payload

Field map for the most common parameters:

| Documo           | mintfax             | Notes                                                                |
| ---------------- | ------------------- | -------------------------------------------------------------------- |
| `recipientFax`   | `to`                | Reformat to E.164. Bare digits return `validation_failed`.           |
| `recipientName`  | `recipient_name`    | Cover-page token value                                               |
| `subject`        | `subject`           | Same                                                                 |
| `notes`          | `message`           | Rename: Documo `notes` becomes mintfax `message`                     |
| `coverPage` bool | `cover_page` string | Pass `standard`, `hipaa`, or `none`                                  |
| `faxNumber`      | `csid`              | Sender CSID transmitted in the T.30 handshake; up to 20 chars        |
| `tags` array     | `tags` object       | Up to 10 keys, 64-char keys, 255-char values                         |
| `customFields`   | (use `tags`)        | Collapse custom fields into the `tags` map                           |
| `scheduledOn`    | (no equivalent)     | mintfax queues immediately; schedule client-side or remove the field |
| file part        | `file`              | One file per request, max 10 MB                                      |

mintfax accepts PDF, DOCX, XLSX, JPG, PNG, TIFF, HTML, and TXT. Documo also accepts PPT/PPTX, ODT/ODS/ODP, RTF, GIF, and printer formats (PCL/EPS/PS). Audit upload paths for those types and convert to PDF or DOCX before submitting; unsupported types return `422 validation_failed`.

<CodeGroup>
  ```bash curl theme={null}
  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=@invoice.pdf' \
    -F 'cover_page=standard' \
    -F 'recipient_name=Acme Billing' \
    -F 'subject=April invoice' \
    -F 'message=See attached, thanks.' \
    -F 'tags[order_id]=1234'
  ```

  ```javascript Node theme={null}
  import { readFile } from 'node:fs/promises';
  import crypto from 'node:crypto';

  const form = new FormData();
  form.append('to', '+12015550100');
  form.append(
    'file',
    new Blob([await readFile('invoice.pdf')], { type: 'application/pdf' }),
    'invoice.pdf'
  );
  form.append('cover_page', 'standard');
  form.append('recipient_name', 'Acme Billing');
  form.append('subject', 'April invoice');
  form.append('message', 'See attached, thanks.');
  form.append('tags[order_id]', '1234');

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

If you customized Documo's PascalCase cover-page tokens (`RecipientName`, `SenderName`, `Pages`, `Date`, `Subject`, `Notes`), rename to `{{recipient_name}}`, `{{sender_name}}`, `{{page_count}}`, `{{date}}`, `{{subject}}`, `{{message}}`. `RecipientPhone`, `RecipientCompany`, and the domain-logo token have no v1 equivalent; render the cover page yourself and pass it through `cover_page_html`.

**Verify** The response is `201` with `id`, `status: "queued"`, the `to` value, and `created_at`.

## Step 3: Add idempotency to every send

Documo does not advertise an idempotency mechanism. If you retry `POST /v1/fax/send` after a network timeout, you can double-send. mintfax accepts an `Idempotency-Key` header on every state-mutating POST. Duplicate keys return the original response without re-processing for 24 hours.

Generate one key per logical send and reuse it on retry. mintfax echoes the key in `X-Idempotency-Key` on the response.

```javascript Node theme={null}
const idempotencyKey = crypto.randomUUID();

for (let attempt = 0; attempt < 3; attempt++) {
  const res = await fetch('https://api.mintfax.com/v1/faxes', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.MINTFAX_KEY}`,
      'Idempotency-Key': idempotencyKey,
    },
    body: form,
  });
  if (res.ok) break;
  if (res.status >= 500) continue; // safe replay with same key
  throw new Error(`fax submit failed: ${res.status}`);
}
```

**Verify** Submit the same request twice with the same key. The second response matches the first, no second fax appears in `GET /faxes`, and `X-Idempotency-Key` echoes your key. See [Idempotency keys](/docs/idempotency).

## Step 4: Update list, get, image, resend, and delete

| Operation      | Documo                             | mintfax                                 |
| -------------- | ---------------------------------- | --------------------------------------- |
| List history   | `GET /v1/fax`                      | `GET /v1/faxes` (cursor-paginated)      |
| Get one fax    | `GET /v1/fax/{messageId}`          | `GET /v1/faxes/{id}`                    |
| Download image | `GET /v1/fax/{messageId}/download` | `GET /v1/faxes/{id}/image` (TIFF)       |
| Resend         | `POST /v1/fax/{messageId}/resend`  | `POST /v1/faxes/{id}/resend`            |
| Delete         | (portal only)                      | `DELETE /v1/faxes/{id}` (terminal only) |

Pagination is cursor-based: pass `meta.next_cursor` back as `?cursor=...`. Default window is seven days; widen with `created_after` and `created_before`.

mintfax has no cancel endpoint. Faxes queue immediately and cannot be recalled. If a workflow depends on schedule-then-cancel, redesign before migrating.

`DELETE /faxes/{id}` only works on terminal faxes. In-flight calls return `409 fax_not_terminal` with `action: wait_for_completion`. Documents purged by retention or zero-footprint mode return `422 fax_data_purged` from image and resend; submit a new fax. See [Errors](/docs/errors) for the full catalog.

## Step 5: Replace webhook Basic auth with Standard Webhooks verification

Documo secures webhooks with HTTP Basic auth on the receiver, optionally pinned to source IP `35.225.187.84`. mintfax implements the [Standard Webhooks](https://www.standardwebhooks.com/) specification: HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{body}`, signed with your endpoint's secret, with three headers on every delivery (`webhook-id`, `webhook-timestamp`, `webhook-signature`). A receiver that only checks Basic credentials will not auto-port; you have to add verification code.

Use the Standard Webhooks reference library for your language - it handles constant-time signature comparison, the 5-minute timestamp window, and parsing rotation tokens. Do not hand-roll the HMAC.

```javascript Node 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 (same value as the webhook-id header), then route by event.type
      res.status(200).send('ok');
    } catch {
      res.status(401).send('bad signature');
    }
  }
);
```

The full multi-language reference (library snippets, raw-body capture, key rotation) is on [Verify webhook signatures](/docs/webhooks/verify-requests).

Event types differ. Documo fires `fax.v1.outbound.init`, `.succeed`, `.failed`, `.complete`, `.extended`. mintfax fires `fax.queued`, `fax.sending`, `fax.delivered`, `fax.failed`. Map your handler:

| Documo event               | mintfax event                                                                              |
| -------------------------- | ------------------------------------------------------------------------------------------ |
| `fax.v1.outbound.init`     | `fax.queued`                                                                               |
| `fax.v1.outbound.succeed`  | `fax.delivered`                                                                            |
| `fax.v1.outbound.failed`   | `fax.failed`                                                                               |
| `fax.v1.outbound.extended` | `fax.sending` - fires once per delivery attempt, so it is also the mid-flight retry signal |
| `fax.v1.outbound.complete` | (no direct equivalent; treat `fax.delivered`/`fax.failed` as terminal)                     |

Payload shape changes: mintfax wraps fields in an envelope (`id`, `type`, `created`, `data.object`). Use `id` (`evt_`-prefixed, also delivered as the `webhook-id` header) for deduplication and `type` to route events. The full schema is in [Event types](/docs/webhooks/events/overview).

Webhook retry budget shrinks. Documo retries seven times over roughly twenty-seven hours (`5s, 25s, 2m, 10m, 52m, 4h, 22h`). mintfax delivers up to four attempts on an exponential backoff inside a \~18-minute window before marking the event permanently failed. If your receiver depended on the long Documo backoff, replay the event from the dashboard or refetch via the [Events API](/docs/api-reference/events), or poll `GET /faxes/{id}` as a fallback. See [Webhook delivery and retries](/docs/webhooks/delivery-and-retries) for the full schedule.

**Verify** Send a fax to `+15005550001` from the sandbox. Your endpoint receives `fax.queued` then `fax.delivered`, both with valid signatures. See [Sandbox testing](/docs/sandbox).

## Step 6: Translate Documo result codes

Documo attaches numeric "Fax Result Codes" to fax records and webhook payloads. mintfax uses string `error_code` values inside `fax.failed` events plus an HTTP error envelope on the API itself. Common translations:

| Documo code | Documo meaning            | mintfax behavior                                                                                |
| ----------- | ------------------------- | ----------------------------------------------------------------------------------------------- |
| 4100        | Fax Number Invalid        | `422 validation_failed` on submit (E.164 check)                                                 |
| 5000        | Fax Resource Issue        | `500 internal_server_error`                                                                     |
| 6100        | Fax Number Busy           | A second `fax.sending` per retry attempt, then `fax.failed` with `line_busy` if retries exhaust |
| 6200        | Fax Machine Not Answering | `fax.failed` with a no-answer `error_code`                                                      |
| 6300 / 6500 | Fax Detection Failed      | `fax.failed` with a no-fax-detected `error_code`                                                |
| 6700        | Fax Blocked by Recipient  | `fax.failed` with a recipient-blocked `error_code`                                              |
| 6800        | Fax Negotiation Failed    | `fax.failed` with a negotiation-failed `error_code`                                             |
| 8000-8400   | Fax Transmit Failed       | `fax.failed` with a transmit-failed `error_code`                                                |
| 9000        | Fax Failed (terminal)     | `fax.failed` with a generic-failure `error_code`                                                |

Switch on the `error_code` strings from `fax.failed` payloads rather than hard-coding a translation table. The HTTP error envelope (`error`, `message`, `action`, `docs`) is in [Errors](/docs/errors); use `action` for programmatic next steps.

## Account model: where Documo subaccounts do not map

Documo accounts can contain subaccounts, and subaccounts can contain subaccounts, each with their own users, fax numbers, API keys, webhooks, and history. Resellers and multi-tenant SaaS use this for per-customer isolation.

mintfax has accounts and one sandbox plus one live environment per account. No multi-level hierarchy. No per-subaccount API key.

Translation paths:

* **One Documo account, no subaccounts.** Trivial. One mintfax account.
* **Subaccounts for dev/staging/prod separation.** Use `mfx_test_` for non-production and `mfx_live_` for production. Sandbox and live are structurally separate environments under the same account.
* **Subaccounts for per-customer tenancy.** No native equivalent. Either run one mintfax account per end-customer (full isolation, including billing) or co-mingle traffic in one account with per-customer namespacing in `tags` (for example, `tags[customer_id]=...`) and reconcile in your own database. Webhooks deliver to one URL; you fan out in your code.
* **Reseller / multi-level subaccounts.** Not supported. Stay on Documo or build a tenant layer of your own on top of mintfax.

If your Documo integration relied on a child subaccount's API key for permission scoping, one mintfax key cannot replicate that. Plan for application-side enforcement before cutover.

## Cutover and rollback

Run both providers in parallel until you trust the new path. The shape that works:

1. Generate a sandbox key (`mfx_test_` prefix).
2. Move the receiver code behind a feature flag so a single deploy flips between Documo and mintfax per request.
3. Mirror traffic for a sample percentage of sends and compare resulting state (status, error code, page count) for one day.
4. Exercise the failure modes against the magic numbers: `+15005550002` (busy), `+15005550003` (no answer), `+15005550004` (transient, succeeds on retry), `+15005550005` (permanent failure), `+15005550006` (insufficient balance), `+15005550099` (validation failure). See [Sandbox testing](/docs/sandbox) for the full reference.
5. Switch reads first (list, get, image), then writes (send, resend, delete), then webhooks. Each switch is its own deploy.

Rollback is the reverse: flip the flag back to Documo and investigate. Because both integrations live behind the same flag, rollback is one config change.

## Verify

Migration is complete when:

* A sandbox fax to `+15005550001` returns a `fax_`-prefixed ID, transitions through `queued` and `submitted`, and ends in `delivered`.
* Your receiver verifies Standard Webhooks signatures on `fax.queued`, `fax.sending`, and `fax.delivered` via the reference library, deduplicates on the `webhook-id` header (same value as `event.id`), and returns 200.
* A duplicate `Idempotency-Key` returns the original response without producing a second fax record.
* An invalid recipient (bare digits, no `+`) returns `422 validation_failed` with `context.errors.to`.
* Your error handler reads `error_code` from `fax.failed` and `error` from the HTTP envelope, not Documo's numeric result codes.

## What to do next

* [Verify webhook signatures](/docs/webhooks/verify-requests) - Standard Webhooks contract and reference-library snippets beyond Node.
* [Idempotency keys](/docs/idempotency) - 24-hour replay window and key generation strategy.
* [Errors](/docs/errors) - error-code catalog with `action` hints.
* [Event types](/docs/webhooks/events/overview) - every event type, payload shape, and delivery guarantee.
* [Sandbox testing](/docs/sandbox) - magic numbers for simulated success, busy, no-answer, and validation failure.
