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 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 HMAC2 | POST /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 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:
- 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.
- 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 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:
# 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:
- The sandbox run produces the expected event for every magic number, and your handler stores the top-level
id for deduplication.
- Your webhook handler rejects tampered payloads (HTTP 401) and stale timestamps (HTTP 403).
- Synchronous error paths return the documented
error codes with the correct context shape, and your code matches on error, not on message.
- Retried requests with the same
Idempotency-Key return the same fax id and do not create a duplicate.
- 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