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.
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ uv.lock
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .env
12
+ .env.*
@@ -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