Developers

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

Quickstart

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

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 faxes
  • fax:read — read fax metadata, status and documents
  • contacts:read — read the address book
  • numbers:read — read the account's fax numbers
  • account: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.

Key handling

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.

Idempotency

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 processing409 request_in_progress. Retry in a moment.
  • Same key, different request422 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.

Fax lifecycle

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 sent
  • converting — a DOCX attachment is being converted to PDF
  • sending — transmission in progress with the carrier
  • delivered — the receiving machine confirmed it
  • failed — see recipients[].error for 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.

POST /faxes

Sending faxes.

POST /api/v1/faxesmultipart/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 with to
  • subject, notes — optional, used on the cover sheet
  • cover_id — optional, a cover sheet from your account
  • files — 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.

Endpoints

Reading faxes, contacts and account.

  • GET /faxes — list. Query: limit (max 100), offset, direction (inbound/outbound), status, created_after, created_before. Returns total and has_more.
  • Incremental sync: pass the created_at of the newest fax you already have as created_after (UTC, ISO 8601) and you only get what is new. Both date bounds are exclusive. Results are ordered created_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, as application/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 } }.

PHI notice

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.

Webhooks

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:read also 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.

Reference

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 parameters
  • 401 authentication_error — missing, invalid or revoked key
  • 403 permission_error — the key lacks the required scope
  • 402 quota_error — page limit reached, or sending paused on the account
  • 404 not_found — the resource does not exist, or belongs to another account
  • 409 request_in_progress — same Idempotency-Key still processing
  • 422 idempotency_key_reused — key reused with a different body
  • 429 rate_limit_error — slow down; see Retry-After
  • 500 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.

Compliance

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.
Plain English

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.