Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## 0.4.0 (2026-11-12)

New resource `one_time_sends` on both clients: one fax paid by card, with no wallet.
Its calls are `config`, `create`, `create_from_quote`, `list`, `iter`, `get`, `wait`, `checkout`, `get_by_token`, `confirm`, `resend_confirmation` and `get_receipt_pdf`.
`create` sends a file as multipart or text as JSON. `wait` polls until the order's `next` is `done`, or the step passed as `until`.
`get`, `wait`, `checkout`, `resend_confirmation` and `get_receipt_pdf` take `order_token`, sent as `X-FFA-Order-Token`. The header name is exported as `ORDER_TOKEN_HEADER`.
`Client(keyless=True)` and `AsyncClient(keyless=True)` build a client with no API key. It sends no `Authorization` header and can call `one_time_sends` only.

The saved spec follows the public document at API 1.16.1, 144 operations. Models are regenerated from it.
New models: `OneTimeOrder`, `OneTimeOrderCreated`, `OneTimeOrderPage`, `OneTimeFromQuote`, `OneTimeCheckout`, `OneTimeConfirm`, `OneTimeConfirmSummary`, `OneTimeConfirmed`, `OneTimeCard`, `OneTimePrice`, `OneTimeJob` and `InsufficientBalance`.
Changed: `FaxJob.one_time` is a `OneTimeJob` model, where 0.3.2 declared a plain dict. Read `fax.one_time.order_id`, not `fax.one_time["order_id"]`. `Receipt` carries `one_time` too.
Other model names, fields and optionality are kept.

`PaymentRequiredError.one_time_offer` reads `details["one_time_offer"]` when a server sends it, and is `None` otherwise. API 1.16.1 does not send it.
The package metadata links to the pricing page and the dashboard. The README names price card launch-v2, effective 2026-10-31.

## 0.3.2 (2026-11-10)

`identity.create_api_key` takes `connected_app` on both clients. A key made with `connected_app=True` is bound by the auto-send setting like an OAuth connection.
Expand Down
60 changes: 57 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ each call needs.
| `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`, `get_by_token`, `confirm`, `resend_confirmation`, `get_receipt_pdf` |

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

Expand Down Expand Up @@ -173,6 +174,58 @@ key = ffa.identity.create_api_key("gemini", scopes=["fax:send"], connected_app=T
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.

```python
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="dana@example.com", # 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.

```python
from faxforagents import PaymentRequiredError

try:
fax = ffa.faxes.create(quote_id=quote.id)
except PaymentRequiredError:
order = ffa.one_time_sends.create_from_quote(
quote_id=quote.id, sender_name="Dana Example", accept_aup=True, approval_id=approval_id
)
```

Paying by card does not replace approval. The send must have an approval or a delegated policy, as a
wallet send does. `confirm(order_id, token=...)` and `get_by_token(token)` serve a page of your own
that receives the mailed link. The amounts are on the [pricing page](https://faxforagents.com/pricing/)
and in `config().routes`, in micro-units.

## Webhooks

```python
Expand Down Expand Up @@ -208,9 +261,10 @@ Every command takes `--json` for the raw answer.

## Prices

Price card launch-v1 is effective from 2026-10-28. $0.039 per page sent, $0.019 per page received.
Price card launch-v2 is effective from 2026-10-31. $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.
`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

Expand All @@ -223,7 +277,7 @@ ruff check src tests && mypy src
sh scripts/truthcheck.sh
```

`spec/openapi.yaml` follows the public document at API version 1.13.2 and keeps the schemas of the earlier live documents.
`spec/openapi.yaml` follows the public document at API version 1.16.1 and keeps the schemas of the earlier live documents.
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.
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "faxforagents"
version = "0.3.2"
version = "0.4.0"
description = "Python client for Fax for Agents: a fax API, a hosted MCP server and SDKs built so AI agents can send and receive faxes."
readme = "README.md"
requires-python = ">=3.10"
Expand Down Expand Up @@ -50,6 +50,8 @@ faxforagents = "faxforagents.cli:main"
[project.urls]
Homepage = "https://faxforagents.com"
Documentation = "https://faxforagents.com/docs"
Pricing = "https://faxforagents.com/pricing"
Dashboard = "https://app.faxforagents.com"
Repository = "https://github.com/faxforagents/sdk-python"
Issues = "https://github.com/faxforagents/sdk-python/issues"
Changelog = "https://github.com/faxforagents/sdk-python/blob/main/CHANGELOG.md"
Expand Down
19 changes: 14 additions & 5 deletions scripts/truthcheck.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
# not on the card. Provenance: faxforagents/openapi scripts/truthcheck.sh (2026-10-28). spec/ is the
# public OpenAPI document, checked in its own repository, so it is exempt here.
#
# The price card (price_version launch-v1, 2026-10-28): $0.039 per page sent, $0.019 per page
# The price card (price_version launch-v2, effective 2026-10-31): $0.039 per page sent, $0.019 per page
# received, $2 per number per month, $19 per month workspace. Any other dollar amount in copy is stale
# or invented. Claims the product does not make: HIPAA, "the first", "cheapest". Competitors are never
# named. CHANGELOG.md is exempt from the price rule (dated entries quote the card as it stood).
# One fax by card (published 2026-11-11): $2.49 for up to 5 pages to a US or Canadian number, cover
# included, $0.49 per extra page. A file that states either amount states that date too (rule 6).
set -u
cd "$(dirname "$0")/.." || exit 1

Expand All @@ -23,10 +25,10 @@ flag() {
prices=$(printf '%s\n' "$FILES" | grep -vE '^tests/' | xargs grep -noE '\$[0-9][0-9,]*(\.[0-9]+)?.?' 2>/dev/null \
| grep -vE '\$[0-9]+[")}]$' \
| sed -E 's/([0-9])[^0-9]$/\1/' \
| grep -vE ':\$(0\.039|0\.019|2|2\.00|19|19\.00)$' || true)
| grep -vE ':\$(0\.039|0\.019|2|2\.00|19|19\.00|2\.49|0\.49)$' || true)
if [ -n "$prices" ]; then
printf '%s\n' "$prices" >&2
flag "a dollar amount that is not on the price card (0.039, 0.019, 2, 19)"
flag "a dollar amount that is not on the price card (0.039, 0.019, 2, 19, and 2.49, 0.49 for one fax by card)"
fi

# 2) Claims the product does not make.
Expand Down Expand Up @@ -58,11 +60,18 @@ fi

# 5) Anchors that have to be present wherever prices are described.
if printf '%s\n' "$FILES" | xargs grep -lq '\$0\.039' 2>/dev/null; then
if ! printf '%s\n' "$FILES" | xargs grep -q '2026-10-28' 2>/dev/null; then
flag "a price is stated without its date (2026-10-28)"
if ! printf '%s\n' "$FILES" | xargs grep -q '2026-10-31' 2>/dev/null; then
flag "a price is stated without its date (2026-10-31)"
fi
fi

# 6) A one-time amount is stated only in a file that also states the day it was published.
for f in $(printf '%s\n' "$FILES" | xargs grep -lE '\$(2\.49|0\.49)' 2>/dev/null); do
if ! grep -q '2026-11-11' "$f"; then
flag "$f states a one-time price without its date (2026-11-11)"
fi
done

if [ "$fail" -eq 0 ]; then
echo "truthcheck: OK"
fi
Expand Down
Loading
Loading