Skip to content

About

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.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Fax for Agents

faxforagents

The Python client for Fax for Agents, the fax API built for AI agents.

Website · Quickstart · API reference · SDKs · MCP · Dashboard · Pricing · Changelog · Affiliate program · GitLab

PyPI version CI MIT licence Python 3.10+

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.

pip install faxforagents            # the client
pip install "faxforagents[cli]"     # plus the faxforagents command

Python 3.10 or later. Two dependencies, httpx and pydantic.

Quickstart, against the sandbox

A test key (ffa_test_...) reaches the sandbox provider only. No carrier, no cost, nothing leaves the box. The destination number picks the outcome. Keys are created in the dashboard at https://app.faxforagents.com or with POST /v1/api-keys.

import os
from faxforagents import Client

os.environ["FAXFORAGENTS_API_KEY"] = "ffa_test_..."      # or export it in the shell

with Client() as ffa:
    doc = ffa.documents.create(text="Hello from an agent.", filename="hello")
    print(doc.id, doc.page_count, doc.sha256[:12])

    quote = ffa.quotes.create(document_id=doc.id, to="+15550000001")
    print(quote.amount_micro, "micro, expires", quote.expires_at)

    fax = ffa.faxes.create(quote_id=quote.id)              # Idempotency-Key minted for you
    print(fax.id, fax.state)                               # queued, never delivered yet

    fax = ffa.faxes.wait(fax.id)                           # polls until a terminal state
    print(fax.state, fax.pages_sent, fax.evidence)         # delivered, 1, {...}

    for job in ffa.faxes.iter(state="failed"):             # follows next_cursor
        print(job.id, job.failure_code)

The same in async form, with AsyncClient and await on every call.

What the client does for you

  • Every mutating call carries an Idempotency-Key. When you pass none, a uuid4 is minted once per call and reused on every retry, so a retried send never creates a second fax. Pass idempotency_key= to pick your own.
  • 429 and 5xx answers are retried up to three times. Retry-After is honoured. Connection errors and timeouts are retried the same way.
  • Cursor-paginated fax, document, event, approval, inbox, extraction and billing lists have iterators that follow next_cursor.
  • documents.create(text=...), markdown=... or html=... renders on the server. HTML prints with scripts off and no network, inline CSS and data: images only, at most 2 MiB.
  • documents.upload(path) runs the whole upload path for a PDF or an image (PNG, JPG, TIFF): the upload session, the signed PUT with the content type read from the filename, the sha256 check, the document. An image becomes gray letter pages.
  • faxes.wait(fax_id) and approvals.wait(approval_id) poll with a timeout.
  • verify_webhook(secret, headers, body) checks the Standard Webhooks signature on a delivery.
  • Models are pydantic v2, generated from the public OpenAPI document. Money is an integer count of USD micro-units (39000 is $0.039). Timestamps are UTC strings.

For raw HTTP upload completion, send a JSON object even when you omit the checksum.

curl -X POST "https://api.faxforagents.com/v1/uploads/$UPLOAD_ID/complete" \
  -H "Authorization: Bearer $FAXFORAGENTS_API_KEY" \
  -H "Content-Type: application/json" -d '{}'

Resources

Method names are the operationIds of the OpenAPI document, shortened by their resource. The reference at https://faxforagents.github.io/openapi/ names the scope each call needs.

Resource Calls
ffa.identity me, list_api_keys, create_api_key, rotate_api_key, revoke_api_key
ffa.documents create, upload, create_upload, put_upload_content, complete_upload, cancel_upload, list, iter, get, delete, get_validation, get_download, download, list_pages
ffa.quotes create, get
ffa.faxes create, list, iter, get, wait, list_events, list_attempts, get_receipt, get_receipt_pdf, cancel
ffa.webhooks list, create, update, delete, rotate_secret, test, list_deliveries, replay_delivery
ffa.events list, iter
ffa.billing balance, list_ledger, iter_ledger, iter_topup_sessions, iter_invoices, settings, top-ups, auto top-ups, budgets, usage, exports, invoices, subscriptions, create_portal_session
ffa.platform capabilities, service_status
ffa.sandbox list_scenarios, create_scenario, create_inbound_fax, reset
ffa.approvals request, request_help, list, iter, get, wait, approve, reject, revoke
ffa.policies list, create, effective, get_auto_send, set_auto_send, get, update, list_versions, evaluate
ffa.agents list, create, get, update, create_key, activity
ffa.inbox list, iter, get, update, delete, get_download, download
ffa.numbers coverage, inventory, create_order, list_orders, get_order, list, get, update, release, check_portability, list_port_orders, create_port_order, get_port_order, create_port_out_request, verify_sender
ffa.extractions create, list, iter, get, correct, classify, list_classifications, iter_classifications, get_classification, summarize, list_summaries, iter_summaries, get_summary, list_schemas, create_schema, get_schema
ffa.organizations list, current, update_current, request_data_export, request_data_deletion, download_export_archive
ffa.members list, update, remove, list_invitations, invite, revoke_invitation
ffa.oauth list_grants, iter_grants, revoke_grant
ffa.one_time_sends config, create, create_from_quote, list, iter, get, wait, checkout, read_by_token, confirm, resend_confirmation, get_receipt_pdf

ffa.request("GET", "/v1/anything") reaches an operation the resources do not cover yet.

States

A fax job is queued, active, partial, failed, unknown or delivered. delivered is written only when the carrier's evidence is on the attempt, and fax.evidence holds it. partial and unknown never retransmit on their own. Whether to resend is a human decision.

Sandbox destinations: +15550000001 delivered, +15550000002 busy then delivered, +15550000003 partial, +15550000004 no answer, +15550000005 unknown, +15550000006 provider timeout, +15550000007 invalid number. Any other +1 number is delivered.

Errors

from faxforagents import APIError, RateLimitedError

try:
    fax = ffa.faxes.create(quote_id=quote.id)
except RateLimitedError as exc:
    print("retry in", exc.retry_after)
except APIError as exc:
    print(exc.code, exc.message, exc.correlation_id)   # quote_expired, insufficient_balance, approval_required

AuthenticationError (401), ScopeError (403), NotFoundError (404), ConflictError (409), PaymentRequiredError (402), InvalidRequestError (400, 422), RateLimitedError (429) and ServerError (5xx) are all APIError. APIConnectionError and APITimeoutError mean the API was not reached.

Every refusal is the same envelope, a 422 for a field that fails validation included. A 413 payload_too_large is a plain APIError: the body is over the limit of its route. The published limits are in one table at https://faxforagents.com/docs/limits/.

Approvals

A live send needs either a human approval bound to the quote or a delegated policy that allows it.

req = ffa.approvals.request(quote.id)          # a person is mailed
req = ffa.approvals.wait(req.id)               # pending until they decide
if req.status == "approved":
    fax = ffa.faxes.create(quote_id=quote.id, approval_id=req.id)

An agent's own API key can never approve. ffa.policies.evaluate(quote_id=...) is a dry run that says whether a policy would allow the send.

A key made for a connected app used over stdio, such as the Gemini CLI extension, is created with connected_app=True. The organization's auto-send setting (ffa.policies.get_auto_send) then binds it like an OAuth connection. An agent's key cannot carry the flag.

key = ffa.identity.create_api_key("gemini", scopes=["fax:send"], connected_app=True)
key.connected_app                              # True, also on ffa.identity.list_api_keys()

One fax by card

A person with one fax to send needs no wallet and no key. one_time_sends makes an order, opens a hosted card payment and follows the order until the fax ends. The person pays on the hosted page and clicks the link mailed to them. The client never sees a card.

from faxforagents import Client

with Client(keyless=True) as ffa:                 # no API key: only one_time_sends answers
    config = ffa.one_time_sends.config()          # open here? the card and the limits
    order = ffa.one_time_sends.create(
        to="+12025550123",
        sender_name="Dana Example",
        email="[email protected]",                 # the confirmation link and the receipt go here
        accept_aup=True,                          # the policy and terms that config names
        file="form.pdf",                          # or text="...", text_format="markdown"
    )
    token = order.status_token                    # answered once: keep it
    page = ffa.one_time_sends.checkout(order.id, order_token=token)
    print(page.checkout_url)                      # the person pays here, then clicks the mailed link
    order = ffa.one_time_sends.wait(order.id, order_token=token, timeout=1800)
    print(order.status, order.fax["state"] if order.fax else None)
    pdf = ffa.one_time_sends.get_receipt_pdf(order.id, order_token=token)

order.next names the step that is waiting: checkout, confirm, wait or done. wait polls until done, or until the step you pass as until. The card is authorized at checkout and charged only when the carrier confirms delivery.

The status token goes in the X-FFA-Order-Token header. It reads the order, opens its checkout, mails the confirmation again and downloads the receipt. A client with a live key of the order's organization needs no token. A test key is refused: an order is always live.

With a live key, create_from_quote turns a quote the wallet does not cover into an order.

from faxforagents import PaymentRequiredError

try:
    fax = ffa.faxes.create(quote_id=quote.id, approval_id=approval_id)
except PaymentRequiredError as exc:
    print(exc.top_up_url)                         # where a person adds funds to the wallet
    offer = exc.one_time_offer                    # None when this send has no card path
    if offer:
        print(offer["amount_micro"])              # the one-time price of this document
        order = ffa.one_time_sends.create_from_quote(
            quote_id=quote.id, sender_name="Dana Example", approval_id=offer["approval_id"]
        )
        print(order.status_url)                   # hand this page to the person who pays

Paying by card does not replace approval. With a key, a card order must have an approval or a delegated policy, as a wallet send does. Pass the approval as approval_id. Without either the answer is 403 approval_required.

The person who pays accepts the acceptable use policy and the terms on the order page, never the key. With a key, the order answers aup_accepted false and a status_url. Give that link to the payer. On a page of your own, checkout(order.id, order_token=token, accept_aup=True) records the payer's acceptance and opens the payment.

order.queue and config().queue tell whether a fax would wait for a carrier line, with jobs_ahead and estimated_wait_minutes. A queued order is kept and sent in turn.

confirm(order_id, token=...) and read_by_token(token) serve a page of your own that receives the mailed link. read_by_token posts the token, so it stays out of the address. get_by_token still works and is deprecated. The amounts are on the pricing page and in config().routes, in micro-units.

Webhooks

from faxforagents import verify_webhook, parse_event, SignatureError

try:
    event_id = verify_webhook(secret, request.headers, request.body)
except SignatureError:
    return 400
event = parse_event(request.body)              # event.type is fax.delivered, fax.failed, fax.received...

Verify on the raw bytes. Store event_id to drop a redelivery you already handled. During a secret rotation two signatures arrive for 24 hours and either one verifies.

faxforagents.webhooks.verify is an alias of verify_webhook. Both names accept the same arguments.

The command

export FAXFORAGENTS_API_KEY=ffa_test_...
faxforagents documents upload letter.pdf        # or .png, .jpg, .tiff, .html, .md, .txt
faxforagents fax quote --document doc_... --to +15550000001
faxforagents fax send --quote quote_... --wait
faxforagents fax list --state failed
faxforagents inbox list
faxforagents approvals list --status pending
faxforagents balance
faxforagents sandbox scenario partial --to +12025550100

Every command takes --json for the raw answer.

Prices

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. ffa.platform.capabilities() returns the card in force. One fax by card has its own price: ffa.one_time_sends.config() returns it.

Working on this repository

pip install -e ".[dev,cli]"
pytest -q                                   # unit tests, respx fixtures, no network
FAXFORAGENTS_API_KEY=ffa_test_... pytest -q -m live    # one real sandbox send
python scripts/gen_models.py --check        # models.py matches spec/openapi.yaml
ruff check src tests && mypy src
sh scripts/truthcheck.sh

spec/openapi.yaml follows the public document at API version 1.20.0 and keeps the schemas of the earlier live documents. The two validation error schemas are the exception. 0.5.0 removed them because the server never sent that shape. 0.6.0 removed two cost fields of an extraction, because the server stopped sending them. It also retains earlier public model names and route-documented response additions. To refresh it, merge the live schemas while preserving existing declarations, then run uv run python scripts/gen_models.py. Keep new response fields optional so older payloads and existing callers remain valid. The schema drift test compares every saved schema property with the generated declarations without network access.

Licence

MIT. See LICENSE. Security reports go to [email protected], see SECURITY.md.

About

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.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages