Send a fax
Updated
A send is a document, a quote and a job. The document is immutable once created, the quote fixes the price, and the job records what the provider did. This page is the contract for all three.
Documents
A document is created from one of five sources and ends as one immutable PDF with a sha256 and a page count. The limit is 200 pages and 25 MiB.
| Source | Body of POST /v1/documents | Notes |
|---|---|---|
| PDF upload | {"upload_id": "up_..."} | validated with a PDF parser first. Encrypted or unreadable files are refused with encrypted_pdf or unsupported_format |
| Image upload | {"upload_id": "up_..."} | PNG, JPG or TIFF, declared as content_type on the upload session. Converted to grayscale letter pages, see Images below |
| Text | {"text": "..."} | rendered to pages by the service |
| Markdown | {"markdown": "..."} | headings, lists, emphasis and tables |
| HTML | {"html": "..."} | rendered in a headless browser with scripts off and no network, see HTML below |
Office formats are not accepted. Convert a Word or Excel file to PDF first.
Uploading a PDF
# 1. open a session (25 MiB cap, one hour)
curl -s $FFA/uploads -H "Authorization: Bearer $FFA_KEY" \
-H "Content-Type: application/json" -d '{"filename": "contract.pdf", "size_bytes": 184233}'
# answer: id (up_...) and a signed upload_url
# 2. send the bytes to the signed upload_url (no key)
curl -s -X PUT "$UPLOAD_URL" --data-binary @contract.pdf
# 3. finish; the size and the optional sha256 are checked
curl -s -X POST $FFA/uploads/up_.../complete -H "Authorization: Bearer $FFA_KEY"
# 4. make the document
curl -s $FFA/documents -H "Authorization: Bearer $FFA_KEY" \
-H "Content-Type: application/json" -d '{"upload_id": "up_..."}' GET /v1/documents/{id}/pages returns page previews behind signed URLs that live ten minutes. DELETE removes the bytes and the previews. The hash and the page count stay on any job that used it.
HTML
# inline CSS and data: images print; an external stylesheet, image or font is refused
curl -s $FFA/documents -H "Authorization: Bearer $FFA_KEY" \
-H "Content-Type: application/json" \
-d '{"html": "<style>@page { margin: 0.5in } body { font-family: Liberation Serif }</style><h1>Purchase order 4411</h1><p>Please confirm by return fax.</p>", "filename": "po-4411"}'
# answer: id (doc_...), source "html", page_count, sha256, and validation with resources_blocked when anything was refused The HTML is rendered in a headless browser with script execution off and no network. Inline CSS and data: images print. An external stylesheet, image or font is refused and counted, and the count comes back on the document's validation as the warning resources_blocked. Pages are US letter with a 0.4 inch margin unless the document's own @page rule says otherwise.
The HTML is at most 2 MiB. A document that takes longer than 45 seconds to render is refused with 422 conversion_failed. A renderer that is down answers 503 render_unavailable, and that call is retryable.
Fonts inside the renderer: Liberation, DejaVu, Noto Sans, Noto Serif and Noto CJK. Liberation is the metric twin of Arial, Times New Roman and Courier New. DejaVu, Noto Sans and Noto Serif cover Latin, Greek, Cyrillic, Hebrew, Arabic and most other scripts, and Noto CJK covers Chinese, Japanese and Korean. Name one of these in your CSS, or the font it stands in for. The MCP tool prepare_fax takes html next to text and markdown, and the CLI takes ffa documents upload letter.html.
Images
# 1. the same upload session as a PDF, with the content type of the image
curl -s $FFA/uploads -H "Authorization: Bearer $FFA_KEY" \
-H "Content-Type: application/json" \
-d '{"filename": "scan.tiff", "size_bytes": 912377, "content_type": "image/tiff"}'
# 2. send the bytes, then complete the session
curl -s -X PUT "$UPLOAD_URL" --data-binary @scan.tiff
curl -s -X POST $FFA/uploads/up_.../complete -H "Authorization: Bearer $FFA_KEY"
# 3. make the document: one grayscale letter page per TIFF frame
curl -s $FFA/documents -H "Authorization: Bearer $FFA_KEY" \
-H "Content-Type: application/json" -d '{"upload_id": "up_..."}'
# answer: source "image", page_count, and validation with image_rotated or image_downsampled when either applied The upload session takes content_type image/png, image/jpeg or image/tiff next to application/pdf, and the same upload_id makes the document. Every page becomes 8-bit grayscale, because fax is monochrome, on a US letter portrait page with a quarter inch margin, scaled to fit. An image wider than it is tall is rotated to fill the page, with the warning image_rotated. A frame larger than 2550 by 3300 pixels is scaled down to 300 dpi, with the warning image_downsampled.
Each frame of a multi-page TIFF is one page. PNG and JPEG use the first frame only. A frame over 50 million pixels is refused with image_too_large, and bytes that do not match the declared type with unsupported_format. The document's source is image, and the limits stay 200 pages and 25 MiB. The dashboard's upload button takes these files too, and the CLI takes .png, .jpg, .jpeg, .tif and .tiff.
Quotes
curl -s $FFA/fax-quotes -H "Authorization: Bearer $FFA_KEY" \
-H "Content-Type: application/json" \
-d '{"document_id": "doc_...", "to": "+14155550142", "from": "+16285550199", "cover": {"to_name": "Accounts", "subject": "PO 4411"}}' The quote carries the route, the document pages, the cover page when one is asked for and the billable pages. Also the amount in micro-USD, the rate table version and expires_at, 10 minutes out. It also says authorization_required: true for a live key with no policy that allows this send. A quote outside the published routes is refused with unsupported_destination.
The send
curl -s $FFA/faxes -H "Authorization: Bearer $FFA_KEY" \
-H "Idempotency-Key: po-4411-first-send" \
-H "Content-Type: application/json" \
-d '{"quote_id": "quote_...", "approval_id": "apr_..."}' Three things happen in one transaction. The wallet is locked and the quoted amount moves to a hold, or the call is refused with insufficient_balance before any provider is touched. The authorization is checked: a test key is allowed, a live API key needs approval_id or a policy that allows the plan. The job is written as queued with an audit row, and a dispatch is queued for the worker.
Always send an Idempotency-Key. A retry after a timeout returns the same job. A different body under the same key is a 409.
What are the six job states?
| State | Meaning | Hold |
|---|---|---|
| queued | accepted, waiting for the worker | held |
| active | the provider has the fax, attempts in progress. Busy and no answer are retried up to three times | held |
| delivered | every page confirmed, evidence on the attempt | captured |
| partial | some pages confirmed. Never retransmitted on its own | confirmed pages captured, rest released |
| failed | attempts exhausted, invalid number, render failure or cancel. failure_code says which | released |
| unknown | no outcome by the deadline. The reconcile tick keeps asking the provider. Never retransmitted on its own | held until reconciled |
Read the job with GET /v1/faxes/{id}, its attempts with /attempts, its ordered events with /events, and the receipt with /receipt. POST /v1/faxes/{id}/cancel cancels a queued job at once and is best effort once sending has started.
What does the receipt hold?
Job id, destination, sender, pages, provider reference, document sha256, amount captured, completion time. It is also sent by mail to the organization's notification address for live jobs, and the same fields are on the fax.delivered event.