LuzardoFax API
Send and receive faxes, retrieve documents and integrate fax into your own systems. Available on every LuzardoFax plan.
Base URL https://luzardofax.com/api/v1
Send your first fax in five minutes.
Three steps: create a key, send a fax, receive the result.
- 1. Create an API key — in your account, go to Settings → API → New key. The key is shown once; store it in a secrets manager, never in client-side code or a public repo.
- 2. Send a fax — one request, file included:
curl -X POST https://luzardofax.com/api/v1/faxes \
-H "Authorization: Bearer lfx_live_YOUR_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-F "to=+13055551234" \
-F "to_name=Dr. Smith" \
-F "subject=Referral" \
-F "[email protected]"
You get back 202 Accepted — the fax is queued, not yet delivered:
{
"data": {
"id": 103,
"reference": "LFX-S-1-103",
"object": "fax",
"status": "queued",
"to": "+13055551234",
"pages": 1
},
"request_id": "req_9f2c1a8b4d"
}
- 3. Get the result — configure a webhook (recommended) or poll
GET /faxes/{id}.
Authentication and scopes.
Every request carries your key in the Authorization header. There is no query-parameter fallback on purpose: keys in URLs end up in server logs.
Authorization: Bearer lfx_live_a1b2c3d4...
Keys are scoped. Grant only what the integration needs:
fax:send— send faxesfax:read— read fax metadata, status and documentscontacts:read— read the address booknumbers:read— read the account's fax numbersaccount:read— read plan, usage and webhooks
Keys are stored hashed on our side. We cannot show you a key again after creation — if it is lost or exposed, revoke it and create a new one. Revocation takes effect immediately.
Store keys server-side in a secrets manager or environment variable. Never ship them in browser JavaScript, mobile apps or public repositories. Rotate them if a developer with access leaves.
Never send the same fax twice.
If your connection drops after we receive a request but before you get the response, retrying blindly would send the fax again. Send an Idempotency-Key header with a unique value per fax and that cannot happen.
Idempotency-Key: 8f14e45f-ea6f-4c0b-9d3a-1c2b7e9a4d55
- Same key, same request — we return the original response with header
Idempotent-Replay: true. No second fax. - Same key, still processing —
409 request_in_progress. Retry in a moment. - Same key, different request —
422 idempotency_key_reused. Use a new key for a new fax. - Keys are remembered for 24 hours and are scoped to your account.
Use a UUID per outbound fax. If a request fails before completing, the key is released so you can retry safely.
What each fax status means.
A fax moves through these states:
queued → sending → delivered
↘ failed
(converting) — when a DOCX needs conversion first
queued— accepted, waiting to be sentconverting— a DOCX attachment is being converted to PDFsending— transmission in progress with the carrierdelivered— the receiving machine confirmed itfailed— seerecipients[].errorfor the reason (busy line, no answer, not a fax machine)partial— multi-recipient fax where some succeeded and some failed
202 Accepted means we accepted the fax, not that it arrived. Delivery is confirmed by the fax.delivered webhook or by reading the status.
Sending faxes.
POST /api/v1/faxes — multipart/form-data. Scope: fax:send.
to— recipient fax number in E.164 (+13055551234). Repeat the field for multiple recipients, up to 20.to_name— optional, matched by position withtosubject,notes— optional, used on the cover sheetcover_id— optional, a cover sheet from your accountfiles— one or more attachments
Files: PDF, DOCX, JPG, PNG, TIFF. Up to 25 MB each, up to 20 per fax. DOCX is converted server-side before sending, which adds a short delay.
You must send at least one file, or a cover_id to send a cover sheet alone.
Reading faxes, contacts and account.
GET /faxes— list. Query:limit(max 100),offset,direction(inbound/outbound),status,created_after,created_before. Returnstotalandhas_more.- Incremental sync: pass the
created_atof the newest fax you already have ascreated_after(UTC, ISO 8601) and you only get what is new. Both date bounds are exclusive. Results are orderedcreated_at DESC, id DESC, which is stable across pages. GET /faxes/{id}— one fax, including per-recipient status and error.GET /faxes/{id}/pdf— the document itself, asapplication/pdf.GET /contacts— address book. Query:q,limit,offset.GET /numbers— active fax numbers on the account.GET /account— plan, pages used, page limit and billing cycle.
All timestamps are UTC. All list endpoints return the same envelope: { data: { object: "list", data: [...], total, limit, offset, has_more } }.
GET /faxes/{id}/pdf returns a document that may contain protected health information. Do not log the response body or expose these URLs to clients that should not see PHI.
Get told when something happens.
Instead of polling, give us an HTTPS endpoint and we notify you. Configure it in Settings → API → Webhooks or via the API:
GET /api/v1/webhooks— list your endpoints. Scope:webhooks:read(account:readalso accepted for backwards compatibility).POST /api/v1/webhooks— create one. Body:url(HTTPS required),events(array, optional — defaults to all),description. The signing secret is returned once. Scope:webhooks:write.DELETE /api/v1/webhooks/{id}— remove it. Scope:webhooks:write.
Events: fax.delivered, fax.partial, fax.failed, fax.received.
Faxes with several recipients emit one event, not one per recipient. You get fax.delivered when every recipient succeeded, fax.failed when all of them failed, and fax.partial when the result is mixed. In every case the payload includes the per-recipient breakdown, so check data.recipients[] to see exactly which numbers went through.
Each delivery looks like this:
{
"id": "evt_7c3f9a1b2e4d6f8a0b1c",
"type": "fax.delivered",
"created": 1784580000,
"data": {
"id": 103,
"reference": "LFX-S-1-103",
"object": "fax",
"status": "delivered",
"pages": 1,
"recipients": [{ "to": "+13055551234", "status": "delivered", "error": null }]
}
}
Webhook payloads never contain fax document contents — only operational fax metadata such as status, page count and recipient numbers. Treat that metadata as sensitive and protect it accordingly in healthcare workflows. If you need the document itself, fetch it with your API key.
Verifying the signature. Every request carries:
X-LuzardoFax-Signature: t=1784580000,v1=5257a869e7ecebeda32affa62cdca3fa...
X-LuzardoFax-Event: fax.delivered
Compute HMAC-SHA256 of "{t}.{raw_body}" using your endpoint's signing secret and compare it to v1 with a constant-time comparison:
const crypto = require('crypto');
function verify(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(
header.split(',').map(p => p.split('='))
);
const expected = crypto
.createHmac('sha256', secret)
.update(parts.t + '.' + rawBody)
.digest('hex');
// constant-time comparison
const ok = crypto.timingSafeEqual(
Buffer.from(expected), Buffer.from(parts.v1)
);
// reject old timestamps (replay protection)
const fresh = Math.abs(Date.now()/1000 - Number(parts.t)) < toleranceSeconds;
return ok && fresh;
}
Use the raw request body, before any JSON parsing — reserialized JSON will not match.
Retries and duplicates. We retry up to 6 times with exponential backoff (30s, 2m, 8m, 32m, 2h, 8h) until your endpoint returns a 2xx. That means you may receive the same event more than once. Store the event id and ignore repeats — make your handler idempotent.
Return a 2xx quickly (under 10 seconds). Do the heavy work after responding, or we treat it as a timeout and retry.
Errors and rate limits.
Every error has the same shape:
{
"error": {
"type": "quota_error",
"code": "page_limit_reached",
"message": "Sending this fax would exceed your plan limit."
},
"request_id": "req_9f2c1a8b4d"
}
Include the request_id when contacting support — it lets us find the exact request.
400 invalid_request— missing or malformed parameters401 authentication_error— missing, invalid or revoked key403 permission_error— the key lacks the required scope402 quota_error— page limit reached, or sending paused on the account404 not_found— the resource does not exist, or belongs to another account409 request_in_progress— same Idempotency-Key still processing422 idempotency_key_reused— key reused with a different body429 rate_limit_error— slow down; seeRetry-After500 api_error— our side. Retry with the same Idempotency-Key.
Rate limits: 120 requests per minute per key. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.
Active keys per plan: Solo 3 · Plus 5 · Team 10 · Business 25. Revoked keys do not count.
Quota: faxes sent through the API count against the same monthly page allowance as the app. At 110% of your limit outbound sending pauses; inbound is never blocked.
Security and HIPAA.
This API can transport protected health information when used in HIPAA-regulated workflows. LuzardoFax supports those workflows under a signed BAA, but how you build and operate your integration is your responsibility.
- HTTPS only. Plain HTTP is refused, including for webhook endpoints.
- Keys server-side. Never in browser code, mobile apps or public repositories.
- Least privilege. Grant only the scopes the integration needs.
- Rotate keys when a developer with access leaves, and revoke immediately if one is exposed.
- Avoid logging PHI. Do not log request or response bodies from fax endpoints.
- Do not put PHI in URLs or query parameters — those end up in access logs and analytics.
- Protect your webhook endpoint and verify every signature before acting on a payload.
Using this API does not by itself make your organization HIPAA compliant. Compliance depends on your own risk analysis, policies, safeguards and how you build and operate the systems around this integration. LuzardoFax is responsible for its obligations under the applicable BAA and law; customers remain responsible for the security and compliance of their own applications, infrastructure, policies and workflows.
Questions about the API? Email [email protected] and include the request_id.