Faxloo

Faxloo API

Send faxes from your own backend in a single HTTP request. No SDK to install, no auth flow to negotiate — generate an API key from your Account page and start sending.

Beta. SDKs are coming soon.

Quick start

  1. 1. Sign in to your Faxloo account.
  2. 2. Scroll to API Keys, click Generate, copy the key (it starts with faxloo_sk_) — you'll only see it once.
  3. 3. Send your first fax with the curl below.

Authentication

Every request to /v1/* needs a bearer token. Use HTTPS; we don't accept plain HTTP.

Authorization: Bearer faxloo_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Keys are tied to one account. Treat them like a password — never commit them, never put them in client-side code, rotate them if you suspect exposure.

Send a fax

POST https://api.faxloo.com/v1/faxes

Multipart body with three fields:

Non-Latin or RTL content? Upload a pre-rendered PDF (with fonts embedded). The Faxloo web editor's automatic font fallback for Urdu, Arabic, CJK, and emoji is only applied to faxes composed there — the API forwards your uploaded file to the carrier as-is. DOC/DOCX uploads rely on the carrier's converter, which may substitute glyphs for non-Latin scripts. PDFs, PNGs, JPGs, and TIFFs are rasterized as-is and preserve any script.

curl

curl https://api.faxloo.com/v1/faxes \
  -H "Authorization: Bearer $FAXLOO_API_KEY" \
  -F "file=@./contract.pdf" \
  -F "to_number=+12065551212" \
  -F "subject=Signed contract"

Node.js

import fs from 'node:fs'

const form = new FormData()
form.append('file', new Blob([fs.readFileSync('./contract.pdf')]), 'contract.pdf')
form.append('to_number', '+12065551212')
form.append('subject', 'Signed contract')

const res = await fetch('https://api.faxloo.com/v1/faxes', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.FAXLOO_API_KEY}` },
  body: form,
})
const fax = await res.json()
console.log(fax.id, fax.status)

Python

import os, requests

with open('contract.pdf', 'rb') as f:
    r = requests.post(
        'https://api.faxloo.com/v1/faxes',
        headers={'Authorization': f"Bearer {os.environ['FAXLOO_API_KEY']}"},
        files={'file': f},
        data={'to_number': '+12065551212', 'subject': 'Signed contract'},
    )
fax = r.json()
print(fax['id'], fax['status'])

Response (HTTP 202)

{
  "id": "9e1c3a..",
  "object": "fax",
  "to_number": "+12065551212",
  "subject": "Signed contract",
  "status": "queued",
  "page_count": 3,
  "charged_amount": 3,
  "sinch_fax_id": null,
  "error": null,
  "created_at": "2026-05-21T18:30:00Z",
  "sent_at": null
}

Status transitions queued → sending → sent (or failed). Poll /v1/faxes/<id> or wait for the webhook (below).

Send from plain text or HTML

Skip the PDF step entirely. Send content_type = text/plain or text/html with a content field instead of file. We render to PDF server-side using the same font stack and RTL fallback the web editor uses — so Urdu, Arabic, CJK, emoji, and accented Latin all work without any setup on your side.

Plain text

curl https://api.faxloo.com/v1/faxes \
  -H "Authorization: Bearer $FAXLOO_API_KEY" \
  -F "to_number=+12065551212" \
  -F "content_type=text/plain" \
  -F "content=Quick note:

Your appointment is confirmed for Friday at 10am.

— Acme Clinic"

HTML

curl https://api.faxloo.com/v1/faxes \
  -H "Authorization: Bearer $FAXLOO_API_KEY" \
  -F "to_number=+12065551212" \
  -F "content_type=text/html" \
  -F 'content=<h1>Invoice #4271</h1><p>Total due: <strong>$1,250</strong></p>'

Response is the same shape as the file-upload path. Rendering adds ~1-2 seconds before the 202 returns.

Check status

GET https://api.faxloo.com/v1/faxes/<id>

curl https://api.faxloo.com/v1/faxes/9e1c3a.. \
  -H "Authorization: Bearer $FAXLOO_API_KEY"

Same shape as the POST response. status is the field that changes over time.

List recent faxes

GET https://api.faxloo.com/v1/faxes?limit=20

{
  "data": [ /* fax objects, newest first */ ],
  "has_more": true,
  "next_cursor": "2026-05-21T17:14:22Z"
}

Paginate by passing cursor=<next_cursor> on the next request.

Errors

All errors return JSON with an error envelope.

{
  "error": {
    "type": "invalid_request_error",
    "code": "insufficient_credits",
    "message": "Not enough credits to send this fax",
    "credits_balance": 3,
    "credits_needed": 12
  }
}
HTTP code Meaning
400invalid_requestBody shape or field invalid.
400unsupported_mime_typeFile type isn't supported.
401authentication_requiredMissing Authorization header.
401invalid_api_keyKey not found or wrong format.
401revoked_api_keyKey has been revoked. Generate a new one.
402insufficient_creditsAccount balance too low. Top up.
404not_foundNo fax with that id (or it belongs to a different account).
413file_too_largeFile exceeds the 20 MB limit.
5xxinternal_errorTransient — retry with exponential backoff.

Webhooks

Subscribe to delivery events instead of polling. Register an endpoint on your Account page — we'll POST a signed JSON event when a fax reaches a terminal state.

Events

Event payload

{
  "id": "evt_9e1c3a.._delivered",
  "object": "event",
  "type": "fax.delivered",
  "created_at": "2026-05-21T18:32:14Z",
  "data": {
    "object": {
      "id": "9e1c3a..",
      "object": "fax",
      "to_number": "+12065551212",
      "subject": "Signed contract",
      "status": "sent",
      "page_count": 3,
      "charged_amount": 3,
      "sinch_fax_id": "fax_abc123",
      "error": null,
      "created_at": "2026-05-21T18:30:00Z",
      "sent_at": "2026-05-21T18:32:14Z"
    }
  }
}

Signature verification

Each request carries a Faxloo-Signature header in the form t=<unix_ts>,v1=<hex_hmac_sha256>. The HMAC is over <t>.<raw_body> using the endpoint's signing secret (shown once when you create the endpoint, starts with whsec_).

import crypto from 'node:crypto'

function verifyFaxlooSignature(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(',').map(s => {
    const i = s.indexOf('='); return [s.slice(0, i), s.slice(i + 1)]
  }))
  const t = parseInt(parts.t, 10)
  if (!t || !parts.v1) return false
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSec) return false
  const mac = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
  return crypto.timingSafeEqual(Buffer.from(mac, 'hex'), Buffer.from(parts.v1, 'hex'))
}

Reject requests where the timestamp is more than 5 minutes old to defeat replay attacks. Respond with any 2xx within 10 seconds; non-2xx responses are retried with exponential backoff for up to 5 attempts (roughly 5s, 25s, 2m, 10m, 1h).

Pricing

API requests draw from the same credit balance as the web app. 1 credit = 1 US/Canada page; 3 credits = 1 international page. Same plan options apply — see pricing. There is no per-API-call surcharge.

Questions, found a bug, or want a feature? Email developers@faxloo.com.