faxforagents 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- faxforagents-0.2.0/.gitignore +12 -0
- faxforagents-0.2.0/CHANGELOG.md +28 -0
- faxforagents-0.2.0/LICENSE +21 -0
- faxforagents-0.2.0/PKG-INFO +253 -0
- faxforagents-0.2.0/README.md +209 -0
- faxforagents-0.2.0/pyproject.toml +90 -0
- faxforagents-0.2.0/scripts/gen_models.py +236 -0
- faxforagents-0.2.0/scripts/truthcheck.sh +69 -0
- faxforagents-0.2.0/spec/openapi.yaml +11956 -0
- faxforagents-0.2.0/src/faxforagents/__init__.py +62 -0
- faxforagents-0.2.0/src/faxforagents/_http.py +304 -0
- faxforagents-0.2.0/src/faxforagents/async_client.py +1249 -0
- faxforagents-0.2.0/src/faxforagents/cli.py +436 -0
- faxforagents-0.2.0/src/faxforagents/client.py +1238 -0
- faxforagents-0.2.0/src/faxforagents/errors.py +110 -0
- faxforagents-0.2.0/src/faxforagents/models.py +1336 -0
- faxforagents-0.2.0/src/faxforagents/py.typed +0 -0
- faxforagents-0.2.0/src/faxforagents/webhooks.py +119 -0
- faxforagents-0.2.0/tests/__init__.py +0 -0
- faxforagents-0.2.0/tests/conftest.py +133 -0
- faxforagents-0.2.0/tests/test_async.py +51 -0
- faxforagents-0.2.0/tests/test_cli.py +103 -0
- faxforagents-0.2.0/tests/test_client.py +264 -0
- faxforagents-0.2.0/tests/test_live.py +59 -0
- faxforagents-0.2.0/tests/test_models.py +48 -0
- faxforagents-0.2.0/tests/test_pagination.py +53 -0
- faxforagents-0.2.0/tests/test_surface.py +194 -0
- faxforagents-0.2.0/tests/test_webhooks.py +88 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0 (2026-10-28)
|
|
4
|
+
|
|
5
|
+
API version 1.1.0: HTML rendering and image uploads.
|
|
6
|
+
|
|
7
|
+
- `documents.create(html=...)` renders HTML on the server, scripts off and no network.
|
|
8
|
+
- `documents.upload` takes a PNG, JPG or TIFF beside a PDF and reads the content type from the
|
|
9
|
+
filename. `create_upload` documents the four content types.
|
|
10
|
+
- New resources, every operation of the document: `numbers` (coverage, inventory, orders, porting,
|
|
11
|
+
sender verification), `extractions` (OCR, fields, classification, summaries, saved schemas),
|
|
12
|
+
`organizations` and `members`. The inbox gains `update`, `delete`, `get_download` and `download`.
|
|
13
|
+
- `sandbox.create_inbound_fax` returns `InboxItemSummary`. `InboxItem` carries the number, project,
|
|
14
|
+
media status and read state of a received fax. `Quote` carries `per_page_micro` and `route_label`.
|
|
15
|
+
- The `faxforagents documents upload` command takes images and HTML files.
|
|
16
|
+
- Generated fields that clash with pydantic's own names get a trailing underscore and an alias
|
|
17
|
+
(`schema_`, alias `schema`).
|
|
18
|
+
|
|
19
|
+
## 0.1.0 (2026-10-28)
|
|
20
|
+
|
|
21
|
+
First release.
|
|
22
|
+
|
|
23
|
+
- `Client` and `AsyncClient` over httpx, every `/v1` operation of API version 0.1.0.
|
|
24
|
+
- Models generated from the public OpenAPI document (`scripts/gen_models.py`), pydantic v2.
|
|
25
|
+
- `Idempotency-Key` on every mutating call, retries for 429 and 5xx with `Retry-After`.
|
|
26
|
+
- `iter` helpers on every list, `faxes.wait` and `approvals.wait`.
|
|
27
|
+
- `verify_webhook` for the Standard Webhooks signature, with rotation support.
|
|
28
|
+
- The `faxforagents` command (extra `cli`): keys, documents, fax, inbox, approvals, balance, sandbox.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 JSB Holdings LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: faxforagents
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python client for Fax for Agents: a fax API, a hosted MCP server and SDKs built so AI agents can send and receive faxes.
|
|
5
|
+
Project-URL: Homepage, https://faxforagents.com
|
|
6
|
+
Project-URL: Documentation, https://faxforagents.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/faxforagents/sdk-python
|
|
8
|
+
Project-URL: Issues, https://github.com/faxforagents/sdk-python/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/faxforagents/sdk-python/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: OpenAPI, https://github.com/faxforagents/openapi
|
|
11
|
+
Author-email: Fax for Agents <hello@faxforagents.com>
|
|
12
|
+
License: MIT
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Keywords: agents,ai-agents,api-client,fax,fax-api,mcp,sdk,webhooks
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Communications :: Fax
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Requires-Dist: httpx<1,>=0.27
|
|
30
|
+
Requires-Dist: pydantic<3,>=2.6
|
|
31
|
+
Provides-Extra: cli
|
|
32
|
+
Requires-Dist: rich>=13; extra == 'cli'
|
|
33
|
+
Requires-Dist: typer>=0.12; extra == 'cli'
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
38
|
+
Requires-Dist: pyyaml>=6; extra == 'dev'
|
|
39
|
+
Requires-Dist: respx>=0.22; extra == 'dev'
|
|
40
|
+
Requires-Dist: rich>=13; extra == 'dev'
|
|
41
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
42
|
+
Requires-Dist: typer>=0.12; extra == 'dev'
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
<p align="center">
|
|
46
|
+
<a href="https://faxforagents.com"><img src="https://faxforagents.com/brand/logo-512.png" width="88" alt="Fax for Agents"></a>
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
<h1 align="center">faxforagents</h1>
|
|
50
|
+
|
|
51
|
+
<p align="center">The Python client for <a href="https://faxforagents.com">Fax for Agents</a>, the fax API built for AI agents.</p>
|
|
52
|
+
|
|
53
|
+
<p align="center">
|
|
54
|
+
<a href="https://faxforagents.com">Website</a> ·
|
|
55
|
+
<a href="https://faxforagents.com/docs/quickstart/">Quickstart</a> ·
|
|
56
|
+
<a href="https://faxforagents.com/docs/api/">API reference</a> ·
|
|
57
|
+
<a href="https://faxforagents.com/docs/sdks/">SDKs</a> ·
|
|
58
|
+
<a href="https://faxforagents.com/mcp/">MCP</a> ·
|
|
59
|
+
<a href="https://app.faxforagents.com">Dashboard</a> ·
|
|
60
|
+
<a href="https://faxforagents.com/pricing/">Pricing</a> ·
|
|
61
|
+
<a href="https://faxforagents.com/changelog/">Changelog</a>
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
<p align="center">
|
|
65
|
+
<a href="https://pypi.org/project/faxforagents/"><img src="https://img.shields.io/pypi/v/faxforagents?label=pypi" alt="PyPI version"></a>
|
|
66
|
+
<a href="https://github.com/faxforagents/sdk-python/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/faxforagents/sdk-python/ci.yml?branch=main&label=ci" alt="CI"></a>
|
|
67
|
+
<a href="https://github.com/faxforagents/sdk-python/blob/main/LICENSE"><img src="https://img.shields.io/badge/licence-MIT-1f6feb" alt="MIT licence"></a>
|
|
68
|
+
<a href="https://www.python.org"><img src="https://img.shields.io/badge/python-3.10%2B-3776ab" alt="Python 3.10+"></a>
|
|
69
|
+
</p>
|
|
70
|
+
|
|
71
|
+
Fax for Agents is a fax API, a hosted MCP server and SDKs built so AI agents can send and receive
|
|
72
|
+
faxes. It is made by JSB Holdings LLC.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install faxforagents # the client
|
|
76
|
+
pip install "faxforagents[cli]" # plus the faxforagents command
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Python 3.10 or later. Two dependencies, httpx and pydantic.
|
|
80
|
+
|
|
81
|
+
## Quickstart, against the sandbox
|
|
82
|
+
|
|
83
|
+
A test key (`ffa_test_...`) reaches the sandbox provider only. No carrier, no cost, nothing leaves the
|
|
84
|
+
box. The destination number picks the outcome. Keys are created in the dashboard at
|
|
85
|
+
<https://app.faxforagents.com> or with `POST /v1/api-keys`.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import os
|
|
89
|
+
from faxforagents import Client
|
|
90
|
+
|
|
91
|
+
os.environ["FAXFORAGENTS_API_KEY"] = "ffa_test_..." # or export it in the shell
|
|
92
|
+
|
|
93
|
+
with Client() as ffa:
|
|
94
|
+
doc = ffa.documents.create(text="Hello from an agent.", filename="hello")
|
|
95
|
+
print(doc.id, doc.page_count, doc.sha256[:12])
|
|
96
|
+
|
|
97
|
+
quote = ffa.quotes.create(document_id=doc.id, to="+15550000001")
|
|
98
|
+
print(quote.amount_micro, "micro, expires", quote.expires_at)
|
|
99
|
+
|
|
100
|
+
fax = ffa.faxes.create(quote_id=quote.id) # Idempotency-Key minted for you
|
|
101
|
+
print(fax.id, fax.state) # queued, never delivered yet
|
|
102
|
+
|
|
103
|
+
fax = ffa.faxes.wait(fax.id) # polls until a terminal state
|
|
104
|
+
print(fax.state, fax.pages_sent, fax.evidence) # delivered, 1, {...}
|
|
105
|
+
|
|
106
|
+
for job in ffa.faxes.iter(state="failed"): # follows next_cursor
|
|
107
|
+
print(job.id, job.failure_code)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The same in async form, with `AsyncClient` and `await` on every call.
|
|
111
|
+
|
|
112
|
+
## What the client does for you
|
|
113
|
+
|
|
114
|
+
- Every mutating call carries an `Idempotency-Key`. When you pass none, a uuid4 is minted once per
|
|
115
|
+
call and reused on every retry, so a retried send never creates a second fax. Pass
|
|
116
|
+
`idempotency_key=` to pick your own.
|
|
117
|
+
- 429 and 5xx answers are retried up to three times. `Retry-After` is honoured. Connection errors and
|
|
118
|
+
timeouts are retried the same way.
|
|
119
|
+
- Every list has an `iter` twin that follows `next_cursor`.
|
|
120
|
+
- `documents.create(text=...)`, `markdown=...` or `html=...` renders on the server. HTML prints with
|
|
121
|
+
scripts off and no network, inline CSS and `data:` images only, at most 2 MiB.
|
|
122
|
+
- `documents.upload(path)` runs the whole upload path for a PDF or an image (PNG, JPG, TIFF): the
|
|
123
|
+
upload session, the signed PUT with the content type read from the filename, the sha256 check, the
|
|
124
|
+
document. An image becomes gray letter pages.
|
|
125
|
+
- `faxes.wait(fax_id)` and `approvals.wait(approval_id)` poll with a timeout.
|
|
126
|
+
- `verify_webhook(secret, headers, body)` checks the Standard Webhooks signature on a delivery.
|
|
127
|
+
- Models are pydantic v2, generated from the public OpenAPI document. Money is an integer count of
|
|
128
|
+
USD micro-units (`39000` is $0.039). Timestamps are UTC strings.
|
|
129
|
+
|
|
130
|
+
## Resources
|
|
131
|
+
|
|
132
|
+
Method names are the operationIds of the [OpenAPI document](https://github.com/faxforagents/openapi),
|
|
133
|
+
shortened by their resource. The reference at <https://faxforagents.github.io/openapi/> names the scope
|
|
134
|
+
each call needs.
|
|
135
|
+
|
|
136
|
+
| Resource | Calls |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `ffa.identity` | `me`, `list_api_keys`, `create_api_key`, `rotate_api_key`, `revoke_api_key` |
|
|
139
|
+
| `ffa.documents` | `create`, `upload`, `create_upload`, `put_upload_content`, `complete_upload`, `cancel_upload`, `list`, `iter`, `get`, `delete`, `get_validation`, `get_download`, `download`, `list_pages` |
|
|
140
|
+
| `ffa.quotes` | `create`, `get` |
|
|
141
|
+
| `ffa.faxes` | `create`, `list`, `iter`, `get`, `wait`, `list_events`, `list_attempts`, `get_receipt`, `cancel` |
|
|
142
|
+
| `ffa.webhooks` | `list`, `create`, `update`, `delete`, `rotate_secret`, `test`, `list_deliveries`, `replay_delivery` |
|
|
143
|
+
| `ffa.events` | `list`, `iter` |
|
|
144
|
+
| `ffa.billing` | `balance`, `list_ledger`, settings, top-ups, auto top-ups, budgets, `usage`, exports, invoices, subscriptions, `create_portal_session` |
|
|
145
|
+
| `ffa.platform` | `capabilities`, `service_status` |
|
|
146
|
+
| `ffa.sandbox` | `list_scenarios`, `create_scenario`, `create_inbound_fax`, `reset` |
|
|
147
|
+
| `ffa.approvals` | `request`, `request_help`, `list`, `iter`, `get`, `wait`, `approve`, `reject`, `revoke` |
|
|
148
|
+
| `ffa.policies` | `list`, `create`, `effective`, `get`, `update`, `list_versions`, `evaluate` |
|
|
149
|
+
| `ffa.agents` | `list`, `create`, `get`, `update`, `create_key`, `activity` |
|
|
150
|
+
| `ffa.inbox` | `list`, `iter`, `get`, `update`, `delete`, `get_download`, `download` |
|
|
151
|
+
| `ffa.numbers` | `coverage`, `inventory`, `create_order`, `get_order`, `list`, `get`, `update`, `release`, `check_portability`, `list_port_orders`, `create_port_order`, `get_port_order`, `create_port_out_request`, `verify_sender` |
|
|
152
|
+
| `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` |
|
|
153
|
+
| `ffa.organizations` | `list`, `current`, `update_current`, `request_data_export`, `request_data_deletion` |
|
|
154
|
+
| `ffa.members` | `list`, `update`, `remove`, `list_invitations`, `invite`, `revoke_invitation` |
|
|
155
|
+
|
|
156
|
+
`ffa.request("GET", "/v1/anything")` reaches an operation the resources do not cover yet.
|
|
157
|
+
|
|
158
|
+
## States
|
|
159
|
+
|
|
160
|
+
A fax job is `queued`, `active`, `partial`, `failed`, `unknown` or `delivered`. `delivered` is written
|
|
161
|
+
only when the carrier's evidence is on the attempt, and `fax.evidence` holds it. `partial` and
|
|
162
|
+
`unknown` never retransmit on their own. Whether to resend is a human decision.
|
|
163
|
+
|
|
164
|
+
Sandbox destinations: `+15550000001` delivered, `+15550000002` busy then delivered, `+15550000003`
|
|
165
|
+
partial, `+15550000004` no answer, `+15550000005` unknown, `+15550000006` provider timeout,
|
|
166
|
+
`+15550000007` invalid number. Any other `+1` number is delivered.
|
|
167
|
+
|
|
168
|
+
## Errors
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from faxforagents import APIError, RateLimitedError
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
fax = ffa.faxes.create(quote_id=quote.id)
|
|
175
|
+
except RateLimitedError as exc:
|
|
176
|
+
print("retry in", exc.retry_after)
|
|
177
|
+
except APIError as exc:
|
|
178
|
+
print(exc.code, exc.message, exc.correlation_id) # quote_expired, insufficient_balance, approval_required
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`AuthenticationError` (401), `ScopeError` (403), `NotFoundError` (404), `ConflictError` (409),
|
|
182
|
+
`PaymentRequiredError` (402), `InvalidRequestError` (400, 422), `RateLimitedError` (429) and
|
|
183
|
+
`ServerError` (5xx) are all `APIError`. `APIConnectionError` and `APITimeoutError` mean the API was
|
|
184
|
+
not reached.
|
|
185
|
+
|
|
186
|
+
## Approvals
|
|
187
|
+
|
|
188
|
+
A live send needs either a human approval bound to the quote or a delegated policy that allows it.
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
req = ffa.approvals.request(quote.id) # a person is mailed
|
|
192
|
+
req = ffa.approvals.wait(req.id) # pending until they decide
|
|
193
|
+
if req.status == "approved":
|
|
194
|
+
fax = ffa.faxes.create(quote_id=quote.id, approval_id=req.id)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
An agent's own API key can never approve. `ffa.policies.evaluate(quote_id=...)` is a dry run that says
|
|
198
|
+
whether a policy would allow the send.
|
|
199
|
+
|
|
200
|
+
## Webhooks
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from faxforagents import verify_webhook, parse_event, SignatureError
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
event_id = verify_webhook(secret, request.headers, request.body)
|
|
207
|
+
except SignatureError:
|
|
208
|
+
return 400
|
|
209
|
+
event = parse_event(request.body) # event.type is fax.delivered, fax.failed, fax.received...
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Verify on the raw bytes. Store `event_id` to drop a redelivery you already handled. During a secret
|
|
213
|
+
rotation two signatures arrive for 24 hours and either one verifies.
|
|
214
|
+
|
|
215
|
+
## The command
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
export FAXFORAGENTS_API_KEY=ffa_test_...
|
|
219
|
+
faxforagents documents upload letter.pdf # or .png, .jpg, .tiff, .html, .md, .txt
|
|
220
|
+
faxforagents fax quote --document doc_... --to +15550000001
|
|
221
|
+
faxforagents fax send --quote quote_... --wait
|
|
222
|
+
faxforagents fax list --state failed
|
|
223
|
+
faxforagents inbox list
|
|
224
|
+
faxforagents approvals list --status pending
|
|
225
|
+
faxforagents balance
|
|
226
|
+
faxforagents sandbox scenario partial --to +12025550100
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Every command takes `--json` for the raw answer.
|
|
230
|
+
|
|
231
|
+
## Prices
|
|
232
|
+
|
|
233
|
+
Price card launch-v1 is effective from 2026-10-28. $0.039 per page sent, $0.019 per page received.
|
|
234
|
+
$2 per number per month, $19 per month for the Agent workspace. Sandbox pages cost 0.
|
|
235
|
+
`ffa.platform.capabilities()` returns the card in force.
|
|
236
|
+
|
|
237
|
+
## Working on this repository
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
pip install -e ".[dev,cli]"
|
|
241
|
+
pytest -q # unit tests, respx fixtures, no network
|
|
242
|
+
FAXFORAGENTS_API_KEY=ffa_test_... pytest -q -m live # one real sandbox send
|
|
243
|
+
python scripts/gen_models.py --check # models.py matches spec/openapi.yaml
|
|
244
|
+
ruff check src tests && mypy src
|
|
245
|
+
sh scripts/truthcheck.sh
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`spec/openapi.yaml` is a copy of the public document. To take a new one, copy it over and run
|
|
249
|
+
`scripts/gen_models.py`.
|
|
250
|
+
|
|
251
|
+
## Licence
|
|
252
|
+
|
|
253
|
+
MIT. See [LICENSE](LICENSE). Security reports go to hello@faxforagents.com, see [SECURITY.md](SECURITY.md).
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://faxforagents.com"><img src="https://faxforagents.com/brand/logo-512.png" width="88" alt="Fax for Agents"></a>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">faxforagents</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">The Python client for <a href="https://faxforagents.com">Fax for Agents</a>, the fax API built for AI agents.</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://faxforagents.com">Website</a> ·
|
|
11
|
+
<a href="https://faxforagents.com/docs/quickstart/">Quickstart</a> ·
|
|
12
|
+
<a href="https://faxforagents.com/docs/api/">API reference</a> ·
|
|
13
|
+
<a href="https://faxforagents.com/docs/sdks/">SDKs</a> ·
|
|
14
|
+
<a href="https://faxforagents.com/mcp/">MCP</a> ·
|
|
15
|
+
<a href="https://app.faxforagents.com">Dashboard</a> ·
|
|
16
|
+
<a href="https://faxforagents.com/pricing/">Pricing</a> ·
|
|
17
|
+
<a href="https://faxforagents.com/changelog/">Changelog</a>
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<a href="https://pypi.org/project/faxforagents/"><img src="https://img.shields.io/pypi/v/faxforagents?label=pypi" alt="PyPI version"></a>
|
|
22
|
+
<a href="https://github.com/faxforagents/sdk-python/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/faxforagents/sdk-python/ci.yml?branch=main&label=ci" alt="CI"></a>
|
|
23
|
+
<a href="https://github.com/faxforagents/sdk-python/blob/main/LICENSE"><img src="https://img.shields.io/badge/licence-MIT-1f6feb" alt="MIT licence"></a>
|
|
24
|
+
<a href="https://www.python.org"><img src="https://img.shields.io/badge/python-3.10%2B-3776ab" alt="Python 3.10+"></a>
|
|
25
|
+
</p>
|
|
26
|
+
|
|
27
|
+
Fax for Agents is a fax API, a hosted MCP server and SDKs built so AI agents can send and receive
|
|
28
|
+
faxes. It is made by JSB Holdings LLC.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install faxforagents # the client
|
|
32
|
+
pip install "faxforagents[cli]" # plus the faxforagents command
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Python 3.10 or later. Two dependencies, httpx and pydantic.
|
|
36
|
+
|
|
37
|
+
## Quickstart, against the sandbox
|
|
38
|
+
|
|
39
|
+
A test key (`ffa_test_...`) reaches the sandbox provider only. No carrier, no cost, nothing leaves the
|
|
40
|
+
box. The destination number picks the outcome. Keys are created in the dashboard at
|
|
41
|
+
<https://app.faxforagents.com> or with `POST /v1/api-keys`.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import os
|
|
45
|
+
from faxforagents import Client
|
|
46
|
+
|
|
47
|
+
os.environ["FAXFORAGENTS_API_KEY"] = "ffa_test_..." # or export it in the shell
|
|
48
|
+
|
|
49
|
+
with Client() as ffa:
|
|
50
|
+
doc = ffa.documents.create(text="Hello from an agent.", filename="hello")
|
|
51
|
+
print(doc.id, doc.page_count, doc.sha256[:12])
|
|
52
|
+
|
|
53
|
+
quote = ffa.quotes.create(document_id=doc.id, to="+15550000001")
|
|
54
|
+
print(quote.amount_micro, "micro, expires", quote.expires_at)
|
|
55
|
+
|
|
56
|
+
fax = ffa.faxes.create(quote_id=quote.id) # Idempotency-Key minted for you
|
|
57
|
+
print(fax.id, fax.state) # queued, never delivered yet
|
|
58
|
+
|
|
59
|
+
fax = ffa.faxes.wait(fax.id) # polls until a terminal state
|
|
60
|
+
print(fax.state, fax.pages_sent, fax.evidence) # delivered, 1, {...}
|
|
61
|
+
|
|
62
|
+
for job in ffa.faxes.iter(state="failed"): # follows next_cursor
|
|
63
|
+
print(job.id, job.failure_code)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The same in async form, with `AsyncClient` and `await` on every call.
|
|
67
|
+
|
|
68
|
+
## What the client does for you
|
|
69
|
+
|
|
70
|
+
- Every mutating call carries an `Idempotency-Key`. When you pass none, a uuid4 is minted once per
|
|
71
|
+
call and reused on every retry, so a retried send never creates a second fax. Pass
|
|
72
|
+
`idempotency_key=` to pick your own.
|
|
73
|
+
- 429 and 5xx answers are retried up to three times. `Retry-After` is honoured. Connection errors and
|
|
74
|
+
timeouts are retried the same way.
|
|
75
|
+
- Every list has an `iter` twin that follows `next_cursor`.
|
|
76
|
+
- `documents.create(text=...)`, `markdown=...` or `html=...` renders on the server. HTML prints with
|
|
77
|
+
scripts off and no network, inline CSS and `data:` images only, at most 2 MiB.
|
|
78
|
+
- `documents.upload(path)` runs the whole upload path for a PDF or an image (PNG, JPG, TIFF): the
|
|
79
|
+
upload session, the signed PUT with the content type read from the filename, the sha256 check, the
|
|
80
|
+
document. An image becomes gray letter pages.
|
|
81
|
+
- `faxes.wait(fax_id)` and `approvals.wait(approval_id)` poll with a timeout.
|
|
82
|
+
- `verify_webhook(secret, headers, body)` checks the Standard Webhooks signature on a delivery.
|
|
83
|
+
- Models are pydantic v2, generated from the public OpenAPI document. Money is an integer count of
|
|
84
|
+
USD micro-units (`39000` is $0.039). Timestamps are UTC strings.
|
|
85
|
+
|
|
86
|
+
## Resources
|
|
87
|
+
|
|
88
|
+
Method names are the operationIds of the [OpenAPI document](https://github.com/faxforagents/openapi),
|
|
89
|
+
shortened by their resource. The reference at <https://faxforagents.github.io/openapi/> names the scope
|
|
90
|
+
each call needs.
|
|
91
|
+
|
|
92
|
+
| Resource | Calls |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `ffa.identity` | `me`, `list_api_keys`, `create_api_key`, `rotate_api_key`, `revoke_api_key` |
|
|
95
|
+
| `ffa.documents` | `create`, `upload`, `create_upload`, `put_upload_content`, `complete_upload`, `cancel_upload`, `list`, `iter`, `get`, `delete`, `get_validation`, `get_download`, `download`, `list_pages` |
|
|
96
|
+
| `ffa.quotes` | `create`, `get` |
|
|
97
|
+
| `ffa.faxes` | `create`, `list`, `iter`, `get`, `wait`, `list_events`, `list_attempts`, `get_receipt`, `cancel` |
|
|
98
|
+
| `ffa.webhooks` | `list`, `create`, `update`, `delete`, `rotate_secret`, `test`, `list_deliveries`, `replay_delivery` |
|
|
99
|
+
| `ffa.events` | `list`, `iter` |
|
|
100
|
+
| `ffa.billing` | `balance`, `list_ledger`, settings, top-ups, auto top-ups, budgets, `usage`, exports, invoices, subscriptions, `create_portal_session` |
|
|
101
|
+
| `ffa.platform` | `capabilities`, `service_status` |
|
|
102
|
+
| `ffa.sandbox` | `list_scenarios`, `create_scenario`, `create_inbound_fax`, `reset` |
|
|
103
|
+
| `ffa.approvals` | `request`, `request_help`, `list`, `iter`, `get`, `wait`, `approve`, `reject`, `revoke` |
|
|
104
|
+
| `ffa.policies` | `list`, `create`, `effective`, `get`, `update`, `list_versions`, `evaluate` |
|
|
105
|
+
| `ffa.agents` | `list`, `create`, `get`, `update`, `create_key`, `activity` |
|
|
106
|
+
| `ffa.inbox` | `list`, `iter`, `get`, `update`, `delete`, `get_download`, `download` |
|
|
107
|
+
| `ffa.numbers` | `coverage`, `inventory`, `create_order`, `get_order`, `list`, `get`, `update`, `release`, `check_portability`, `list_port_orders`, `create_port_order`, `get_port_order`, `create_port_out_request`, `verify_sender` |
|
|
108
|
+
| `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` |
|
|
109
|
+
| `ffa.organizations` | `list`, `current`, `update_current`, `request_data_export`, `request_data_deletion` |
|
|
110
|
+
| `ffa.members` | `list`, `update`, `remove`, `list_invitations`, `invite`, `revoke_invitation` |
|
|
111
|
+
|
|
112
|
+
`ffa.request("GET", "/v1/anything")` reaches an operation the resources do not cover yet.
|
|
113
|
+
|
|
114
|
+
## States
|
|
115
|
+
|
|
116
|
+
A fax job is `queued`, `active`, `partial`, `failed`, `unknown` or `delivered`. `delivered` is written
|
|
117
|
+
only when the carrier's evidence is on the attempt, and `fax.evidence` holds it. `partial` and
|
|
118
|
+
`unknown` never retransmit on their own. Whether to resend is a human decision.
|
|
119
|
+
|
|
120
|
+
Sandbox destinations: `+15550000001` delivered, `+15550000002` busy then delivered, `+15550000003`
|
|
121
|
+
partial, `+15550000004` no answer, `+15550000005` unknown, `+15550000006` provider timeout,
|
|
122
|
+
`+15550000007` invalid number. Any other `+1` number is delivered.
|
|
123
|
+
|
|
124
|
+
## Errors
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from faxforagents import APIError, RateLimitedError
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
fax = ffa.faxes.create(quote_id=quote.id)
|
|
131
|
+
except RateLimitedError as exc:
|
|
132
|
+
print("retry in", exc.retry_after)
|
|
133
|
+
except APIError as exc:
|
|
134
|
+
print(exc.code, exc.message, exc.correlation_id) # quote_expired, insufficient_balance, approval_required
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`AuthenticationError` (401), `ScopeError` (403), `NotFoundError` (404), `ConflictError` (409),
|
|
138
|
+
`PaymentRequiredError` (402), `InvalidRequestError` (400, 422), `RateLimitedError` (429) and
|
|
139
|
+
`ServerError` (5xx) are all `APIError`. `APIConnectionError` and `APITimeoutError` mean the API was
|
|
140
|
+
not reached.
|
|
141
|
+
|
|
142
|
+
## Approvals
|
|
143
|
+
|
|
144
|
+
A live send needs either a human approval bound to the quote or a delegated policy that allows it.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
req = ffa.approvals.request(quote.id) # a person is mailed
|
|
148
|
+
req = ffa.approvals.wait(req.id) # pending until they decide
|
|
149
|
+
if req.status == "approved":
|
|
150
|
+
fax = ffa.faxes.create(quote_id=quote.id, approval_id=req.id)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
An agent's own API key can never approve. `ffa.policies.evaluate(quote_id=...)` is a dry run that says
|
|
154
|
+
whether a policy would allow the send.
|
|
155
|
+
|
|
156
|
+
## Webhooks
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from faxforagents import verify_webhook, parse_event, SignatureError
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
event_id = verify_webhook(secret, request.headers, request.body)
|
|
163
|
+
except SignatureError:
|
|
164
|
+
return 400
|
|
165
|
+
event = parse_event(request.body) # event.type is fax.delivered, fax.failed, fax.received...
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Verify on the raw bytes. Store `event_id` to drop a redelivery you already handled. During a secret
|
|
169
|
+
rotation two signatures arrive for 24 hours and either one verifies.
|
|
170
|
+
|
|
171
|
+
## The command
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
export FAXFORAGENTS_API_KEY=ffa_test_...
|
|
175
|
+
faxforagents documents upload letter.pdf # or .png, .jpg, .tiff, .html, .md, .txt
|
|
176
|
+
faxforagents fax quote --document doc_... --to +15550000001
|
|
177
|
+
faxforagents fax send --quote quote_... --wait
|
|
178
|
+
faxforagents fax list --state failed
|
|
179
|
+
faxforagents inbox list
|
|
180
|
+
faxforagents approvals list --status pending
|
|
181
|
+
faxforagents balance
|
|
182
|
+
faxforagents sandbox scenario partial --to +12025550100
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Every command takes `--json` for the raw answer.
|
|
186
|
+
|
|
187
|
+
## Prices
|
|
188
|
+
|
|
189
|
+
Price card launch-v1 is effective from 2026-10-28. $0.039 per page sent, $0.019 per page received.
|
|
190
|
+
$2 per number per month, $19 per month for the Agent workspace. Sandbox pages cost 0.
|
|
191
|
+
`ffa.platform.capabilities()` returns the card in force.
|
|
192
|
+
|
|
193
|
+
## Working on this repository
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
pip install -e ".[dev,cli]"
|
|
197
|
+
pytest -q # unit tests, respx fixtures, no network
|
|
198
|
+
FAXFORAGENTS_API_KEY=ffa_test_... pytest -q -m live # one real sandbox send
|
|
199
|
+
python scripts/gen_models.py --check # models.py matches spec/openapi.yaml
|
|
200
|
+
ruff check src tests && mypy src
|
|
201
|
+
sh scripts/truthcheck.sh
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`spec/openapi.yaml` is a copy of the public document. To take a new one, copy it over and run
|
|
205
|
+
`scripts/gen_models.py`.
|
|
206
|
+
|
|
207
|
+
## Licence
|
|
208
|
+
|
|
209
|
+
MIT. See [LICENSE](LICENSE). Security reports go to hello@faxforagents.com, see [SECURITY.md](SECURITY.md).
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "faxforagents"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
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."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Fax for Agents", email = "hello@faxforagents.com" }]
|
|
13
|
+
keywords = ["fax", "fax-api", "ai-agents", "mcp", "agents", "api-client", "sdk", "webhooks"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
25
|
+
"Topic :: Communications :: Fax",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"httpx>=0.27,<1",
|
|
31
|
+
"pydantic>=2.6,<3",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
cli = ["typer>=0.12", "rich>=13"]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=8",
|
|
38
|
+
"pytest-asyncio>=0.23",
|
|
39
|
+
"respx>=0.22",
|
|
40
|
+
"ruff>=0.6",
|
|
41
|
+
"mypy>=1.11",
|
|
42
|
+
"pyyaml>=6",
|
|
43
|
+
"typer>=0.12",
|
|
44
|
+
"rich>=13",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.scripts]
|
|
48
|
+
faxforagents = "faxforagents.cli:main"
|
|
49
|
+
|
|
50
|
+
[project.urls]
|
|
51
|
+
Homepage = "https://faxforagents.com"
|
|
52
|
+
Documentation = "https://faxforagents.com/docs"
|
|
53
|
+
Repository = "https://github.com/faxforagents/sdk-python"
|
|
54
|
+
Issues = "https://github.com/faxforagents/sdk-python/issues"
|
|
55
|
+
Changelog = "https://github.com/faxforagents/sdk-python/blob/main/CHANGELOG.md"
|
|
56
|
+
OpenAPI = "https://github.com/faxforagents/openapi"
|
|
57
|
+
|
|
58
|
+
[tool.hatch.build.targets.wheel]
|
|
59
|
+
packages = ["src/faxforagents"]
|
|
60
|
+
|
|
61
|
+
[tool.hatch.build.targets.sdist]
|
|
62
|
+
include = ["src/faxforagents", "README.md", "LICENSE", "CHANGELOG.md", "tests", "spec", "scripts"]
|
|
63
|
+
|
|
64
|
+
[tool.ruff]
|
|
65
|
+
line-length = 110
|
|
66
|
+
target-version = "py310"
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint]
|
|
69
|
+
select = ["E", "F", "W", "I", "UP", "B", "C4", "SIM"]
|
|
70
|
+
ignore = ["B008"]
|
|
71
|
+
|
|
72
|
+
[tool.ruff.lint.per-file-ignores]
|
|
73
|
+
# Generated: descriptions are copied from the OpenAPI document as they are.
|
|
74
|
+
"src/faxforagents/models.py" = ["E501"]
|
|
75
|
+
|
|
76
|
+
[tool.ruff.format]
|
|
77
|
+
exclude = ["src/faxforagents/models.py"]
|
|
78
|
+
|
|
79
|
+
[tool.pytest.ini_options]
|
|
80
|
+
testpaths = ["tests"]
|
|
81
|
+
asyncio_mode = "auto"
|
|
82
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
83
|
+
markers = [
|
|
84
|
+
"live: calls https://api.faxforagents.com with FAXFORAGENTS_API_KEY; skipped when the key is not set",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
[tool.mypy]
|
|
88
|
+
strict = true
|
|
89
|
+
warn_unused_ignores = true
|
|
90
|
+
ignore_missing_imports = true
|