Skip to main content

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.

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 land on the same mintfax endpoints, auth becomes a single Bearer token, callbacks become Standard Webhooks 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 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.1

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.
# 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"
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 collapse 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 operationUniversal OutboundEnterprise APImintfax
Send a faxPOST /EFax_WebFax.serv with <OutboundRequest>POST /faxesPOST /v1/faxes (multipart)
Get fax status / metadataPOST /EFax_WebFax.serv with <OutboundStatus><DOCID>GET /faxes/{fax_id}/metadataGET /v1/faxes/{id}
Get rendered fax imageReturned in disposition POST or via portalGET /faxes/{fax_id}/imageGET /v1/faxes/{id}/image
List sent faxesNot a documented endpointGET /faxes/sentGET /v1/faxes
Delete a faxNot a documented endpointDELETE /faxes/{fax_id}DELETE /v1/faxes/{id} (terminal only)
Resend a faxNo equivalentNo equivalentPOST /v1/faxes/{id}/resend
Cancel in flightJob Cancelled status exists, mechanism not publicNot in public Quick Start GuideNot supported
Status callback / disposition<DispositionURL> per call, XML POST, creds in bodyWebhook with optional HMAC2POST /v1/webhooks (Standard Webhooks: HMAC-SHA256)
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:
{
  "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.
eFax outcomemintfax synchronous response
StatusCode 1 (success, sync)HTTP 201 with status: "queued"
StatusCode 2 + ErrorLevel UserHTTP 422 validation_failed
StatusCode 2 + ErrorLevel SystemHTTP 500 internal_server_error
Unauthorized Sending AddressHTTP 401 unauthenticated or HTTP 403 forbidden
Invalid Fax NumberHTTP 422 validation_failed, context.errors.to
No Attachment / Invalid AttachmentHTTP 422 validation_failed, context.errors.file
Terminal delivery outcomes arrive as events on your webhook endpoint, not as XML POSTs:
eFax disposition stringmintfax eventmintfax error_code on fax.failed
SENT (StatusCode 4)fax.deliveredn/a
Busy, Fast Busyfax.failedline_busy
No Answer, Voice Answer, Not a Fax Machinefax.failedcarrier-mapped on data.object.error_code
Negotiation Failed, Exceeded ECM Retransmitfax.failedcarrier-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 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 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:
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.
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');
    }
  },
);
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.

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

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 recipientWhat happens
+15005550001Always succeeds on first attempt
+15005550002Returns busy on every attempt
+15005550004Fails first attempt, succeeds on retry
+15005550005Always fails permanently
+15005550006Returns insufficient_balance before send
+15005550099Returns validation_failed (invalid number shape)
Run through the integration once per number:
# 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"
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.

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

Footnotes

  1. 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.
  2. 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 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.
Last modified on May 14, 2026