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. Sign in to your Faxloo account.
- 2. Scroll to API Keys, click Generate, copy the key (it starts with
faxloo_sk_) — you'll only see it once. - 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:
file— the document. PDF, DOCX, DOC, JPG, PNG, or TIFF. Max 20 MB.to_number— E.164 format, e.g.+12065551212.subject— optional, used on the cover page if your plan generates one.
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.
- Max content size: 1 MB.
text/plain: blank lines become paragraphs; single newlines become line breaks.text/html: HTML fragments are wrapped in a default stylesheet (Letter page size, 0.75in margins). To take full control, send a complete document starting with<!DOCTYPE html>and we'll forward it untouched.- Provide either
fileorcontent— sending both is rejected.
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 |
|---|---|---|
| 400 | invalid_request | Body shape or field invalid. |
| 400 | unsupported_mime_type | File type isn't supported. |
| 401 | authentication_required | Missing Authorization header. |
| 401 | invalid_api_key | Key not found or wrong format. |
| 401 | revoked_api_key | Key has been revoked. Generate a new one. |
| 402 | insufficient_credits | Account balance too low. Top up. |
| 404 | not_found | No fax with that id (or it belongs to a different account). |
| 413 | file_too_large | File exceeds the 20 MB limit. |
| 5xx | internal_error | Transient — 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
fax.delivered— fax was successfully transmitted to the destination.fax.failed— fax failed permanently; theerrorfield on the fax has the reason.
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.