The machine-readable contract of Fax for Agents, the fax API built for AI agents.
Website · Quickstart · API reference · SDKs · MCP · Dashboard · Pricing · Changelog · Affiliate program · GitLab
Fax for Agents is a fax API, a hosted MCP server and SDKs built so AI agents can send and receive faxes. It is made by JSB Holdings LLC and built by Synapse. This repository holds the public OpenAPI document of the API.
| File | What it is |
|---|---|
openapi.yaml |
OpenAPI 3.1, 145 operations in 115 paths, 151 schemas, version 1.20.0 |
docs/ |
The rendered reference, published at https://faxforagents.github.io/openapi/ |
docs/llms.txt |
The same contract written for a model |
scripts/curate.py |
Produces openapi.yaml from the server's own document |
The official SDKs are generated from this document. Python, TypeScript and the MCP server. When the document and an SDK disagree, the document is right and the SDK has a bug.
Point any OpenAPI tool at the raw file.
https://raw.githubusercontent.com/faxforagents/openapi/main/openapi.yaml
The server also serves its own document at https://api.faxforagents.com/openapi.json. That one
includes operations a customer never calls. Those are the admin routes, the affiliate area of the
dashboard, the carrier and payment webhook receivers, and the dashboard's cookie sign-in. This repository is the curated, public form: every operation an API key can call,
and the one-time send. Whole path prefixes that stay out are in EXCLUDED_PATHS in
scripts/curate.py. Single operations left out are named in EXCLUDED, each with its reason.
Base URL https://api.faxforagents.com. Send the key as Authorization: Bearer ffa_live_..., or in
X-API-Key. Keys are created in the dashboard at https://app.faxforagents.com or with
POST /v1/api-keys.
A test key (ffa_test_...) reaches the sandbox provider only. No carrier, no cost, nothing leaves the
box. A live key (ffa_live_...) reaches the carrier and spends the prepaid wallet.
Every operation states its security: an API key in bearerAuth or apiKeyHeader, the orderToken of
a one-time order, or a signedUrl the API handed out. Five operations of the one-time send need no
credential. x-required-scope names the scope the key needs. The server's document also names the
dashboard session as a credential. That session and the operations that take it alone stay out of
this document.
Every operation carries an x-llm block naming the scope the call needs, whether the call mutates and
whether it honours Idempotency-Key. A key is minted with the scopes it needs. An agent's own API key
can never carry approval:decide, key:admin, policy:admin, agent:admin, billing:write or
webhook:admin.
POST /v1/documents text, markdown, html or an upload -> doc_..., sha256, page_count
POST /v1/fax-quotes document_id + to -> quote_..., amount_micro, expires_at
POST /v1/faxes quote_id (+ Idempotency-Key) -> fax_..., state queued
GET /v1/faxes/{fax_id} poll -> state, evidence once delivered
A file goes through POST /v1/uploads with its content_type, a PUT of the bytes to the signed
upload_url, POST /v1/uploads/{id}/complete with the sha256, then POST /v1/documents with the
upload_id. The file is a PDF or an image: application/pdf, image/png, image/jpeg or
image/tiff. An image becomes gray letter pages, every TIFF frame is a page. HTML passed as html
is rendered by a headless browser with scripts off and no network. Inline CSS and data: images
print, external resources are counted as blocked. At most 2 MiB of HTML.
Beyond the send flow the document covers keys, webhooks, events, billing, the sandbox, approvals, delegated policies, agent identities and the inbox. It covers fax numbers: coverage, inventory, orders, porting and sender verification. It covers extractions: OCR, fields, classification, summaries and saved schemas. It covers the organization with its members, the auto-send setting and connected apps.
A person with one fax to send needs no wallet. The one-time-sends operations make an order, open a
hosted card payment and release the fax when the sender clicks the link mailed to them.
GET /v1/one-time-sends/config open here? the card, the limits -> enabled, routes
POST /v1/one-time-sends a file or text, to, email, accept_aup -> oto_..., status_token
POST /v1/one-time-sends/{id}/checkout open the hosted payment page -> checkout_url
POST /v1/one-time-sends/{id}/confirm the token of the mailed link -> the order, fax released
GET /v1/one-time-sends/{id} poll -> status, next, fax
GET /v1/one-time-sends/{id}/receipt.pdf once the fax ended -> a one-page PDF
The create takes no key. It answers a status token once. Send the token as X-FFA-Order-Token, or as
?t=, to read the order, open its checkout and have the confirmation mailed again. A live key with
fax:send may create an order for its organization, and POST /v1/one-time-sends/from-quote turns
a live quote the wallet does not cover into an order. Paying by card does not replace approval. With a
key, the send must have an approval or a delegated policy, as a wallet send does. Pass the approval as
approval_id on the from-quote body. A test key is refused: an order is always live. The card is
authorized at checkout and charged only when the carrier confirms delivery.
The person who pays accepts the acceptable use policy and the terms, never a key. With a key, the
order starts with aup_accepted false and carries a status_url, the order's page. Hand that link to
the person who pays. They accept the terms there and the checkout opens. queue on the config and on the order says whether a
fax would wait for a carrier line, with jobs_ahead and estimated_wait_minutes. The confirm page
reads a mailed link with POST /v1/one-time-sends/by-token and the token in the body. The older
GET /v1/one-time-sends/by-token/{token} is deprecated.
A fax job is in exactly one of six states.
| State | Meaning | Money |
|---|---|---|
queued |
accepted, waiting for the worker | held |
active |
at the carrier | held |
delivered |
every page arrived, the carrier's evidence is on the attempt | captured |
partial |
the call dropped after some pages, pages_sent says how many |
those pages captured, the rest released |
failed |
nothing arrived, failure_code says why |
released |
unknown |
the carrier gave no outcome, so nobody knows whether it arrived | held until a person reconciles |
delivered is never written without evidence. partial and unknown never retransmit on their own.
Whether to resend is a human decision.
With a test key, the destination number picks the outcome.
| Destination | Outcome |
|---|---|
+15550000001 |
delivered on the first attempt |
+15550000002 |
busy on attempt 1, delivered on attempt 2 |
+15550000003 |
partial after 3 pages |
+15550000004 |
no answer on all 3 attempts, failed |
+15550000005 |
accepted, no outcome reported, unknown with callback_missing |
+15550000006 |
the provider does not answer the send call, unknown with provider_timeout |
+15550000007 |
rejected at once, failed with invalid_number |
any other +1 number |
delivered |
POST /v1/sandbox/scenarios pins a scenario to a destination for 24 hours. DELETE /v1/sandbox/data
resets the organization's test data and reseeds the test wallet.
Amounts are integer USD micro-units. 39000 is $0.039. Price card launch-v3 is effective from
2026-11-12. $0.039 per page sent, $0.019 per page received. $2 per number per month, $19 per month
for the Agent workspace. Sandbox pages cost 0. GET /v1/capabilities returns the card in force with its
price_version.
One fax by card costs $2.49 for a document of up to 5 pages to a US or Canadian number, cover page
included. Each extra page is $0.49, up to 20 pages. These one-time prices were published on 2026-11-11.
GET /v1/one-time-sends/config returns them in micro-units while the product is open.
One envelope on every error.
{"error": {"code": "quote_expired", "message": "...", "retryable": false, "correlation_id": "..."}}402 insufficient_balance on POST /v1/faxes has its own schema, InsufficientBalance: details
carries available_micro, needed_micro and top_up_url, the page where a person adds funds. It
carries one_time_offer too when the same send can be paid by card. 413 payload_too_large answers a
request body over the limit of its route: 1 MiB by default and 4 MiB on POST /v1/documents. The bytes
of an upload and the one-time create form have their own larger limits. Every published limit is in
one table at https://faxforagents.com/docs/limits/. 429 carries Retry-After.
The errors an operation can answer are declared on it. The shared ones are in components.responses:
401, 402, 403, 404, 409, 413, 422, 429 and 503, each the envelope above. A field that fails validation
answers 422 invalid_request in the same envelope.
Repeat a mutating call with the same Idempotency-Key and the same resource comes back with
Idempotent-Replayed: true.
python scripts/curate.py # regenerate openapi.yaml from the live server document
python scripts/curate.py --vendor # the same, and keep the source in scripts/server-openapi.json
python scripts/build_reference.py # regenerate docs/reference.html and docs/llms.txt
npx @redocly/cli lint openapi.yaml # the lint CI runs
sh scripts/truthcheck.sh # the facts CI pinsCI fails when openapi.yaml, the reference or llms.txt is stale. It fails when an operation lacks an
operationId or an x-llm.scope. It fails when a tracked file states a price that is not on the card,
and when the dashboard session reaches the document. scripts/curate.py refuses a server document
whose credentials or scopes differ from its own tables.
MIT. See LICENSE. Security reports go to [email protected], see SECURITY.md.
