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"
}'| Field | Type | Notes |
|---|---|---|
| to | string | array | Required. One fax number, or up to 50 of them. US and Canadian numbers, any format — 9015550123, (901) 555-0123 and +19015550123 are all accepted. |
| document | string | The PDF, base64-encoded. Use this, or document_url, or a multipart file part. |
| document_url | string | A URL we fetch the PDF from. It has to be reachable from our network without credentials. |
| file_name | string | What to call the document in the dashboard and in webhook payloads. |
| from | string | One of your own fax numbers. Defaults to your default number. Sending from a number that isn't on your account is refused. |
| subject | string | Stored with the fax and shown in the dashboard. |
| recipient_name | string | Who the fax is for. Recorded on the fax record. |
| page_size | string | letter (default), legal or a4. Oversized pages are scaled to fit rather than refused by the carrier. |
| send_at | string | ISO 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
| queued | Accepted by us, not yet handed to the carrier. | |
| sending | The carrier accepted the job. This is not delivery. | |
| delivered | The receiving machine acknowledged every page. This is the only status that means it arrived. | |
| failed | It did not go through. `error` says why in plain language. | |
| scheduled | Held for `send_at`. | |
| received | An 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."
}
}| Type | Status | When |
|---|---|---|
| authentication_error | 401 | Missing, malformed, revoked or expired key. |
| permission_error | 403 | The key is valid but lacks the scope the endpoint needs. |
| plan_error | 403 | The API isn't included on this plan. |
| billing_error | 402 | Suspended account, or the page allowance and its overage cap are both spent. |
| invalid_request_error | 400 | Something in the request is wrong. The message says what. |
| not_found | 404 | No such fax on this account. |
| rate_limit_error | 429 | Too many requests. `Retry-After` says how long to wait. |
| api_error | 502 | Something on our side, or at the carrier. |
Limits
| Requests | 120 per minute per key. | |
| Recipients | 50 per request. | |
| Document | 50 MB and 350 pages — the carrier’s ceilings, not ours. | |
| Pages | Counted 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.