How to Send a Fax via API (With Code Examples)

Learn how to send a fax via API with copy-paste code examples in cURL, Node.js, Python, and PHP — using the real mFax API. We cover authentication, the send request, status webhooks, and error handling so you can send your first fax programmatically.

How to Send a Fax via API (With Code Examples)

By Sarah Martinez · Published May 14, 2026 · Updated June 8, 2026 · 11 min read

Quick Answer: To send a fax via the mFax API, make an authenticated POST request to https://developers.mfax.to/v1/faxes with two multipart/form-data fields — the recipient number (to) and your document (file). mFax converts the file and handles transmission, then notifies your app of the result through a webhook. You can send your first fax with a single cURL command — no fax machine or phone line required.


A send fax API lets your application send faxes with a simple HTTP request instead of a fax machine, modem, or phone line. You upload a document, name a recipient, and the provider's cloud infrastructure does the rest — converting your PDF to the fax format, dialing the number, and reporting back whether it went through.

This guide is the hands-on, code-first companion to our broader fax API overview. The examples below use the mFax API (documented at developers.mfax.to) and come in cURL, Node.js, Python, and PHP, plus everything around the request that production code actually needs: authentication, status tracking, delivery webhooks, and error handling. The patterns translate cleanly to other providers, and we keep the comparison honest throughout.

The Fastest Way to Test

Want to send a fax right now? Skip the SDK. The single cURL command in Step 2 sends a real fax from your terminal — perfect for confirming your credentials work before you write a line of application code.

What You Need Before Sending a Fax via API

Three things get you to your first successful send:

1

An API key

Sign up for mFax Business, open the dashboard, and create an API key at developers.mfax.to. mFax live keys start with zk_live_. Treat the key as a server-side secret — it can send faxes on your account.

2

A fax number to send from

With mFax, your sender number is tied to your organization's subscription — there's no from field to set per request. The same virtual number is your inbound line if you want to receive faxes too. Learn more in our virtual fax number guide.

3

A document to send

The mFax send endpoint takes a PDF file. PDF is the safest choice on any fax API because it preserves layout. Other providers may also accept DOC/DOCX, TIFF, JPG, and PNG, converting them server-side.

If your document isn't a PDF yet, convert it first with our free document converter, or merge several files into one PDF before sending.

How a Send-Fax API Request Works

A send-fax API call carries the same core ingredients regardless of provider:

  • Authentication — an API key in the request header proves who you are.
  • to — the recipient's fax number in E.164 format (e.g., +15417543010).
  • The document — uploaded as a file (multipart/form-data) or, on some providers, passed as a hosted URL.

How the sender number and delivery callback are handled varies. With mFax, the sender number is fixed to your subscription, and webhooks are configured once in the dashboard — so the send request itself needs only to and file. Some other providers instead accept extra per-request fields such as from, a transmission quality, or a callback_url; check each provider's reference for which apply.

Faxing is asynchronous: the API accepts your request in milliseconds, but the actual transmission takes 30–90 seconds, so you learn the final outcome later — by polling the fax or, better, via webhook — rather than in the initial response.

POST /v1/faxes HTTP/1.1
Host: developers.mfax.to
Authorization: Bearer zk_live_your_key
Content-Type: multipart/form-data; boundary=----boundary

to=+15417543010
file=@invoice.pdf

Step 1: Authenticate Your Requests

Fax APIs use one of three authentication schemes. Check your provider's docs for which applies:

SchemeHow it's sentUsed by
Bearer tokenAuthorization: Bearer zk_live_…mFax, Telnyx
Basic authAuthorization: Basic base64(key:secret)Sinch / Phaxio
OAuth 2.0 / JWTExchange client credentials for a short-lived access tokenRingCentral

mFax uses a Bearer token: attach your business API key (created in the mFax dashboard) on every request as Authorization: Bearer zk_live_xxxxxxxx. It is a server-side credential — never ship it in a browser or mobile app.

Never Hard-Code Keys

Keep your zk_live_ key in environment variables or a secrets manager — never in source control. A leaked fax key can be used to send faxes on your account, and rotating it means re-deploying everywhere it's pasted.

Step 2: Send Your First Fax

Here's the same mFax send-fax request in four languages. Each one posts a PDF to a recipient and prints the returned fax uuid and status. The mFax send endpoint is POST https://developers.mfax.to/v1/faxes, encoded as multipart/form-data with exactly two fields — to and file.

cURL

curl -X POST https://developers.mfax.to/v1/faxes \
  -H "Authorization: Bearer $MFAX_API_KEY" \
  -F "to=+15417543010" \
  -F "file=@/path/to/invoice.pdf"

Node.js

import fs from 'node:fs';

const form = new FormData();
form.append('to', '+15417543010');
form.append('file', new Blob([fs.readFileSync('invoice.pdf')]), 'invoice.pdf');

const res = await fetch('https://developers.mfax.to/v1/faxes', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.MFAX_API_KEY}` },
  body: form,
});

const fax = await res.json();
console.log('Queued fax:', fax.uuid, fax.status);

Python

import os
import requests

with open("invoice.pdf", "rb") as document:
    response = requests.post(
        "https://developers.mfax.to/v1/faxes",
        headers={"Authorization": f"Bearer {os.environ['MFAX_API_KEY']}"},
        data={"to": "+15417543010"},
        files={"file": document},
    )

fax = response.json()
print("Queued fax:", fax["uuid"], fax["status"])

PHP

<?php
$ch = curl_init('https://developers.mfax.to/v1/faxes');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('MFAX_API_KEY')],
    CURLOPT_POSTFIELDS => [
        'to'   => '+15417543010',
        'file' => new CURLFile('/path/to/invoice.pdf'),
    ],
]);

$fax = json_decode(curl_exec($ch), true);
echo "Queued fax: {$fax['uuid']} {$fax['status']}\n";

Real Provider Syntax Varies

The multipart pattern is common, but the details differ. mFax uses a Bearer zk_live_ key and multipart with just to + file, and webhooks are configured in the dashboard (not per request). Telnyx uses Bearer auth with a hosted media_url plus a required connection_id. Sinch/Phaxio uses -u 'API_KEY:API_SECRET' Basic auth with a multipart file. Always check your provider's reference — our fax API guide compares the major ones.

Step 3: Read the Response and Track Status

A successful send returns HTTP 202 Accepted and a Fax object with a uuid and an initial status of queued. The fax is accepted, not yet delivered. page_count may be 0 until mFax has rendered the document.

{
  "uuid": "9f8b2c1a-4e7d-4a3b-9c2e-1d6f7a8b9c0d",
  "to": "+15417543010",
  "status": "queued",
  "direction": "outbound",
  "page_count": 0,
  "created_at": "2026-06-05T14:30:00Z",
  "updated_at": "2026-06-05T14:30:00Z"
}

A fax moves through a predictable lifecycle. The mFax status enum is:

StatusMeaning
queuedAccepted, waiting to be processed
sendingDialing and transmitting
deliveredRecipient machine confirmed receipt
failedBusy, no answer, or transmission error (see failure_reason)

To check the current state, fetch the fax by its UUID:

curl https://developers.mfax.to/v1/faxes/<uuid> \
  -H "Authorization: Bearer $MFAX_API_KEY"

A 200 response returns the full Fax object — including status, the final page_count, and a presigned, time-limited media_url you can use to download the rendered fax PDF. (A 404 means the UUID isn't found on your organization.) You can poll GET /v1/faxes/{uuid} for the current status, but polling wastes requests and adds latency. Webhooks are the better pattern.

List and Manage Faxes

Beyond fetching one fax, the mFax API offers GET /v1/faxes to list your organization's faxes newest-first (with optional status, direction, since, and limit query params, capped at 100), and DELETE /v1/faxes/{uuid} to remove a fax. Inbound faxes show up here too — see the full reference at developers.mfax.to.

Step 4: Get Delivery Updates with Webhooks

A webhook is an HTTP request mFax sends to you when a fax finishes. You expose a public HTTPS endpoint, then register its URL — along with an optional signing secret — in the mFax dashboard, not in the send request. mFax POSTs three event types: fax.delivered, fax.failed, and fax.received (the last fires for inbound faxes arriving on your mFax number).

Each event is a WebhookEvent envelope: an event id, a type, a created unix timestamp, and a data field holding the Fax object.

{
  "id": "evt_a1b2c3d4",
  "type": "fax.delivered",
  "created": 1749134402,
  "data": {
    "uuid": "9f8b2c1a-4e7d-4a3b-9c2e-1d6f7a8b9c0d",
    "to": "+15417543010",
    "status": "delivered",
    "direction": "outbound",
    "page_count": 3,
    "media_url": "https://media.mfax.to/...presigned...",
    "created_at": "2026-06-05T14:30:00Z",
    "updated_at": "2026-06-05T14:31:02Z"
  }
}

On a fax.failed event, data.failure_reason carries the carrier reason (for example, user_busy). The media_url is a presigned, time-limited link — download it promptly or refetch the fax later for a fresh one.

A receiver in Node.js / Express that verifies the signature and dedupes looks like this. mFax signs each payload (when a signing secret is configured) with two headers: X-Zelda-Timestamp (unix seconds) and X-Zelda-Signature in the form t=<timestamp>,v1=<hex>, where the hex is HMAC-SHA256(secret, "<timestamp>.<raw_body>").

import express from 'express';
import crypto from 'node:crypto';

const app = express();
const seen = new Set(); // back this with Redis/DB in production

app.post(
  '/webhooks/fax-status',
  express.raw({ type: 'application/json' }), // need the RAW body to verify
  (req, res) => {
    const rawBody = req.body.toString('utf8');
    const timestamp = req.get('X-Zelda-Timestamp');
    const sigHeader = req.get('X-Zelda-Signature') || '';

    // Reject replays older than 5 minutes
    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
      return res.sendStatus(400);
    }

    // Recompute HMAC over "<timestamp>.<raw_body>" and compare in constant time
    const v1 = sigHeader.split(',').find((p) => p.startsWith('v1='))?.slice(3);
    const expected = crypto
      .createHmac('sha256', process.env.MFAX_WEBHOOK_SECRET)
      .update(`${timestamp}.${rawBody}`)
      .digest('hex');

    if (
      !v1 ||
      !crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
    ) {
      return res.sendStatus(401);
    }

    const event = JSON.parse(rawBody);

    // Delivery is at-least-once — dedupe on the event id
    if (seen.has(event.id)) return res.sendStatus(200);
    seen.add(event.id);

    if (event.type === 'fax.delivered') {
      // mark the fax delivered in your database
    } else if (event.type === 'fax.failed') {
      // event.data.failure_reason explains why; queue a retry or alert
    } else if (event.type === 'fax.received') {
      // an inbound fax arrived — fetch event.data.media_url
    }

    res.sendStatus(200); // ACK fast — return 2xx within a few seconds
  },
);

app.listen(3000);

Two rules keep webhook handling reliable:

  • Verify the signature. Recompute the HMAC-SHA256 over "<timestamp>.<raw_body>" with your signing secret, compare in constant time, and reject anything that doesn't match or that's older than 5 minutes. (Other providers sign differently — Telnyx, for instance, uses an Ed25519 signature.) This stops an attacker from forging "delivered" events.
  • Be idempotent. mFax delivery is at-least-once, so you may receive the same event more than once. Dedupe on the event id so you don't, for example, charge a customer twice for one delivered fax.

ACK Quickly, Process Later

Return a 200 response within a few seconds. Do heavy work (database writes, sending emails) in a background job. If your endpoint is slow, the provider assumes failure and retries — multiplying the load.

Handling Errors, Rate Limits, and Retries

Faxing fails more often than typical HTTP APIs because it depends on the recipient's analog line. Build for it.

mFax returns errors as a JSON object — { "code": "...", "message": "..." } — with these codes:

CodeHTTPMeaning
invalid_number400The to number isn't valid E.164
quota_exceeded402You're out of fax quota
subscription_inactive403Your mFax subscription isn't active
rate_limited429Too many requests — back off

Practical rules:

  • Distinguish API errors from fax failures. A 4xx like invalid_number means your request was wrong — don't retry without fixing it. A failed fax status (delivered later via fax.failed, with a failure_reason) means the transmission didn't complete (busy, no answer) — that one is worth retrying.
  • Retry transmission failures with exponential backoff. When a fax fails on a busy or unanswered line, wait and retry on a growing interval (e.g., 1, 2, 4, 8 minutes) rather than hammering immediately. Cap the attempts — 3 to 5 is typical.
  • Respect rate limits. mFax uses a per-key token bucket. On 429 you get a Retry-After header (seconds) plus RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset. Wait the indicated time, add a little jitter, and spread large batches over time instead of firing thousands of requests at once.
  • Consider idempotency for retried sends. To make sure a network timeout-and-retry doesn't fax a recipient twice, some providers accept an Idempotency-Key header. This is a general best practice — confirm support in your provider's docs before relying on it.
async function sendWithRetry(send, maxAttempts = 4) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const res = await send();

    if (res.status === 429) {
      const wait = (Number(res.headers.get('Retry-After')) || 2 ** attempt) * 1000;
      await new Promise((r) => setTimeout(r, wait + Math.random() * 500)); // jitter
      continue;
    }

    return res; // 202 Accepted (or a 4xx you should fix, not retry)
  }
  throw new Error('Rate limited after retries');
}

Preparing the Document You Send

The cleaner your input, the cleaner the fax. A few practical tips:

  • Send PDF. The mFax send endpoint takes a PDF, and PDF preserves layout everywhere. Where other providers convert other formats server-side, that conversion can shift formatting.
  • Keep it within size limits. Large scanned PDFs transmit slowly and cost more pages. If a scan is bloated, compress it first.
  • Mind resolution for small text. Fine resolution (~196 dpi) is the safe default for documents and forms; lower resolutions are faster but blur small print.
  • Combine attachments into one file. A fax is a single document — merge multiple PDFs before sending rather than firing several faxes.

Sending International Faxes via API

Sending across borders works the same way — just format the to number in full E.164 with the country code: +44 for the UK, +81 for Japan, +49 for Germany. Per-page rates are higher internationally, and some providers require you to enable international sending on your account first. See our international faxing guide for country codes and rate details.

Send-Fax API Providers at a Glance

The send request looks similar everywhere; the differences are in auth, document input, and pricing. Here's a quick orientation (verify current figures on each provider's pricing page):

ProviderAuthDocument InputPer-Page PriceBAA / HIPAA
mFaxBearer (zk_live_)Multipart PDFFrom $9/moYes — BAA included
TelnyxBearer tokenHosted media_url~$0.007/pageYes — signs BAA
Sinch / PhaxioBasic (key:secret)Multipart file~$0.045/pageYes — free BAA
NotifyreAPI keyFile upload (JSON)~$0.03/pageYes — BAA available
RingCentralOAuth 2.0 / JWTFile uploadBundled in seat planEnterprise BAA

Twilio Fax Is Retired

If you're migrating off Twilio Programmable Fax, note it was fully sunset on December 17, 2021 — both send and receive endpoints stopped working. mFax and Telnyx are the common landing spots for former Twilio Fax users.

If your faxes carry health information, your provider choice is narrower — read our companion guide on building a HIPAA-compliant fax API integration before you write any code that touches patient data. mFax Business includes a signed BAA on plans from about $9/mo.

Frequently Asked Questions

What programming languages can I use to send a fax via API?

Any language that can make an HTTPS request — the examples above cover cURL, Node.js, Python, and PHP, and the same pattern works in Ruby, Java, C#, and Go. mFax doesn't ship official SDKs; you call its REST endpoints with any HTTP client. Some other providers publish language SDKs that wrap the REST calls for you.

How fast does a fax send through an API?

The API accepts your request in well under a second (mFax returns 202 Accepted), but the actual fax transmission takes roughly 30 to 90 seconds for a few pages, depending on the recipient's line. That's why the final delivery status arrives by webhook (fax.delivered / fax.failed) rather than in the initial response.

Can I receive faxes through the same API?

Yes. mFax receives faxes on your virtual number: inbound faxes arrive via the fax.received webhook, and you can list them with GET /v1/faxes?direction=inbound. See how to receive a fax online for the full setup.

Is there a free fax API?

Most production fax APIs are paid. Some providers offer free trial credits or a sandbox that sends a limited number of test faxes; mFax bundles API access into its paid Business plans. For occasional, non-programmatic sending, a free fax service may be enough.

Get Started

You can send your first fax via the mFax API in the time it takes to copy a cURL command, paste in your zk_live_ key, and hit enter. From there, layer in dashboard-configured webhooks for delivery tracking and backoff for rate limits, and you have production-ready faxing without a single piece of hardware. Full endpoint, field, and webhook reference lives at developers.mfax.to.

For developers and teams, mFax Business includes API access (documented at developers.mfax.to) on every plan, virtual fax numbers, and HIPAA-ready features (with a signed BAA). You build your own plan — choose the exact seats and pages you need with a live calculator and pay only for what you use instead of squeezing into rigid fixed tiers — from about $9/mo. For quick, no-code sending, the mFax app lets you upload a document and fax it from your phone in under two minutes.

Frequently Asked Questions

How do I send a fax through an API?
Make an authenticated HTTP `POST` request to your provider's `/faxes` endpoint. With the mFax API you send `multipart/form-data` with just two fields — the recipient number (`to`) in E.164 and the document (`file`). The API returns a fax `uuid` and a `status` you can use to track delivery. See the [code examples](#step-2-send-your-first-fax) above for cURL, Node.js, Python, and PHP.
Can I send a fax from the command line with cURL?
Yes. A single `curl` command with your API key, the recipient number, and a file path sends a fax in seconds — no SDK required. It's the fastest way to test a fax API before writing application code.
How much does it cost to send a fax via API?
Pay-as-you-go fax APIs run roughly $0.007 to $0.05 per page, plus about $1–$2/mo for a dedicated fax number. Subscription plans with API access and HIPAA features — like mFax Business — start around $9/mo (billed annually). See our [fax API guide](/blog/fax-api/) for a full provider price comparison.
Can I send a fax via API without a fax machine or phone line?
Yes. A fax API runs entirely in the cloud — the provider operates the fax infrastructure and converts your document to the fax format, so you never need a machine, modem, or landline. You only need an internet connection and an API key.
Does Twilio still have a fax API?
No. Twilio sunset its Programmable Fax product on December 17, 2021, and no longer offers send or receive fax endpoints. Developers have migrated to alternatives like mFax, Telnyx, and Sinch.
Home Business Pricing Fax API Blog Document Converter Company
Terms of Service Privacy Policy