Online Fax API: Send & Receive Faxes From Your App (2026)

An online fax API lets your application send and receive faxes over the internet with a single REST call — no fax machine, no phone line, no on-premise server. This guide covers the full integration: outbound sends, inbound webhooks, receiving PDFs, and real-world patterns for web apps, EHRs, and CRMs.

Online Fax API: Send & Receive Faxes From Your App (2026)

By Michael Chen · Published June 25, 2026 · Updated June 25, 2026 · 11 min read

Quick answer: An online fax API lets you send and receive faxes from any internet-connected app via standard HTTP requests. Sign up for mFax Business, get an API key at developers.mfax.to, and you can send a fax in one POST call — no fax machine, no phone line, no on-premise server.


You have an app. Somewhere in the workflow — a healthcare portal, a legal platform, an insurance back-office — faxes are still required. The fastest way to handle them isn't a physical fax machine or a shared email inbox. It's an online fax API: a REST interface that turns fax send/receive into ordinary HTTP calls your backend can make like any other external service.

This guide covers the full picture: what an online fax API actually is, how to send, how to receive inbound faxes (the part most tutorials skip), and how to wire faxing into a real web app, EHR, or CRM. The mFax API is used for all examples — the same patterns apply to any REST fax provider.

Already know the basics?

If you've sent faxes via API before and want to skip straight to receiving faxes and integration patterns, jump to Step 3: Receive Inbound Faxes and Step 4: Real Integration Patterns.

What Is an Online Fax API?

An online fax API (also called a cloud fax API or internet fax API) is a hosted REST service that converts your HTTP requests into fax transmissions — and converts incoming faxes into webhook events your code can process.

Instead of maintaining a fax server connected to a phone line, you call an endpoint:

POST https://developers.mfax.to/v1/faxes
Authorization: Bearer zk_live_…
Content-Type: multipart/form-data

to:   +14155550100
file: [your PDF]

The provider takes it from there: queues the job, converts your PDF to T.38 or G3 fax protocol, transmits it over the PSTN or VoIP, and fires a fax.delivered webhook when the recipient confirms receipt.

The key difference from a manual online fax dashboard: the API is programmable. Your code decides when to send, what to send, and what to do when something arrives — no human in the loop.

Why "online" matters

"Online fax" distinguishes this approach from two older alternatives:

MethodHow it worksWhy teams move away
Physical fax machineAnalog line, thermal paperNo audit trail, no automation, maintenance overhead
On-premise fax serverA server inside your network with a phone cardCapital cost, IT maintenance, single point of failure
Online fax APIREST calls to a hosted providerNo hardware, scales with demand, built-in delivery receipts

Healthcare organizations still exchange over 9 billion fax pages per year in the United States, and 70% of clinic-to-clinic communication flows through fax. An online fax API lets software handle that volume automatically.


Step 1: Get Your API Key and Fax Number

1

Create an mFax Business account

Go to mFax.to/business and sign up. Plans start from about $9/mo (annual billing) and include API access on every tier. When your organization is created at app.mfax.to, a virtual fax number is assigned automatically — this is the number that receives inbound faxes.

2

Generate an API key

In the mFax dashboard, navigate to Developer → API Keys and create a new key. Keys look like zk_live_…. Treat this like a database password: store it in an environment variable (MFAX_API_KEY), never in source code.

3

Note your fax number

Find your assigned virtual number under Settings → Fax Numbers. You'll need it when configuring inbound routing and when sharing your fax address with senders. The format is E.164: +1XXXXXXXXXX.

Never expose your API key in frontend code

Your fax API key grants full account access — send faxes, read inbound PDFs, manage settings. It must live only on your server. Frontend JavaScript calling the API directly will expose the key to anyone who views source.


Step 2: Send a Fax

Sending requires exactly two form fields: to (recipient in E.164 format) and file (the PDF document).

cURL

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

Node.js (fetch)

import { readFileSync } from 'fs';

async function sendFax(to, pdfPath) {
  const form = new FormData();
  form.append('to', to);
  form.append('file', new Blob([readFileSync(pdfPath)], { type: 'application/pdf' }), 'document.pdf');

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

  if (!res.ok) throw new Error(`Fax send failed: ${res.status}`);
  return res.json(); // { id, status: 'queued', to, pages, created_at }
}

Python

import os, requests

def send_fax(to: str, pdf_path: str) -> dict:
    with open(pdf_path, 'rb') as f:
        response = requests.post(
            'https://developers.mfax.to/v1/faxes',
            headers={'Authorization': f"Bearer {os.environ['MFAX_API_KEY']}"},
            data={'to': to},
            files={'file': ('document.pdf', f, 'application/pdf')},
        )
    response.raise_for_status()
    return response.json()

Go

package main

import (
    "bytes"
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "os"
    "path/filepath"
)

func sendFax(to, pdfPath string) error {
    f, err := os.Open(pdfPath)
    if err != nil {
        return err
    }
    defer f.Close()

    var buf bytes.Buffer
    w := multipart.NewWriter(&buf)
    _ = w.WriteField("to", to)
    fw, _ := w.CreateFormFile("file", filepath.Base(pdfPath))
    io.Copy(fw, f)
    w.Close()

    req, _ := http.NewRequest("POST", "https://developers.mfax.to/v1/faxes", &buf)
    req.Header.Set("Authorization", "Bearer "+os.Getenv("MFAX_API_KEY"))
    req.Header.Set("Content-Type", w.FormDataContentType())

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    if resp.StatusCode != 202 {
        return fmt.Errorf("unexpected status: %d", resp.StatusCode)
    }
    return nil
}

The API responds with HTTP 202 and a fax object:

{
  "id": "fax_01HXYZ…",
  "status": "queued",
  "to": "+14155550100",
  "pages": 3,
  "created_at": "2026-06-25T10:30:00Z"
}

Track delivery by polling GET /v1/faxes/{id} or — better — listening for the fax.delivered webhook (covered next).

Status lifecycle:

StatusMeaning
queuedJob accepted, waiting for a transmission slot
sendingActively dialing and transmitting
deliveredRecipient confirmed receipt
failedCould not deliver; check failure_reason (e.g. user_busy, no_answer)

Step 3: Receive Inbound Faxes — The Part Most Tutorials Skip

Sending is well-documented. Receiving is where most integrations get stuck. Here's the full inbound pipeline.

How inbound faxes work

When someone sends a fax to your virtual number, the provider:

  1. Receives the analog transmission
  2. Converts it to a PDF
  3. Stores the PDF on secure, time-limited storage
  4. POSTs a fax.received webhook event to your configured URL

The webhook fires within seconds of the fax completing. Your endpoint has a short window — typically 24–72 hours — to download the PDF before the storage URL expires.

Configure your webhook URL

In the mFax dashboard: Developer → Webhooks → Add Endpoint. Set the URL to your server endpoint (e.g. https://yourapp.com/webhooks/fax). Enable the fax.received event. Optionally add a signing secret for signature verification.

The fax.received payload

{
  "id": "evt_01HXYZ…",
  "type": "fax.received",
  "created": "2026-06-25T11:05:42Z",
  "data": {
    "id": "fax_01HABC…",
    "status": "received",
    "from": "+14085551234",
    "to": "+14155550100",
    "pages": 2,
    "media_url": "https://storage.mfax.to/faxes/fax_01HABC…/document.pdf?token=…",
    "media_url_expires_at": "2026-06-26T11:05:42Z",
    "received_at": "2026-06-25T11:05:38Z"
  }
}

Key fields:

  • from — sender's fax number (use for routing)
  • media_url — time-limited URL to download the PDF; download immediately
  • media_url_expires_at — when the URL stops working; never rely on fetching it later

Handle the webhook in Node.js

import express from 'express';
import crypto from 'crypto';
import { writeFileSync } from 'fs';

const app = express();
app.use(express.json());

const SIGNING_SECRET = process.env.MFAX_WEBHOOK_SECRET;
const processed = new Set(); // use Redis or a DB in production

app.post('/webhooks/fax', async (req, res) => {
  // 1. Verify signature
  const sig = req.headers['x-zelda-signature']; // t=…,v1=…
  const timestamp = req.headers['x-zelda-timestamp'];
  const payload = `${timestamp}.${JSON.stringify(req.body)}`;
  const expected = 'v1=' + crypto
    .createHmac('sha256', SIGNING_SECRET)
    .update(payload)
    .digest('hex');
  const received = sig.split(',').find(p => p.startsWith('v1='));
  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
    return res.status(401).send('Invalid signature');
  }

  // 2. Deduplicate (webhooks deliver at-least-once)
  const eventId = req.body.id;
  if (processed.has(eventId)) return res.status(200).send('duplicate');
  processed.add(eventId);

  // 3. Respond quickly, process async
  res.status(200).send('ok');

  if (req.body.type === 'fax.received') {
    await handleInboundFax(req.body.data);
  }
});

async function handleInboundFax(fax) {
  // Download the PDF before the URL expires
  const resp = await fetch(fax.media_url);
  const buffer = await resp.arrayBuffer();

  // Store durably — S3, GCS, your DB blob, etc.
  const filename = `fax_${fax.id}_${Date.now()}.pdf`;
  writeFileSync(`/storage/faxes/${filename}`, Buffer.from(buffer));

  // Route based on sender or your business logic
  await routeFax({ from: fax.from, to: fax.to, filename, pages: fax.pages });
}

Download immediately — URLs expire

The media_url in a fax.received event expires within 24–72 hours. If your webhook handler fails or your server is down, you may lose access to the document. Always download and persist the PDF as the first action inside your handler — before any other processing.

Polling as a webhook fallback

If your environment can't receive webhooks (local dev, firewall-restricted network, batch jobs), poll the list endpoint instead:

# Get all faxes received in the last hour
curl "https://developers.mfax.to/v1/faxes?direction=inbound&since=2026-06-25T10:00:00Z" \
  -H "Authorization: Bearer $MFAX_API_KEY"

Polling is fine for low-volume or non-real-time workflows. For anything that needs near-instant processing — a patient intake form, a legal filing confirmation — use webhooks.


Step 4: Real Integration Patterns

Pattern A: Web Form → Backend → Fax

The most common pattern. A user fills out a form in your web UI; your backend sends it as a fax. Never call the fax API from the browser directly.

Browser (form submit)
  → POST /api/send-fax  (your backend)
      → validates input
      → generates/fetches the PDF
      → POST /v1/faxes  (mFax API)
      → returns { jobId } to browser
  → browser polls GET /api/fax-status/:jobId
// Your Express backend route
app.post('/api/send-fax', upload.single('file'), async (req, res) => {
  const { recipientFax } = req.body;
  const pdfBuffer = req.file.buffer;

  const form = new FormData();
  form.append('to', recipientFax);
  form.append('file', new Blob([pdfBuffer], { type: 'application/pdf' }), 'document.pdf');

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

Pattern B: CRM Trigger → Fax Send

When a deal closes in your CRM (Salesforce, HubSpot), automatically fax the contract. Use your CRM's webhook or automation rules to call your backend, which then calls the fax API.

// Receive a CRM webhook (e.g. deal status changed to "closed-won")
app.post('/crm/webhooks/deal-closed', async (req, res) => {
  res.status(200).send('ok'); // respond fast

  const { contactFaxNumber, contractPdfUrl } = req.body;

  // Fetch the contract PDF from your document storage
  const pdfResponse = await fetch(contractPdfUrl);
  const pdfBuffer = await pdfResponse.arrayBuffer();

  // Send via mFax API
  await sendFax(contactFaxNumber, Buffer.from(pdfBuffer));
});

Pattern C: Serverless Inbound Fax Processor

Receive a fax webhook in an AWS Lambda function, download the PDF, and email it to the right team member based on the sender's number.

// Lambda handler (Node.js)
export const handler = async (event) => {
  const body = JSON.parse(event.body);
  if (body.type !== 'fax.received') return { statusCode: 200 };

  const fax = body.data;
  const pdfResp = await fetch(fax.media_url);
  const pdfBuffer = await pdfResp.arrayBuffer();

  // Upload to S3
  await s3Client.send(new PutObjectCommand({
    Bucket: process.env.FAX_BUCKET,
    Key: `inbound/${fax.id}.pdf`,
    Body: Buffer.from(pdfBuffer),
    ContentType: 'application/pdf',
  }));

  // Route by sender number to the right inbox
  const recipient = routingTable[fax.from] ?? process.env.DEFAULT_FAX_EMAIL;
  await sesClient.send(new SendEmailCommand({
    Destination: { ToAddresses: [recipient] },
    Message: {
      Subject: { Data: `Incoming fax from ${fax.from} (${fax.pages} pages)` },
      Body: { Text: { Data: `PDF saved to s3://${process.env.FAX_BUCKET}/inbound/${fax.id}.pdf` } },
    },
    Source: 'fax-router@yourcompany.com',
  }));

  return { statusCode: 200, body: 'ok' };
};

Local Development and Testing

You can't test a webhook endpoint that isn't on the public internet. Use ngrok (or Cloudflare Tunnel) to expose your local server:

# Terminal 1 — start your local server
node server.js  # listens on :3000

# Terminal 2 — expose it publicly
ngrok http 3000
# Output: Forwarding https://abc123.ngrok-free.app → localhost:3000

Set https://abc123.ngrok-free.app/webhooks/fax as your webhook URL in the mFax dashboard, then send a test fax to your virtual number. The webhook fires on your local machine.

Mock webhook payloads for unit tests

Don't send real faxes in automated tests. Instead, build a fixture from a real event and call your handler function directly:

// test/webhooks.test.js
import { handleInboundFax } from '../src/fax-handler.js';

const MOCK_FAX_RECEIVED = {
  type: 'fax.received',
  data: {
    id: 'fax_TEST001',
    from: '+14085551234',
    to: '+14155550100',
    pages: 2,
    media_url: 'https://example.com/test-fax.pdf',
    media_url_expires_at: '2099-01-01T00:00:00Z',
    received_at: '2026-06-25T11:00:00Z',
  },
};

test('routes fax to correct inbox', async () => {
  const routed = await handleInboundFax(MOCK_FAX_RECEIVED.data);
  expect(routed.recipient).toBe('billing@yourcompany.com');
});

Tip: Use a separate test API key

Create a second API key in the mFax dashboard labeled "test" and use it only in development. If it's accidentally logged or leaked, you can revoke it without affecting production.


Common Use Cases

🏥

Healthcare & EHR

Automatically fax referrals, prior authorization forms, and lab results from your EHR when a provider approves them — no manual printing. Receive inbound results and route them to the patient record.

⚖️

Legal Platforms

Trigger a fax send when a contract is signed or a court deadline fires. Receive signed documents back via inbound webhook and attach them to the case file automatically.

💼

Insurance & Finance

Send claims, policy documents, and compliance filings to carriers that still require fax. Receive confirmations and auto-file them with an audit timestamp.

🏛️

Government & IRS

Submit IRS forms, state filings, and federal applications that accept fax. Useful in healthcare, real estate, and non-profit sectors where agencies have set fax-only receipt windows.


Generating Fax-Ready PDFs Programmatically

Many integrations need to generate the PDF dynamically — filling a template with patient data, contract fields, or form values — before faxing. Two common approaches:

Puppeteer (headless Chrome):

import puppeteer from 'puppeteer';

async function generateFaxPdf(html) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.setContent(html, { waitUntil: 'networkidle0' });
  const pdf = await page.pdf({
    format: 'Letter',       // North American fax standard
    printBackground: true,
    margin: { top: '0.5in', bottom: '0.5in', left: '0.5in', right: '0.5in' },
  });
  await browser.close();
  return pdf; // Buffer — pass directly to FormData
}

Tips for fax-compatible PDFs:

  • Use Letter or A4 page size — fax machines default to these
  • Keep resolution at 200–300 DPI — higher doesn't improve fax quality and increases file size
  • Black and white content transmits faster and more reliably than color
  • Target under 5 MB per document; use Optimize PDF to compress if needed

Online Fax API Providers Compared

ProviderAuthSend endpointInbound webhooksHIPAA/BAAPrice (approx.)
mFaxBearer zk_live_…POST /v1/faxesfax.received✅ Signed BAAFrom ~$9/mo (annual)
TelnyxBearerPOST /v2/faxesfax.received✅ BAA available~$0.007/page + number
Sinch/PhaxioBasic (key/secret)POST /phaxio/faxesfax_completed✅ BAA available~$0.007/page
Fax.PlusOAuth 2.0RESTfax_sent, fax_received✅ BAA availableFrom $4.99/mo
NotifyreBearerRESTYes✅ BAA availablePay-per-page

Twilio Programmable Fax is retired

Twilio sunset its fax product on December 17, 2021. If your codebase references api.twilio.com/…/Fax, you need to migrate. The mFax API is a drop-in replacement with an equivalent request structure.

For a deeper comparison including pricing tiers and feature differences, see Best Fax API Providers.


Idempotency: Prevent Duplicate Fax Sends

Network timeouts can cause your code to retry a send that already succeeded. The result: the same fax delivered twice. Protect against this with an idempotency key — a unique identifier you generate per logical send operation:

async function sendFaxIdempotent(to, pdfBuffer, idempotencyKey) {
  const form = new FormData();
  form.append('to', to);
  form.append('file', new Blob([pdfBuffer]), 'document.pdf');

  // Check if we've already sent this (store keys in Redis or DB)
  const existing = await redis.get(`fax:idem:${idempotencyKey}`);
  if (existing) return JSON.parse(existing);

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

  // Store result with a TTL longer than your retry window
  await redis.set(`fax:idem:${idempotencyKey}`, JSON.stringify(result), 'EX', 86400);
  return result;
}

// Usage: use a stable key derived from the business event
const key = `contract-${contractId}-${recipientFax}`;
await sendFaxIdempotent(recipientFax, pdfBuffer, key);

Generate idempotency keys from stable business identifiers — a contract ID, an order number, a form submission ID. That way, even if your server restarts mid-request, retrying with the same key returns the original result without sending twice.


Error Handling Reference

HTTP StatusCodeMeaningAction
400invalid_numberRecipient number invalid or unroutableValidate E.164 format before sending
402quota_exceededMonthly page limit reachedUpgrade plan or wait for reset
403subscription_inactiveAccount or plan suspendedCheck billing at app.mfax.to
429rate_limitedToo many requests per secondHonor Retry-After header; use exponential backoff

On 429, always check the Retry-After response header for the exact wait time before retrying. For all other transient errors (5xx), retry with exponential backoff: wait 1 s, then 2 s, then 4 s — up to 3 retries.


Get Started With the mFax API

mFax Business gives you API access on every plan — from a single-developer setup at about $9/mo (annual) to multi-seat teams with thousands of pages per month. Plans are usage-based: $3 per seat + $4 per 100 pages/mo, with a 20% discount on annual billing. A signed BAA is included on every plan.

Read the full reference at developers.mfax.to and create your first API key in the mFax dashboard.

Related guides:

  • Fax API overview: concepts and providers
  • How to send a fax via API with code examples in 4 languages
  • HIPAA-compliant fax API for health apps

Frequently Asked Questions

What is an online fax API?
An online fax API is a set of REST endpoints that lets your application send and receive faxes over the internet — no fax machine or phone line required. You authenticate with a Bearer token, POST a document to the send endpoint, and receive delivery confirmations via webhooks. The [mFax API](/blog/fax-api/) at developers.mfax.to is a fully-hosted example: one POST request sends a fax, and a `fax.received` webhook fires when inbound faxes arrive.
How do I receive faxes with an API?
Configure a webhook URL in your API provider's dashboard. When a fax arrives on your virtual fax number, the provider POSTs a `fax.received` event to your endpoint containing the sender number, page count, and a time-limited `media_url` to download the PDF. Download and store the file immediately — most providers expire the URL within 24–72 hours. You can also poll `GET /v1/faxes?direction=inbound` as a backup when webhooks are unavailable.
Can I use an online fax API in a web app?
Yes, but your API key must stay server-side. The correct pattern is: browser sends a form to your backend, your backend calls the fax API with its secret key, and returns a job ID to the browser for status polling. Never embed a fax API key in frontend JavaScript — treat it like a database password.
Do I need a dedicated fax number to use the API?
Yes, for receiving faxes you need a virtual fax number assigned to your account. You can get one through your provider's dashboard (mFax Business assigns one automatically). For outbound-only integrations, sending works without a dedicated number on some providers, but a number is required to receive a delivery confirmation callback that identifies the sender.
Is an online fax API HIPAA compliant?
A fax API can be HIPAA compliant if the provider signs a Business Associate Agreement (BAA) and transmits data with TLS 1.2+ in transit and AES-256 at rest. mFax Business includes a signed BAA on every plan. See our [HIPAA fax API guide](/blog/hipaa-fax-api/) for the full compliance checklist.
Home Business Pricing Fax API Blog Document Converter Company
Terms of Service Privacy Policy