> ## 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 Sinch Fax v3 (or Phaxio v2.1) to mintfax

> Endpoint, auth, payload, webhook, and error mapping with curl and Node samples. Walks through what changes and what to verify before you cut over.

## Outcome

When you finish this guide, your integration sends to `https://api.mintfax.com/v1` with a Bearer key, reads snake\_case fields, signs an `Idempotency-Key` on every send, verifies Standard Webhooks signatures on every webhook, and has cleared a full fax round-trip in the sandbox. Code samples are curl and Node. Marketing positioning is at [/compare/sinch-alternative](https://mintfax.com/compare/sinch-alternative); this page is the dev-execution path.

## Prerequisites

* A mintfax environment with a sandbox API key (`mfx_test_...`). Live keys (`mfx_live_...`) come later, after the sandbox round-trip.
* A webhook endpoint configured in the mintfax dashboard. The signing secret is shown once; store it as `MINTFAX_WEBHOOK_SECRET`.
* Your existing Sinch v3 (or Phaxio v2.1) integration in source control. The cutover diff is small, but you want a clean revert.
* Node 18+ for the Node samples, or any HTTP client that speaks `multipart/form-data` for the curl samples.
* Read [Verify webhook signatures](/docs/webhooks/verify-requests), [Idempotency](/docs/idempotency), [Errors](/docs/errors), [Event types](/docs/webhooks/events/overview), and [Sandbox](/docs/sandbox) before cutover. The migration leans on each one.

## Step 1: Swap the base URL and authentication

Sinch v3 uses HTTP Basic with a project-scoped key and secret, and the project ID lives in the URL path. mintfax uses a Bearer token, infers the environment from the key prefix (`mfx_test_` for sandbox, `mfx_live_` for live), and has no project ID in the URL.

| Concern    | Sinch Fax v3                                        | mintfax                              |
| ---------- | --------------------------------------------------- | ------------------------------------ |
| Base URL   | `https://fax.api.sinch.com/v3/projects/{projectId}` | `https://api.mintfax.com/v1`         |
| Auth       | HTTP Basic (`-u {KEY_ID}:{KEY_SECRET}`)             | `Authorization: Bearer mfx_test_...` |
| Env switch | Different project IDs                               | Different key prefixes               |

<CodeGroup>
  ```bash curl theme={null}
  # Sinch v3
  curl -X POST 'https://fax.api.sinch.com/v3/projects/{PROJECT_ID}/faxes' \
    -u {KEY_ID}:{KEY_SECRET} \
    -F 'to=+15551235656' \
    -F 'file=@document.pdf'

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

  ```js node theme={null}
  // Sinch v3 (axios)
  await axios.post(
    `https://fax.api.sinch.com/v3/projects/${PROJECT_ID}/faxes`,
    form,
    { auth: { username: KEY_ID, password: KEY_SECRET } },
  );

  // mintfax (axios)
  await axios.post(
    'https://api.mintfax.com/v1/faxes',
    form,
    { headers: { Authorization: `Bearer ${process.env.MINTFAX_API_KEY}` } },
  );
  ```
</CodeGroup>

**Verify** A `GET https://api.mintfax.com/v1/account` with your sandbox key returns 200 and your account ID. A 401 means the header is missing or malformed; see [`unauthenticated`](/docs/errors#unauthenticated) and [`api_key_invalid`](/docs/errors#api-key-invalid).

## Step 2: Rename fields from camelCase to snake\_case

Phaxio v2.1 used snake\_case. Sinch v3 broke that convention. mintfax restores it. Most send-path fields map directly; a few drop or rename.

| Sinch v3 (camelCase) | mintfax (snake\_case) | Notes                                             |
| -------------------- | --------------------- | ------------------------------------------------- |
| `id`                 | `id`                  | mintfax IDs are `fax_`-prefixed                   |
| `to`                 | `to`                  | E.164 string, single recipient                    |
| `from`               | (not in send schema)  | Caller ID is environment-level, not per-fax       |
| `numberOfPages`      | `pages`               | Null until the document has been rasterized       |
| `status`             | `status`              | Different enum; see Step 4                        |
| `createTime`         | `created_at`          | ISO 8601 UTC                                      |
| `completedTime`      | `completed_at`        | ISO 8601 UTC                                      |
| `callbackUrl`        | `webhook_url`         | Per-fax override                                  |
| `maxRetries`         | `retries`             | 0-10 in mintfax, 0-5 in Sinch v3                  |
| `retryDelaySeconds`  | (not exposed)         | mintfax controls retry timing                     |
| `errorCode`          | `error_code`          | Free-form string in mintfax                       |
| `direction`          | (not in public API)   | mintfax v1 is outbound-only                       |
| `projectId`          | (not applicable)      | Environment is inferred from the key              |
| `serviceId`          | (not applicable)      | mintfax has no Services concept                   |
| `price.amount`       | (not on fax payload)  | Pricing is on the environment, not the fax record |
| `coverPageId`        | (not in v1)           | Cover-page templating is roadmap, not v1          |
| `labels`             | `tags`                | Up to 10 keys, each up to 64 chars                |

The mintfax `Fax` resource also adds fields Sinch v3 does not have: `csid`, `page_size` (`letter`/`legal`/`a4`/`b4`), `resolution` (`standard`/`fine`), `optimize_for` (`text`/`photo`), and `idempotency_key`. All are optional and fall back to your environment fax settings.

**Verify** Submit one fax, then `GET /v1/faxes/{id}`. Confirm every field your old code reads is present under its snake\_case name.

## Step 3: Add an idempotency key

Sinch v3 has no documented idempotency feature. A retried `POST /faxes` against Sinch can double-bill silently. mintfax accepts an `Idempotency-Key` header on `POST /v1/faxes` and `POST /v1/faxes/{id}/resend`; duplicate keys return the original response without re-processing, and keys expire after 24 hours.

Generate a UUID v4 per submission. Reuse the same key on retry.

<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=+15005550001" \
    -F "file=@document.pdf"
  ```

  ```js node theme={null}
  import { randomUUID } from 'crypto';
  import FormData from 'form-data';
  import fs from 'fs';
  import axios from 'axios';

  const idempotencyKey = randomUUID();
  const form = new FormData();
  form.append('to', '+15005550001');
  form.append('file', fs.createReadStream('document.pdf'));

  await axios.post('https://api.mintfax.com/v1/faxes', form, {
    headers: {
      Authorization: `Bearer ${process.env.MINTFAX_API_KEY}`,
      'Idempotency-Key': idempotencyKey,
      ...form.getHeaders(),
    },
  });
  ```
</CodeGroup>

**Verify** Submit the same request twice with the same `Idempotency-Key`. Both responses carry the same `id`. Read the [Idempotency guide](/docs/idempotency) for full semantics.

## Step 4: Translate status and retry behavior

Sinch v3 status names are uppercase; mintfax statuses are lowercase, and add one extra value (`submitted`) to distinguish "in queue" from "handed to the carrier."

| Sinch v3 status | mintfax status             |
| --------------- | -------------------------- |
| `QUEUED`        | `queued`                   |
| `IN_PROGRESS`   | `submitted`, `in_progress` |
| `COMPLETED`     | `delivered`                |
| `FAILURE`       | `failed`                   |

Retry caps differ. Sinch v3 caps at 5 retries (`maxRetries` 0-5); mintfax caps at 10 (`retries` 0-10), with a default of 3. Configure it per fax or as an environment default at `PUT /v1/environment/fax-settings`. Retry timing is controlled by mintfax, so there is no per-request `retryDelaySeconds` to map.

If you previously cancelled queued faxes, note that neither product exposes a cancel endpoint. A queued fax in mintfax runs to a terminal state (`delivered` or `failed`) before it can be deleted via `DELETE /v1/faxes/{id}`. A delete attempt against an in-flight fax returns [`fax_not_terminal`](/docs/errors#fax-not-terminal).

**Verify** Submit a fax to `+15005550004` (the transient-failure magic number) and watch the lifecycle: `fax.queued`, `fax.sending` (attempt 1), `fax.sending` (attempt 2 after the transient failure), then `fax.delivered`. `fax.sending` fires once per delivery attempt, so the repeat is how you observe mid-flight retries. See [Sandbox](/docs/sandbox).

## Step 5: Replace the v3 webhook scheme with Standard Webhooks

This is the highest-value step in the migration. Sinch v3 dropped the Phaxio v2.1 `X-Phaxio-Signature` (HMAC-SHA1) header. The v2-to-v3 guide states verbatim: *"Webhook signatures: Removed due to low usage, at the moment V3 does not support."* What replaced it is HTTP Basic credentials inside the callback URL. That mechanism leaks the credential to any reverse proxy in the path and to Sinch's own request logs, and it does not authenticate the payload.

mintfax follows the [Standard Webhooks specification](https://www.standardwebhooks.com/) and sends three headers on every delivery:

| Header              | Value                                                                                    |
| ------------------- | ---------------------------------------------------------------------------------------- |
| `webhook-id`        | Event identifier (`evt_`-prefixed). Stable across retry attempts; use it to deduplicate. |
| `webhook-timestamp` | Unix epoch seconds, re-stamped per delivery attempt.                                     |
| `webhook-signature` | One or more space-separated `v1,<base64-hmac>` tokens.                                   |

The signature is `HMAC-SHA256(secret, "{webhook-id}.{webhook-timestamp}.{body}")`, base64-encoded, emitted as `v1,<base64>`. Use the Standard Webhooks reference library for your language - it handles constant-time comparison, the 5-minute timestamp tolerance, and parsing multi-secret rotation tokens.

<CodeGroup>
  ```js 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'],
        });
        // event.type, event.id, event.data.object.id, event.data.object.status, ...
        res.sendStatus(200);
      } catch (err) {
        res.status(401).send('Invalid signature');
      }
    },
  );
  ```
</CodeGroup>

**Verify** Send a fax to `+15005550001` (success) and confirm your handler returns 200. Tamper with one byte of the body in a replayed request and confirm 401. Read the full [verify webhook signatures](/docs/webhooks/verify-requests) page for the multi-language reference and rotation guidance before you ship.

## Step 6: Adjust webhook event handling

Sinch v3 fires one `FAX_COMPLETED` event whose `fax.status` is either `COMPLETED` or `FAILURE`. mintfax fires distinct event types, plus events Sinch does not have: `fax.queued`, `fax.sending`, `fax.delivered`, `fax.failed`, `balance.low`, and `balance.topup`.

| Sinch v3 event            | mintfax event                                                                         |
| ------------------------- | ------------------------------------------------------------------------------------- |
| `FAX_COMPLETED` (success) | `fax.delivered`                                                                       |
| `FAX_COMPLETED` (failure) | `fax.failed`                                                                          |
| (no equivalent)           | `fax.queued`                                                                          |
| `inProgressNotifications` | `fax.sending` (fires once per delivery attempt - this is also the mid-attempt signal) |
| (no equivalent)           | `balance.low`, `balance.topup`                                                        |
| `INCOMING_FAX`            | (not supported; mintfax v1 is outbound-only)                                          |

Two consequences for your handler. First, code that branched on `fax.status` inside one event now branches on `event.type` at the top of the payload; a switch over `event.type` is clearer. Second, if your Sinch v3 integration consumes `INCOMING_FAX`, mintfax has no equivalent today. Treat that as a known gap, and do not migrate inbound flows yet.

The mintfax envelope is the same across all event types: `id` (`evt_`-prefixed, also delivered as the `webhook-id` header; use it for deduplication), `type` (the event name), `created` (Unix timestamp in seconds), and `data.object` (event-specific payload). The full catalog is at [Event types](/docs/webhooks/events/overview).

**Verify** Confirm your handler routes `fax.delivered` and `fax.failed` correctly. Replay protection plus `id` deduplication means you can replay any captured event safely while testing.

## Step 7: Translate error codes

Sinch v3 returns an integer `errorCode` plus a `type` field on failure (one of `DOCUMENT_CONVERSION_ERROR`, `CALL_ERROR`, `FAX_ERROR`, `FATAL_ERROR`, `GENERAL_ERROR`), and uses HTTP statuses for top-level rejections. mintfax returns a stable string `error` field in a consistent envelope with `message`, `action`, and `docs`.

| Sinch v3 (HTTP / errorCode)     | Meaning                          | mintfax error code                                                                                                                       |
| ------------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| HTTP 401                        | Authentication failure           | [`unauthenticated`](/docs/errors#unauthenticated), [`api_key_invalid`](/docs/errors#api-key-invalid), [`api_key_missing`](/docs/errors#api-key-missing) |
| HTTP 402                        | Account balance at zero          | [`insufficient_balance`](/docs/errors#insufficient-balance)                                                                                   |
| HTTP 404                        | Resource not found               | [`not_found`](/docs/errors#not-found)                                                                                                         |
| HTTP 422                        | Invalid parameters               | [`validation_failed`](/docs/errors#validation-failed), [`fax_data_purged`](/docs/errors#fax-data-purged)                                           |
| HTTP 429                        | Rate limit                       | [`rate_limit_exceeded`](/docs/errors#rate-limit-exceeded)                                                                                     |
| HTTP 500                        | Temporary server error           | [`internal_server_error`](/docs/errors#internal-server-error)                                                                                 |
| HTTP 800                        | URL fetch failure (`contentUrl`) | (no equivalent; mintfax accepts multipart only)                                                                                          |
| `128-130` DOCUMENT\_CONVERSION  | Bad / unsupported MIME           | `validation_failed` at submit time                                                                                                       |
| `17` CALL\_ERROR                | Destination busy                 | Surfaces as `fax.failed` with `error_code: line_busy`                                                                                    |
| `16` CALL\_ERROR                | Ring timeout / no answer         | Surfaces as `fax.failed` with carrier-supplied `error_code`                                                                              |
| `4` / `54` / `133` (conversion) | Document conversion failures     | Surfaces as `fax.failed`                                                                                                                 |

The per-failure-reason error catalog for the fax record itself (the `error_code` field on `fax.failed`) is intentionally compact in v1; treat unfamiliar values as opaque-but-logged. Top-level API errors (the envelope above) are stable, documented at [Errors](/docs/errors), and machine-matchable on the `error` field.

**Verify** Submit to `+15005550005` (the permanent-failure magic number) and confirm your `fax.failed` handler logs `data.error_code` and routes to your alerting path.

## Step 8: Adapt sandbox tests

Sinch v3 ships a single magic number for sandbox testing. mintfax ships per-failure-mode magic numbers so you can exercise each branch deterministically.

| Scenario             | mintfax magic number |
| -------------------- | -------------------- |
| Always succeeds      | `+15005550001`       |
| Always busy          | `+15005550002`       |
| Rings out, no pickup | `+15005550003`       |
| Fails once, then OK  | `+15005550004`       |
| Always fails         | `+15005550005`       |
| Insufficient balance | `+15005550006`       |
| Validation failure   | `+15005550099`       |

Existing test fixtures built around Sinch's single number expand here: one fixture per failure mode, deterministic, no carrier dependency. The full list and behavior are on [Sandbox](/docs/sandbox).

**Verify** Run your full integration suite against the sandbox using these numbers before any traffic moves to live keys.

## Verify

End-to-end check before you flip live traffic:

1. Send a fax to `+15005550001` with a sandbox key and an `Idempotency-Key`. The response carries `status: "queued"` and a `fax_`-prefixed `id`.
2. Your webhook endpoint receives `fax.queued`, then `fax.sending`, then `fax.delivered`. Each request passes Standard Webhooks verification via the reference library.
3. Repeat the same `POST /v1/faxes` with the same idempotency key. The response is identical; no second fax is created.
4. Send to `+15005550005` and confirm a `fax.failed` event with a populated `error_code`.
5. Replace `mfx_test_` with `mfx_live_` in your config, send one real fax to a known-good destination, and watch the same event sequence land.

## What to do next

* [Verify webhook signatures](/docs/webhooks/verify-requests) - Standard Webhooks library snippets in five languages, plus key rotation guidance.
* [Idempotency](/docs/idempotency) - retry semantics, key lifetime, and what happens on replay.
* [Errors](/docs/errors) - the stable error catalog with HTTP statuses, causes, and next actions.
* [Event types](/docs/webhooks/events/overview) - every event type, payload shape, and delivery behavior.
* [Sandbox](/docs/sandbox) - magic numbers and simulated failure scenarios.
