API reference

Send a fax with one POST.

A REST API for sending faxes, checking what happened to them, and pulling the document back down. No SDK to install and nothing to model — if your language can make an HTTPS request, you have a fax integration. Included on Team and Scale.

Authentication

Create a key under Organization settings → API. It is shown once and stored only as a hash, so if you lose it, make another. Send it as a bearer token on every request.

curl https://my.signalfax.com/api/v1/faxes \
  -H "Authorization: Bearer sfx_your_key_here"

Keys carry scopes: fax:send to send, fax:readto list and read. A key only ever sees its own organisation’s faxes. The plan is checked on every call, so a key stops working if the account moves to Starter and starts working again on upgrade — you don’t have to reissue it.

Send a fax

POSThttps://my.signalfax.com/api/v1/faxes

Send the PDF as base64, as a URL we fetch, or as a multipart file part — whichever is least work at your end.

curl -X POST https://my.signalfax.com/api/v1/faxes \
  -H "Authorization: Bearer $SIGNAL_FAX_KEY" \
  -F "to=9015550123" \
  -F "subject=Referral — Chen" \
  -F "file=@referral.pdf"
curl -X POST https://my.signalfax.com/api/v1/faxes \
  -H "Authorization: Bearer $SIGNAL_FAX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": ["9015550123", "9015550124"],
    "document_url": "https://files.example.com/referral.pdf",
    "file_name": "referral.pdf",
    "subject": "Referral — Chen"
  }'
FieldTypeNotes
tostring | arrayRequired. One fax number, or up to 50 of them. US and Canadian numbers, any format — 9015550123, (901) 555-0123 and +19015550123 are all accepted.
documentstringThe PDF, base64-encoded. Use this, or document_url, or a multipart file part.
document_urlstringA URL we fetch the PDF from. It has to be reachable from our network without credentials.
file_namestringWhat to call the document in the dashboard and in webhook payloads.
fromstringOne of your own fax numbers. Defaults to your default number. Sending from a number that isn't on your account is refused.
subjectstringStored with the fax and shown in the dashboard.
recipient_namestringWho the fax is for. Recorded on the fax record.
page_sizestringletter (default), legal or a4. Oversized pages are scaled to fit rather than refused by the carrier.
send_atstringISO 8601. Queues the fax instead of sending it now.

A 202 comes back with one fax object per recipient:

{
  "data": [
    {
      "id": "9d6a4768-0b4f-46e5-9b2b-59381876ba7d",
      "object": "fax",
      "direction": "outbound",
      "status": "sending",
      "from": "+19015550100",
      "to": "+19015550123",
      "pages": null,
      "file_name": "referral.pdf",
      "subject": "Referral — Chen",
      "error": null,
      "scheduled_at": null,
      "created_at": "2026-08-20T17:48:14.313Z",
      "document_url": "/api/v1/faxes/9d6a4768.../document"
    }
  ]
}

202 does not mean delivered. It means the carrier took the job. Fax is a real phone call to a real machine, and that call can still fail — a busy line, no answer, a machine that hangs up mid-page. Wait fordelivered before you tell anyone the fax arrived. If a page was oversized we scale it to fit and say so in a warnings array rather than letting the carrier reject it.

Read a fax

GEThttps://my.signalfax.com/api/v1/faxes/{id}

GEThttps://my.signalfax.com/api/v1/faxes/{id}/document

The first returns the fax object. The second returns the PDF bytes — not a redirect to storage, because a signed storage link would be a copy of the document that outlives your key.

GEThttps://my.signalfax.com/api/v1/faxes

Newest first. limit up to 100, direction of inbound or outbound, and before for the next page — pass back the next_before you were given.

curl "https://my.signalfax.com/api/v1/faxes?direction=inbound&limit=50" \
  -H "Authorization: Bearer $SIGNAL_FAX_KEY"

Status values

queuedAccepted by us, not yet handed to the carrier.
sendingThe carrier accepted the job. This is not delivery.
deliveredThe receiving machine acknowledged every page. This is the only status that means it arrived.
failedIt did not go through. `error` says why in plain language.
scheduledHeld for `send_at`.
receivedAn inbound fax.

Webhooks

Register an HTTPS endpoint under Organization settings → APIand we will POST to it when a fax is delivered, fails, or arrives — so you don’t have to poll. Events are fax.delivered, fax.failed and fax.received, each carrying the same fax object the API returns.

POST /your-endpoint
Signal-Event: fax.delivered
Signal-Signature: t=1755712094,v1=6b1f…

{ "id": "evt_…", "type": "fax.delivered", "created": 1755712094,
  "data": { "id": "9d6a4768…", "status": "delivered", "pages": 3, … } }

Verify the signature before you trust the body. It is an HMAC-SHA256 over `${timestamp}.${body}`using your signing secret, with the timestamp inside the signed material so a captured delivery can’t be replayed later:

import { createHmac, timingSafeEqual } from "crypto"

function verify(secret, header, rawBody, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")))
  const t = Number(parts.t)
  if (!Number.isFinite(t)) return false
  if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false

  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex")

  if (parts.v1?.length !== expected.length) return false
  return timingSafeEqual(Buffer.from(parts.v1, "hex"), Buffer.from(expected, "hex"))
}

Verify against the rawrequest body — re-serialising the JSON changes the bytes and the signature won’t match. Answer 2xxto acknowledge; we retry twice more, then stop and log it. Changing your endpoint rotates the secret, so an old endpoint can’t keep verifying deliveries.

Errors

Every error is JSON with a stable type you can branch on and a message written for whoever ends up reading the log.

{
  "error": {
    "type": "invalid_request_error",
    "message": "`555` isn't a valid US or Canadian fax number. Ten digits, area code first."
  }
}
TypeStatusWhen
authentication_error401Missing, malformed, revoked or expired key.
permission_error403The key is valid but lacks the scope the endpoint needs.
plan_error403The API isn't included on this plan.
billing_error402Suspended account, or the page allowance and its overage cap are both spent.
invalid_request_error400Something in the request is wrong. The message says what.
not_found404No such fax on this account.
rate_limit_error429Too many requests. `Retry-After` says how long to wait.
api_error502Something on our side, or at the carrier.

Limits

Requests120 per minute per key.
Recipients50 per request.
Document50 MB and 350 pages — the carrier’s ceilings, not ours.
PagesCounted against your monthly allowance on transmission, the same as the dashboard.

Something missing?

This is the whole API today — sending, reading, documents and webhooks. If you need an endpoint that isn’t here, tell us what you’re building and we’ll say honestly whether it’s close or not on the list.