Fax API

Send PDFs to any fax number and receive faxes on your own number, over plain HTTPS. Delivery updates arrive as signed webhooks, busy lines are retried automatically, and documents are deleted once they are delivered.

Overview

Base URLhttps://xenfax.com/v1
FormatJSON responses; multipart or JSON requests
AuthBearer API key (live and test keys)
Statusesqueued → sending → delivered or failed
MethodPathPurpose
POST/v1/faxesSend a fax
GET/v1/faxes/{id}Current status of a fax
GET/v1/inbound-faxes/{id}Details of a received fax
GET/v1/inbound-faxes/{id}/pdfDownload a received fax
DELETE/v1/inbound-faxes/{id}Delete a received fax now
POST/v1/test/inbound-faxesTest keys only: simulate an incoming fax
GET/v1/accountYour fax number, webhook URL and limits
PATCH/v1/accountSet your webhook URL (live key)

Authentication

Send your key in the Authorization header. Keys starting xf_live_ send real faxes; keys starting xf_test_ never touch the phone network (see Test mode). Each key only sees its own faxes.

Authorization: Bearer xf_live_...

Account & webhook URL

GET /v1/account shows your dedicated fax number, caller name, webhook URL and daily limit. Set or change the webhook URL with your live key. Test and live events go to the same URL, and livemode tells them apart. The URL must be public HTTPS.

curl -X PATCH https://xenfax.com/v1/account   -H "Authorization: Bearer $XENFAX_API_KEY"   -H "Content-Type: application/json"   -d '{"webhook_url":"https://your-app.example/webhooks/xenfax"}'
{
  "object": "account",
  "name": "Acme Health",
  "contact_email": "dev@acme.example",
  "livemode": true,
  "fax_number": "+15185550100",
  "caller_name": "Acme Health",
  "webhook_url": "https://your-app.example/webhooks/xenfax",
  "webhook_secret_configured": true,
  "daily_send_cap": 500
}

Your webhook signing secret is issued once, when your account is created, and is never shown by the API.

Send a fax

POST /v1/faxes. Provide the document either as an upload or as a link we download once.

FieldRequiredDescription
toyesRecipient fax number in E.164 format, e.g. +14155550123.
fileone ofThe PDF, as a multipart upload (up to ~4 MB).
file_urlone ofHTTPS link to the PDF (up to 20 MB). Downloaded once, immediately; a link valid for 5 minutes is enough.
referenceyesYour own ID for this fax (e.g. an order ID), 1–100 chars of letters, digits and . _ : -. This is the only label we store; never put personal details here.
idempotency_keyyesUnique per fax, 8–200 chars. Repeating a request with the same key returns the original fax instead of sending again. Can also be sent as an Idempotency-Key header.

Upload

curl https://xenfax.com/v1/faxes \
  -H "Authorization: Bearer $XENFAX_API_KEY" \
  -F to=+14155550123 \
  -F reference=ORD-10293 \
  -F idempotency_key=ORD-10293-rx-1 \
  -F file=@prescription.pdf

Link

curl https://xenfax.com/v1/faxes \
  -H "Authorization: Bearer $XENFAX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"+14155550123","reference":"ORD-10293",
       "idempotency_key":"ORD-10293-rx-1",
       "file_url":"https://storage.googleapis.com/...signed..."}'

Response · 201 Created

{
  "id": "fax_3kQ9d0w1ZQ2m6n3pX8aL1g",
  "object": "fax",
  "livemode": true,
  "reference": "ORD-10293",
  "to": "+14155550123",
  "from": "+15185550100",
  "status": "queued",
  "attempts": 0,
  "max_attempts": 3,
  "next_attempt_at": null,
  "page_count": 2,
  "failure_reason": null,
  "failure_message": null,
  "file_deleted": false,
  "created_at": "2026-11-08T17:02:11.204Z",
  "queued_at": "2026-11-08T17:02:11.391Z",
  "sending_at": null,
  "delivered_at": null,
  "failed_at": null,
  "updated_at": "2026-11-08T17:02:11.391Z"
}

A repeat with the same idempotency_key returns 200 with the same fax and an Idempotent-Replayed: true header. Reusing a key with a different to or reference returns 409. If a request fails validation, the key is not used up: fix the request and retry with the same key.

Get fax status

GET /v1/faxes/{id} returns the same object as above. Use it to reconcile if you think you missed a webhook.

curl https://xenfax.com/v1/faxes/fax_3kQ9d0w1ZQ2m6n3pX8aL1g -H "Authorization: Bearer $XENFAX_API_KEY"

Webhooks

We POST JSON to your webhook URL when something changes:

TypeWhen
fax.sendingAn attempt started dialling (sent once per attempt).
fax.deliveredThe receiving machine confirmed the fax. Includes page_count and delivered_at.
fax.failedFinal failure, after any retries. Includes failure_reason, failure_message and failed_at.
fax.receivedA fax arrived on your number. Includes download_url.
POST https://your-app.example/webhooks/xenfax
Content-Type: application/json
Xenfax-Event-Id: evt_x1Y2...
Xenfax-Event-Type: fax.delivered
Xenfax-Signature: t=1762621400,v1=5f2b...e9

{
  "id": "evt_x1Y2...",
  "object": "event",
  "type": "fax.delivered",
  "livemode": true,
  "created_at": "2026-11-08T17:03:20.118Z",
  "data": { ...fax object, as returned by GET /v1/faxes/{id}... }
}

Verifying the signature

v1 is the hex HMAC-SHA256 of `${t}.${rawBody}` using your webhook secret. Compare it in constant time and reject timestamps more than 5 minutes old.

import crypto from 'crypto';

export function verifyXenfax(rawBody: string, header: string, secret: string) {
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
  const given = Buffer.from(parts.v1 ?? '', 'hex');
  return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}

Delivery guarantees

  • Respond with any 2xx within 10 seconds. Anything else is retried after 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h and 24 h.
  • Delivery is at least once, so you may see the same event twice: dedupe on id (also in Xenfax-Event-Id).
  • Events can arrive out of order (for example, a retried fax.sending after fax.delivered). Treat delivered and failed as final, and use GET /v1/faxes/{id} if unsure.

Retries

If the line is busy, doesn't answer, or the call drops, we try again automatically: 3 attempts over about 30 minutes (immediately, about 10 minutes later, then about 20 minutes after that). Between attempts the fax shows status: "queued" with attempts, the last failure_reason, and next_attempt_at. Numbers that are invalid, not in service, or voice lines fail at once with no retry. After the final attempt the fax becomes failed and you get fax.failed.

failure_reasonRetried?Meaning
user_busyyesLine busy
receiver_no_answeryesNo answer
receiver_no_responseyesNo fax tone
receiver_call_droppedyesCall dropped mid-transmission
receiver_incompatible_destinationnoNot a fax machine (voice line)
receiver_unallocated_numbernoNumber not in service
destination_invalidnoInvalid number

Receiving faxes

Your account has a dedicated fax number. Outgoing faxes are sent from it, with your business name as the caller name, and faxes sent to it trigger fax.received:

{
  "type": "fax.received",
  "data": {
    "id": "infax_8Hk2...",
    "object": "inbound_fax",
    "livemode": true,
    "from": "+15185550142",
    "to": "+15185550100",
    "page_count": 1,
    "received_at": "2026-11-08T18:11:04.002Z",
    "file_available": true,
    "download_url": "https://xenfax.com/v1/inbound-faxes/infax_8Hk2.../pdf",
    "file_expires_at": "2026-11-15T18:11:04.002Z"
  }
}

Download with your API key (the link alone is not enough):

curl -o fax.pdf "$DOWNLOAD_URL" -H "Authorization: Bearer $XENFAX_API_KEY"

The file is deleted 24 hours after the first download, or 7 days after it arrived, whichever comes first. Call DELETE /v1/inbound-faxes/{id} to delete it as soon as you have saved it. After deletion the download returns 410.

Test mode

Requests made with an xf_test_ key go through the same validation, idempotency, webhooks and retry logic, but no fax is ever dialled. Test faxes finish within seconds, test retries wait about 20 seconds instead of minutes, and webhooks carry "livemode": false.

Send toSimulated result
+15005550001Delivered on the first attempt (any other number behaves the same)
+15005550002Busy on every attempt, then failed with user_busy after 3 attempts
+15005550003Busy on attempt 1, delivered on attempt 2
+15005550004No answer on every attempt, then failed with receiver_no_answer
+15005550005Voice line: failed immediately, no retry

To simulate a pharmacy faxing you back:

curl -X POST https://xenfax.com/v1/test/inbound-faxes \
  -H "Authorization: Bearer $XENFAX_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"from":"+15185550142","pages":2}'

Data handling

  • Outgoing documents are deleted from XenFax storage as soon as a fax is delivered or finally fails, and never kept longer than 7 days.
  • Our carrier is told not to store media, and we delete the carrier's fax record after each attempt and after each inbound fax is collected.
  • Received faxes are deleted 24 hours after first download or 7 days after arrival, or straight away on DELETE.
  • The only label stored for a fax is your reference. Our logs contain fax IDs and references only: no document content, no file links.
  • Files are kept in private storage and are only served to your API key over HTTPS.

Errors

{ "error": { "code": "invalid_to", "message": "`to` must be an E.164 fax number, e.g. +14155550123." } }
HTTPcodeMeaning
401unauthorizedMissing or invalid API key
403live_key_required, test_mode_onlyEndpoint needs the other key type
409idempotency_key_reusedKey already used with different parameters
410file_deletedInbound fax file deleted under the retention policy
415unsupported_media_typeUse multipart/form-data or application/json
422invalid_webhook_urlwebhook_url must be public HTTPS
422invalid_to, invalid_reference, invalid_idempotency_keyBad field
422invalid_file, file_too_large, too_many_pagesPDF problem (max 20 MB, 100 pages)
422invalid_file_url, file_download_failedfile_url not public HTTPS, expired, or unreachable
429daily_cap_reachedDaily send limit reached (contact us to raise it)
500internal_errorSafe to retry with the same idempotency_key

Get API access

Tell us your expected monthly volume and whether you need a dedicated number. We'll send live and test keys.

Email hello@xenfax.com