Programmatic Faxing: Send Faxes Automatically From Code

Programmatic faxing means faxes send themselves — triggered by an event in your system, not a human clicking Send. This guide covers the architecture: choosing triggers, queueing sends, building your own idempotency, verifying webhooks, and deciding what to retry.

Programmatic Faxing: Send Faxes Automatically From Code

By Sarah Martinez · Published June 18, 2026 · Updated June 18, 2026 · 9 min read

Quick answer: Programmatic faxing is faxing triggered by your software instead of a person. An event in your system enqueues a job, a worker POSTs the document to a fax API, and a signed webhook reports delivery. Build it on the mFax API, included with every mFax Business plan.


Most teams arrive at programmatic faxing the same way. Someone is manually re-faxing referral forms, or a nightly export gets emailed to a fax gateway and nobody knows whether it landed. The fix is not a better dashboard. It is treating fax as what it is — an unreliable external delivery channel — and wrapping it in the same patterns you already use for payments and email.

This guide is about that wrapper. Not the syntax of a single request (our send a fax via API tutorial covers that in four languages) and not the business case (see fax automation), but the design decisions that separate a fax integration that runs itself from one that pages you every week.

What Is Programmatic Faxing?

Programmatic faxing is sending or receiving faxes from code, triggered by system events rather than human action. The defining characteristic is not the API — it is the absence of a person in the loop.

ApproachTriggerConfirmationFails how
Fax machineA person walks overA printed report nobody readsSilently
Web dashboardA person uploads and clicksAn on-screen statusSilently, after they log out
Email-to-faxA person sends an emailA reply email, sometimesInto a spam folder
Programmatic faxA system eventA webhook your code handlesLoudly, into your alerting

That last column is the real upgrade. Automation is nice; knowing when delivery failed is what makes fax safe to depend on.

The Anatomy of an Automatic Fax

Every reliable programmatic fax integration is the same five-stage loop, regardless of language or provider.

1

Trigger

A business event occurs — a discharge summary is signed, a claim is approved, a nightly batch closes. Something durable records that a fax is owed.

2

Build

The document is assembled: rendered to PDF, merged with attachments, given a cover sheet, and checked for fax-readiness.

3

Send

A background worker POSTs the document to the fax API and stores the returned uuid against your own idempotency key.

4

Confirm

A signed webhook reports fax.delivered or fax.failed. Your code updates the record and, on failure, decides what happens next.

5

Reconcile

A periodic sweep catches jobs that never reached a terminal state — dropped webhooks, deploy windows, provider incidents.

Skip stage 5 and the system works fine until the day it doesn't, quietly.

Step 1: Choose the Trigger

The trigger is the highest-leverage decision in the whole integration, because it determines what happens when your process crashes halfway through.

TriggerBest forWatch out for
Transactional outboxAnything where the fax must match a committed database changeNeeds a relay process; worth it
Message queueHigh volume, multiple producersAt-least-once delivery — you must dedupe
Scheduled batchNightly reports, statements, digestsA whole day of faxes fails at once
Inbound webhookFax triggered by another SaaS (CRM, e-sign)You inherit their retry semantics
Direct user actionLow volume, human-initiatedStill enqueue — never fax inside a request handler

The pattern that survives contact with production is the transactional outbox: in the same database transaction that records the business event, insert a row into a fax_jobs table. If the transaction rolls back, no fax is owed. If it commits, the job exists and a worker will pick it up — even if the process dies one line later.

Never call the fax API from a request handler

A fax takes 30–90 seconds to deliver, and the API responds in milliseconds with 202 queued — but a network hiccup on that call inside a web request leaves you with no record of whether the fax was created. Write the job down first, send from a worker.

Step 2: Build a Document That Survives Transmission

Fax reduces everything to 1-bit black and white at roughly 204 × 196 dpi. Documents that look fine on screen degrade badly. Before your worker sends anything:

  • Render at 200 dpi or better, black on white, with no reliance on colour to convey meaning.
  • Avoid hairline rules and light grey text — both disappear entirely once rasterized.
  • Merge attachments into a single PDF. One transmission is faster and cheaper than three, and a partial multi-fax delivery is a mess to reconcile. Our free merge PDF tool does this by hand when you need a one-off.
  • Compress large scans. Fewer, cleaner pages transmit faster and fail less; try the optimize PDF tool to see the difference.
  • Normalize the recipient number to E.164 before it reaches the queue — +14155550100, not (415) 555-0100. A number rejected at send time is a job you have to reprocess.
  • Generate the cover sheet in code if the recipient requires one — and keep sensitive detail off it, since it sits face-up in a shared tray. The fax cover sheet generator shows the fields that matter.

Page count drives both cost and failure rate. Every extra page is another ~7 seconds on the line and another opportunity for the call to drop.

Step 3: Send — and Make It Safe to Retry

Here is the constraint that shapes the whole send path: the mFax API has no Idempotency-Key header, and neither do several other fax APIs. If your worker retries after a timeout, nothing on the provider side stops a second fax from going out.

So you own deduplication. The pattern is a unique key derived from the business event, written before the call:

// fax_jobs: idempotency_key TEXT UNIQUE, fax_uuid TEXT NULL, status TEXT

async function sendFaxJob(job) {
  // 1. Already sent? The uuid is the proof.
  if (job.fax_uuid) return job.fax_uuid;

  const form = new FormData();
  form.append('to', job.to);                       // E.164, e.g. +14155550100
  form.append('file', new Blob([job.pdf], { 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.status === 429) {
    throw new RetryableError(Number(res.headers.get('Retry-After') ?? 30));
  }
  if (!res.ok) {
    const { code, message } = await res.json();
    throw new FaxError(code, message);             // classified in Step 5
  }

  const fax = await res.json();                    // 202 Accepted
  // 2. Persist the uuid immediately — this is what makes the retry safe.
  await db.faxJobs.update(job.id, { fax_uuid: fax.uuid, status: fax.status });
  return fax.uuid;
}

Two details do the heavy lifting. The unique constraint on idempotency_key means two workers racing on the same event cannot both create a job. Writing fax_uuid the instant the API returns means a crash after the send is recoverable: the next attempt sees the uuid and stops.

If you crash between the POST and the write

Rare, but real. Recover by listing recent faxes — GET /v1/faxes?since=<timestamp> — and matching on recipient and time window before creating a new send. Cheaper than apologising to a recipient who got the same document twice.

Step 4: Confirm With Webhooks, Verify Before You Trust

The provider POSTs three event types to your endpoint: fax.delivered, fax.failed, and fax.received. Each arrives in a stable envelope with an id, a type, a unix created timestamp, and a data object shaped exactly like the REST fax resource.

Delivery is at-least-once, so duplicates are expected, not exceptional. Two rules cover it: verify the signature, then dedupe on the event id.

import crypto from 'crypto';

function verifyMfaxSignature(secret, header, timestampHeader, rawBody) {
  const v1 = header.split(',').find(p => p.startsWith('v1='))?.slice(3);
  const expected = crypto.createHmac('sha256', secret)
    .update(`${timestampHeader}.${rawBody}`)
    .digest('hex');

  // Reject anything older than five minutes — replay protection.
  if (Math.abs(Date.now() / 1000 - Number(timestampHeader)) > 300) return false;
  return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}

app.post('/webhooks/fax', express.raw({ type: 'application/json' }), async (req, res) => {
  const ok = verifyMfaxSignature(
    process.env.MFAX_WEBHOOK_SECRET,
    req.get('X-Zelda-Signature'),
    req.get('X-Zelda-Timestamp'),
    req.body.toString(),
  );
  if (!ok) return res.sendStatus(400);

  const event = JSON.parse(req.body.toString());

  // Accept fast, then queue. Never do slow work inside the handler.
  if (await seenEvents.add(event.id)) return res.sendStatus(200);  // duplicate
  await faxEventQueue.push(event);
  res.sendStatus(200);
});

Note the shape: verify at the edge, dedupe against a store, enqueue, return 200 immediately. A webhook handler that downloads a PDF inline will eventually time out, the provider will retry, and you will have built yourself a duplicate storm.

Then reconcile. A sweep every few minutes over jobs still in queued or sending past a sane threshold, using GET /v1/faxes?since=<timestamp>, closes out anything a dropped webhook left dangling.

Step 5: Decide What to Retry

Retrying everything is as wrong as retrying nothing. Classify by cause:

SignalCauseRetry?Policy
429 rate_limitedYou are sending too fastYesHonour Retry-After, exponential backoff with jitter
Network timeoutTransportYesBackoff — but check for an existing uuid first
failed + user_busyRecipient's line in useYesWait 5–15 minutes, cap at 3–5 attempts
failed, no fax toneWrong number typeMaybe onceThen flag for a human to verify the number
400 invalid_numberBad inputNoDead-letter and alert — repeating cannot help
402 quota_exceededPlan limit hitNoHold the queue, alert operations
403 subscription_inactiveBilling lapsedNoPage someone; every send will fail until fixed

Anything that exhausts its retries belongs in a dead-letter table with the original document reference, the last failure_reason, and the attempt history — not in a log line. Faxes usually carry documents someone is waiting for, so a weekly review of that table is part of the system, not a nice-to-have. Our fax error codes reference decodes the machine-side vocabulary behind these failures.

The one retry rule people get wrong

A partially transmitted fax that fails mid-document is retried as a whole document. Group 3 fax has no resume: if the recipient received four of six pages, they will receive all six again — worth a note in your cover sheet or your ops runbook.

Batch and Scheduled Faxing

Nightly runs are where programmatic faxing meets rate limits. Three habits keep batches healthy:

  • Bound your concurrency. A fixed worker pool sized to your provider's rate limit beats firing 5,000 requests and absorbing 429s.
  • Spread the schedule. Starting every batch at midnight means every failure happens at midnight too. Stagger by recipient or region.
  • Make the batch resumable. Each job carries its own idempotency key, so a rerun after a crash sends only what is genuinely outstanding.

For teams that want automation without a codebase, no-code connectors like Zapier and Make can trigger sends from spreadsheet rows or form submissions — covered in our fax automation guide. They are a fine starting point and a poor place to end up: you lose idempotency control and signature verification.

Observability: What to Log

Fax is a channel you cannot inspect after the fact, so log at the boundaries:

  • ✓Every state transition with the fax uuid, timestamp, and failure_reason — this is your audit trail.
  • ✓Time from trigger to delivered, not just API latency. The business cares about the former.
  • ✓Retry counts per recipient number — a number failing repeatedly is bad data, not bad luck.
  • ✓Webhook receipt gaps, so a silent provider or a broken endpoint surfaces within minutes.
  • ✓Never log the document or the API key. If you handle PHI, that rule is a compliance requirement — see the HIPAA fax API guide.

Frequently Asked Questions

Do I need a fax number to send faxes programmatically?

Not for outbound — the provider transmits over its own carrier infrastructure. You need a virtual fax number to receive, and inbound faxes then arrive as fax.received webhook events. See how to receive a fax online.

Can I send faxes programmatically without writing backend code?

Partly. No-code platforms can trigger sends, and mFax Business itself sends from web, mobile, and desktop. But your API key must never live in a browser or mobile app, so any genuinely programmatic flow needs something server-side holding the credential.

How many faxes can I send at once?

That depends on your provider's rate limit and your plan's page quota, not on your code. Size your worker pool to the limit, honour Retry-After, and treat 402 quota_exceeded as a signal to pause rather than push harder.

Is programmatic faxing HIPAA compliant?

It can be, with a signed BAA, TLS in transit, AES-256 at rest, access controls, and audit logging. mFax Business includes a signed BAA on every plan. The HIPAA fax API guide has the full checklist.

Start Sending Faxes From Code

You need three things: an organization on mFax Business (usage-based pricing from about $9/mo annually, API access on every plan), a zk_live_… key from the mFax dashboard, and a webhook endpoint that verifies signatures.

The reference lives at developers.mfax.to. Start with one send and one webhook, add idempotency before you go to production, and add reconciliation before you go to volume. For the layer underneath — what actually happens once your request is accepted — read how programmatic cloud faxing works.

Frequently Asked Questions

What is programmatic faxing?
Programmatic faxing is sending and receiving faxes from code rather than by hand — an event in your system triggers an API call, and delivery confirmation comes back as a webhook. No dashboard, no email-to-fax address, no human clicking Send. It is the automation layer built on top of a [cloud fax API](/blog/cloud-fax-api/).
How do I send a fax automatically from my application?
Emit an event when the business condition is met, enqueue a fax job, and have a background worker POST the document to your fax provider. With the mFax API that is `POST /v1/faxes` with `to` and `file`. Record the returned `uuid`, then wait for the `fax.delivered` or `fax.failed` webhook to close the loop.
How do I stop a retry from sending the same fax twice?
Generate your own idempotency key from the business event, store it with a unique constraint before you call the API, and record the returned fax `uuid` against it. If the row already has a `uuid`, skip the send. The mFax API has no `Idempotency-Key` header, so deduplication is the caller's responsibility.
Should I use webhooks or polling to track a programmatic fax?
Webhooks first, polling as a safety net. Webhooks tell you within seconds of the recipient confirming; a periodic sweep of `GET /v1/faxes?since=` catches anything a failed delivery or a deploy window dropped. Running both is normal and cheap.
Which fax failures should I retry?
Retry transient telephony failures such as `user_busy`, and back off on `429 rate_limited` using the `Retry-After` header. Never retry `invalid_number` — the number is wrong and repeating the call will not fix it. Treat `quota_exceeded` and `subscription_inactive` as operational alerts, not retries.
Home Business Pricing Fax API Blog Document Converter Company
Terms of Service Privacy Policy