> ## 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 eFax Developer / Consensus

> Map eFax Universal Outbound (XML) and eFax Enterprise API (REST) onto mintfax. Auth swap, endpoint table, error translation, Standard Webhooks signing, sandbox verification.

## Outcome

You finish this guide with your eFax integration replaced by mintfax. eFax has two developer surfaces and the migration treats them separately: **Universal Outbound** (the legacy XML-over-HTTPS-POST API branded "eFax Developer") and **Enterprise API** (the newer REST + JSON + OAuth2 product). Both shapes map onto the same mintfax endpoints, auth becomes a single Bearer token, callbacks become [Standard Webhooks](https://www.standardwebhooks.com/) signed deliveries, and you verify delivery in the sandbox before any live key gets touched.

## Which eFax interface are you on?

If your code POSTs an XML body to `https://secure.efaxdeveloper.com/EFax_WebFax.serv` with `<AccessControl><UserName>` credentials, you are on **Universal Outbound**. If your code mints OAuth2 bearer tokens via `POST /tokens`, sends a `user-id: <fax-number>` header, and calls resource-shaped paths like `GET /faxes/sent`, you are on **Enterprise API**. mintfax is the same target for both.

## Prerequisites

* A mintfax account with a sandbox API key (prefix `mfx_test_`). [Register here](https://mintfax.com/register) if you do not have one.
* Your existing eFax integration code and webhook handler.
* `curl` or Node.js 18+ available.
* For Enterprise API migrations: access to your eFax developer portal. The request schema for `POST /faxes`, the HMAC settings on inbound webhooks, the API base URL, and any error code catalog are gated behind a portal login and are not in public eFax documentation.[^portal]

[^portal]: The Enterprise API JSON request body shape, the HMAC algorithm and header names for inbound webhooks, the file size and page-count limits, and the exact base URL for `/faxes` all live behind a login on the eFax Enterprise Developer Portal. The endpoint table below names the operations that map; the field-level details on the eFax side come from your portal docs.

## Step 1: Replace your credentials

mintfax authenticates with a single Bearer token in the `Authorization` header. Drop the body-embedded `<AccessControl>` block from Universal Outbound payloads, and stop minting and refreshing OAuth2 tokens for Enterprise API. There is no token expiration on the mintfax side; rotate keys from the dashboard when you need to.

<CodeGroup>
  ```bash curl theme={null}
  # Universal Outbound (before): credentials inside the XML body
  curl -X POST https://secure.efaxdeveloper.com/EFax_WebFax.serv \
    -d "id=15555550100" \
    --data-urlencode 'xml=<?xml version="1.0"?><OutboundRequest><AccessControl><UserName>acme_user</UserName><Password>acme_password</Password></AccessControl>...</OutboundRequest>'

  # Enterprise API (before): OAuth2 bearer token plus user-id header
  curl -X POST "$EFAX_BASE_URL/faxes" \
    -H "Authorization: bearer $EFAX_TOKEN" \
    -H "user-id: 15555550100" \
    -H "Content-Type: application/json" \
    -d '{ ... }'

  # mintfax (after): single Bearer token, idempotency key, multipart upload
  curl -X POST https://api.mintfax.com/v1/faxes \
    -H "Authorization: Bearer $MINTFAX_API_KEY" \
    -H "Idempotency-Key: $(uuidgen)" \
    -F "to=+12125551234" \
    -F "file=@document.pdf"
  ```

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

  const form = new FormData();
  form.append('to', '+12125551234');
  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_API_KEY}`,
      'Idempotency-Key': crypto.randomUUID(),
    },
    body: form,
  });
  const fax = await res.json();
  ```
</CodeGroup>

**Verify** A successful submission returns HTTP 201 with `status: "queued"` and a fax `id`. A wrong key returns HTTP 401 with `error: "api_key_invalid"`.

## Step 2: Map your endpoints

Both eFax interfaces map onto resource-shaped routes under `/v1/faxes`. Universal Outbound used a single URL with the operation determined by the XML root element; Enterprise API split operations across paths. mintfax follows the same resource shape Enterprise API does, with a few additions and one removal.

| eFax operation                | Universal Outbound                                      | Enterprise API                    | mintfax                                              |
| ----------------------------- | ------------------------------------------------------- | --------------------------------- | ---------------------------------------------------- |
| Send a fax                    | `POST /EFax_WebFax.serv` with `<OutboundRequest>`       | `POST /faxes`                     | `POST /v1/faxes` (multipart)                         |
| Get fax status / metadata     | `POST /EFax_WebFax.serv` with `<OutboundStatus><DOCID>` | `GET /faxes/{fax_id}/metadata`    | `GET /v1/faxes/{id}`                                 |
| Get rendered fax image        | Returned in disposition POST or via portal              | `GET /faxes/{fax_id}/image`       | `GET /v1/faxes/{id}/image`                           |
| List sent faxes               | Not a documented endpoint                               | `GET /faxes/sent`                 | `GET /v1/faxes`                                      |
| Delete a fax                  | Not a documented endpoint                               | `DELETE /faxes/{fax_id}`          | `DELETE /v1/faxes/{id}` (terminal only)              |
| Resend a fax                  | No equivalent                                           | No equivalent                     | `POST /v1/faxes/{id}/resend`                         |
| Cancel in flight              | `Job Cancelled` status exists, mechanism not public     | Not in public Quick Start Guide   | Not supported                                        |
| Status callback / disposition | `<DispositionURL>` per call, XML POST, creds in body    | Webhook with optional HMAC[^hmac] | `POST /v1/webhooks` (Standard Webhooks: HMAC-SHA256) |

[^hmac]: The eFax Enterprise API Quick Start Guide v2.0 states "Webhook notifications can be set up to require HMAC authentication." The algorithm, header names, signing input, and replay window are not in the public Quick Start Guide. mintfax follows the [Standard Webhooks](https://www.standardwebhooks.com/) specification: HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{body}`, signed with your endpoint's secret, with a documented five-minute replay window enforced by the reference libraries.

A few mapping notes:

* **Disposition URL is per-account, not per-call.** Universal Outbound let you set `<DispositionURL>` on each send. mintfax registers webhook endpoints once via `POST /v1/webhooks` and routes every event there.
* **Status query becomes an idempotent GET.** Universal Outbound asked you to re-POST an XML body with `<DOCID>` to learn the current state. mintfax exposes state on `GET /v1/faxes/{id}`. Use it as a fallback; webhooks are the recommended path.

## Step 3: Translate status and error semantics

Universal Outbound returns a numeric `StatusCode` and a free-text `StatusDescription`. Enterprise API returns HTTP-level errors documented in the gated portal. mintfax returns one error envelope on synchronous failures and one event payload on terminal async outcomes:

```json theme={null}
{
  "error": "validation_failed",
  "message": "The request body failed validation.",
  "action": "fix_request",
  "docs": "https://mintfax.com/docs/errors/validation-failed",
  "context": {
    "errors": { "to": "Must be a valid E.164 number, e.g. +12125551234" }
  }
}
```

Match on the `error` field. It is a stable machine code; the full catalog is in the [error catalog](/docs/errors).

| eFax outcome                           | mintfax synchronous response                        |
| -------------------------------------- | --------------------------------------------------- |
| `StatusCode 1` (success, sync)         | HTTP 201 with `status: "queued"`                    |
| `StatusCode 2` + `ErrorLevel User`     | HTTP 422 `validation_failed`                        |
| `StatusCode 2` + `ErrorLevel System`   | HTTP 500 `internal_server_error`                    |
| `Unauthorized Sending Address`         | HTTP 401 `unauthenticated` or HTTP 403 `forbidden`  |
| `Invalid Fax Number`                   | HTTP 422 `validation_failed`, `context.errors.to`   |
| `No Attachment` / `Invalid Attachment` | HTTP 422 `validation_failed`, `context.errors.file` |

Terminal delivery outcomes arrive as events on your webhook endpoint, not as XML POSTs:

| eFax disposition string                          | mintfax event   | mintfax `error_code` on `fax.failed`       |
| ------------------------------------------------ | --------------- | ------------------------------------------ |
| `SENT` (StatusCode 4)                            | `fax.delivered` | n/a                                        |
| `Busy`, `Fast Busy`                              | `fax.failed`    | `line_busy`                                |
| `No Answer`, `Voice Answer`, `Not a Fax Machine` | `fax.failed`    | carrier-mapped on `data.object.error_code` |
| `Negotiation Failed`, `Exceeded ECM Retransmit`  | `fax.failed`    | carrier-mapped on `data.object.error_code` |

Two changes in your handler logic:

1. **Match on `error_code`, not the human string.** eFax's `StatusDescription` is convenient right up to the day carrier wording shifts. mintfax's `data.object.error_code` on `fax.failed` is stable. See the [event catalog](/docs/webhooks/events/overview) for the full envelope and per-event payloads.
2. **Each attempt fires its own event.** mintfax retries transient failures automatically (default three attempts, configurable per fax or via account fax settings) and fires a fresh `fax.sending` event at the start of each attempt - the repeated `fax.sending` is your in-flight retry signal. `fax.failed` is terminal and fires once after all attempts exhaust.

## Step 4: Replace credential-echoed callbacks with Standard Webhooks

This is the security swap that matters. Universal Outbound disposition POSTs are authenticated by checking that the eFax credentials echoed back inside the XML body match what you stored. No signature, no timestamp, no replay window. mintfax follows the [Standard Webhooks](https://www.standardwebhooks.com/) specification: every delivery is signed HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{body}` with your endpoint's secret, and arrives with three headers (`webhook-id`, `webhook-timestamp`, `webhook-signature`).

Register an endpoint once per environment:

```bash theme={null}
curl -X POST https://api.mintfax.com/v1/webhooks \
  -H "Authorization: Bearer $MINTFAX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://acme.example/webhooks/mintfax",
    "events": ["fax.sending", "fax.delivered", "fax.failed"]
  }'
```

mintfax returns a `signing_secret` in the response, shown once. Store it as `MINTFAX_WEBHOOK_SECRET`.

Replace your eFax disposition handler. Use the Standard Webhooks reference library for your language - it handles signature comparison, the 5-minute timestamp tolerance, and parsing multi-secret rotation tokens. Do not hand-roll the HMAC.

<CodeGroup>
  ```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'],
        });
        // event.id (also delivered as the webhook-id header) is stable across
        // retry attempts; use it for deduplication. Route by event.type.
        res.status(200).end();
      } catch {
        res.status(401).send('Invalid signature');
      }
    },
  );
  ```
</CodeGroup>

**Verify** Tamper with one byte of the request body and confirm your endpoint returns 401. Send a request whose `webhook-timestamp` is ten minutes old and confirm the reference library rejects it (the default tolerance is 5 minutes).

For the full multi-language reference, including key rotation handling, see [verify webhook signatures](/docs/webhooks/verify-requests).

## Step 5: Add idempotency keys

Universal Outbound has no idempotency primitive. `TransmissionID` is an audit identifier, not duplicate suppression: a retried POST after a network blip can send the same fax twice. The Enterprise API public Quick Start Guide does not document an idempotency key either.

mintfax accepts `Idempotency-Key` on every mutating request. Send the same key on retry and you get the same response back; the side effect (a credit hold and a fax submission) happens once.

```bash theme={null}
curl -X POST https://api.mintfax.com/v1/faxes \
  -H "Authorization: Bearer $MINTFAX_API_KEY" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -F "to=+12125551234" \
  -F "file=@document.pdf"
```

Generate a UUID once per logical operation, persist it alongside your domain record, and reuse it on every retry. Full semantics, including the 24-hour replay window and what counts as a conflict, are in [idempotency keys](/docs/idempotency).

## Step 6: Verify in the sandbox before flipping a live key

mintfax's sandbox is a structurally separate environment. Use a `mfx_test_` key and the magic recipient numbers below to drive every code path you care about without sending a real fax.

| Magic recipient | What happens                                       |
| --------------- | -------------------------------------------------- |
| `+15005550001`  | Always succeeds on first attempt                   |
| `+15005550002`  | Returns busy on every attempt                      |
| `+15005550004`  | Fails first attempt, succeeds on retry             |
| `+15005550005`  | Always fails permanently                           |
| `+15005550006`  | Returns `insufficient_balance` before send         |
| `+15005550099`  | Returns `validation_failed` (invalid number shape) |

Run through the integration once per number:

<CodeGroup>
  ```bash curl theme={null}
  # Success path
  curl -X POST https://api.mintfax.com/v1/faxes \
    -H "Authorization: Bearer $MINTFAX_TEST_KEY" \
    -H "Idempotency-Key: $(uuidgen)" \
    -F "to=+15005550001" \
    -F "file=@document.pdf"

  # Permanent failure (expect a fax.failed event with a stable error_code)
  curl -X POST https://api.mintfax.com/v1/faxes \
    -H "Authorization: Bearer $MINTFAX_TEST_KEY" \
    -H "Idempotency-Key: $(uuidgen)" \
    -F "to=+15005550005" \
    -F "file=@document.pdf"
  ```

  ```javascript Node theme={null}
  const numbers = ['+15005550001', '+15005550002', '+15005550005'];
  for (const to of numbers) {
    const form = new FormData();
    form.append('to', to);
    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_TEST_KEY}`,
        'Idempotency-Key': crypto.randomUUID(),
      },
      body: form,
    });
    console.log(to, res.status, await res.json());
  }
  ```
</CodeGroup>

**Verify** Your webhook endpoint receives `fax.queued` then `fax.delivered` for `+15005550001`, `fax.queued` then `fax.failed` with `data.object.error_code: "line_busy"` for `+15005550002`, and an HTTP 402 with `error: "insufficient_balance"` for `+15005550006`. Full coverage of magic numbers and other deterministic scenarios is in the [sandbox guide](/docs/sandbox).

## Verify

You are ready to swap the live key when:

1. The sandbox run produces the expected event for every magic number, and your handler stores the top-level `id` for deduplication.
2. Your webhook handler rejects tampered payloads (HTTP 401) and stale timestamps (HTTP 403).
3. Synchronous error paths return the documented `error` codes with the correct `context` shape, and your code matches on `error`, not on `message`.
4. Retried requests with the same `Idempotency-Key` return the same fax `id` and do not create a duplicate.
5. Your eFax `<DispositionURL>` and any Enterprise API webhook destination are pointed at the new mintfax handler, or removed once mintfax is the only sender.

## Rollback

If you need to roll back during cutover, the eFax side is unchanged: your old credentials still work, your `<DispositionURL>` still receives XML POSTs, and your OAuth2 tokens still mint. The change is in your code, not in eFax. Keep the eFax integration deployed and feature-flagged behind a runtime switch until you have a clean signal that the mintfax path is producing the events and outcomes you expect.

## What to do next

* [Verify webhook signatures](/docs/webhooks/verify-requests) - full multi-language reference for the Standard Webhooks scheme.
* [Idempotency keys](/docs/idempotency) - retry semantics and the 24-hour replay window.
* [Error catalog](/docs/errors) - every machine code with HTTP status, action, and context shape.
* [Event catalog](/docs/webhooks/events/overview) - every event type and the exact `data` payload.
* [Sandbox](/docs/sandbox) - magic numbers and deterministic failure scenarios.
