Metadata-Version: 2.4
Name: atoa-agent-pay
Version: 0.0.1
Summary: The ergonomic, standalone Python SDK for agentic payments: embed payments in your product (or give your AI agent a payment tool) under a signed, capped, payee-scoped contract over HTTP. Three nouns — agent, contract, payment.
Project-URL: Homepage, https://github.com/ATOAPaymentsLimited/AtoaAgenticFramework/tree/main/packages/agent-pay/python
Project-URL: Repository, https://github.com/ATOAPaymentsLimited/AtoaAgenticFramework
Author: Atoa and contributors
License: UNLICENSED
Keywords: agentic-payments,ai-agents,fintech,jws,mcp,open-banking,payments,sdk
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: cryptography>=42
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.7
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Description-Content-Type: text/markdown

# atoa-agent-pay (Python)

**Both directions of money for your product or your AI agent: collect payments from your customers (link, QR, or an
off-session charge under a COLLECT contract they authorized), and send payments under signed, capped contracts Atoa
enforces server-side — over HTTP, in a few lines.**

The ergonomic, **standalone** Python SDK for [Atoa](https://paywithatoa.co.uk) payments. Behaviour and the signed wire
format are **byte-identical** to the TypeScript `@atoa/agent-pay` (proven by a shared conformance suite), so a Python
integration and a TypeScript one talk to the same backend interchangeably.

> New to AI agents, MCP, contracts, or request-signing? Read **[../CONCEPTS.md](../CONCEPTS.md)** first — every term in a
> few minutes.

---

## Install

```bash
pip install atoa-agent-pay
```

Requires Python 3.10+. Depends only on `httpx`, `cryptography`, and `pydantic`.

## The six nouns

The surface is **agent · contract · payment · customer · store**, driven through `atoa.agent` /
`atoa.contract` / `atoa.payment` / `atoa.customer` / `atoa.store`:

- **`agent`** — your identity + a signing key. `register(name=...)` is idempotent (a human-readable `name` is required).
- **`contract`** — a limited, revocable authority bounded by a per-payment cap + ≥1 period cap + an end date, in one
  of two directions: `SEND` (money out — the account holder authorizes once at their bank) or `COLLECT` (money in
  while the customer is not present — the **customer** authorizes once on the contract page). **A pay-link collect
  needs no contract.**
- **`payment`** — `collect` (money IN — link/QR, or an off-session charge when `contract_id` is passed), `send`
  (money OUT — contract-capped, 1–20 instructions per call), and `get` / `list` / `list_all` / `await_settled` /
  `cancel` / `capture` / `refund` / `list_refunds` / `cancel_refund`.
- **`customer`** — your payers + their saved payment methods (guest checkout needs none).
- **`store`** — read-only reference data (your stores, to find a `store_id`).

One **`Payment`** shape covers both directions — `type: "DEBIT"` (money out) or `"CREDIT"` (money in). Money you send
in is a grouped `Amount` in **DECIMAL major units** (`12.50`, not `1250`; `currency` defaults to GBP); money you read
back on a `Payment` is flat (`paid_amount` + `currency`). Both sync (`AtoaAgentPay`, via `init`) and async
(`AsyncAtoaAgentPay`, via `init_async`) clients are provided. No webhooks — you poll.

## Quickstart

`api_url` is optional (defaults per environment); the API key defaults to the `ATOA_API_KEY` env var (see
[../QUICKSTART.md](../QUICKSTART.md)).

```python
import atoa_agent_pay

private_key_pem, _ = atoa_agent_pay.generate_es256_keypair()   # prod: load from your secrets manager / sign in your KMS
atoa = atoa_agent_pay.init(environment="sandbox", private_key_pem=private_key_pem)
atoa.agent.register(name="Bookings assistant")                 # idempotent; name required

# ── collect — money IN (no contract) ──
req = atoa.payment.collect(amount={"amount": 45.00}, order_id="booking-8812")
print(req.payment_url)                                          # give the customer this link (or the QR)
print(atoa.payment.await_settled(req.payment_request_id).status)  # "COMPLETED"

# ── send — money OUT (under a capped contract) ──
contract = atoa.contract.create(
    name="Supplier payouts",
    limits={
        "max_per_payment": 50.00,
        "period_limits": [{"amount": 500.00, "period": "MONTH"}],
        "valid_to": "2026-12-31T23:59:59Z",
    },
)
atoa.contract.await_active(contract.contract_id)               # resolves once the account holder authorizes (sandbox: the Atoa Test Bank)
results = atoa.payment.send(                                   # payments is ALWAYS a list; result in order
    contract_id=contract.contract_id,
    payments=[{
        "amount": {"amount": 12.50},
        "beneficiary": {"name": "ACME LTD", "sort_code": "040004", "account_number": "12345678"},
        "order_id": "order-9281",
    }],
)
payment = results[0]
if payment.payment_idempotency_id:                             # a business FAILED is RETURNED, not raised
    print(atoa.payment.await_settled(payment.payment_idempotency_id).status)   # "COMPLETED"
else:
    print("refused:", payment.failure_reason, "—", payment.failure_reason_description)
```

A business outcome (a `FAILED`/`CANCELLED` payment with a `failure_reason`) is RETURNED on the `Payment` (branch on
`.status`), never an exception; only operational faults raise typed `AgentPayError`s. Full walkthrough with every
positive and negative case (real responses stored in the cells): [`notebooks/lifecycle.ipynb`](notebooks/lifecycle.ipynb)
(and the plain-script [`examples/walkthrough.py`](examples/walkthrough.py)). The COLLECT-contract cookbook (charge a
customer on file, every refusal recovered): [`examples/collect_contracts.py`](examples/collect_contracts.py).
Custom-signer / KMS example:
[`examples/kms_signer.py`](examples/kms_signer.py).

## API reference

**Client-level**

| Member | Signature | Notes |
| --- | --- | --- |
| `atoa.agent_id` | `str \| None` | Set after `register`. |
| `atoa.environment` | `"sandbox" \| "production"` | Bound to the key. |
| `atoa.api_url` | `str` | The resolved service base URL. |
| `atoa.check_availability()` | `→ {"available": bool, ...}` | Unauthenticated probe; never raises. |
| `atoa.sandbox_test_accounts()` | `→ {"sandbox", "note", "accounts"}` | Env-aware **send** test recipients; empty in production. |

- **`atoa.agent`** — `register(name=..., description=?, agent_id=?, public_key_pem=?) → RegisteredAgent`,
  `me() → AgentIdentity`.
- **`atoa.contract`** — `create(name=..., limits=..., description=?, type=?, atoa_customer_id=?) → Contract`
  (`type="COLLECT"` needs `atoa_customer_id`), `get(id)`, `list(...) → Page[Contract]`, `list_all(...)`,
  `await_active(id, timeout_ms=?)`, `update(id, limits=...)` (staged until re-approved), `revoke(id)`.
- **`atoa.payment`** — `collect(amount=..., order_id=..., contract_id=?, ...) → PaymentRequest`,
  `send(contract_id=..., payments=[...]) → list[Payment]` (1–20 per call), `get(id) → Payment` (either id kind),
  `list(...) → Page[Payment]`, `list_all(...)`, `await_settled(id, timeout_ms=?)`, `cancel(id)`, `capture(id)`,
  `refund(id, amount=..., reason=?)`, `list_refunds(id)`, `cancel_refund(refund_id)`.
- **`atoa.customer`** — `create` / `get` / `list` / `update` / `delete`, plus `list_payment_methods` /
  `get_payment_method` / `delete_payment_method`. **`atoa.store.list(...)`** —
  read-only reference data.

`Payment` mirrors the service — branch on `.status` (`AWAITING_AUTHORIZATION` / `PENDING` / `AUTHORIZED` / `COMPLETED` /
`FAILED` / `CANCELLED` / `EXPIRED` / `REFUNDED` / `PARTIALLY_REFUNDED` / `DISPUTE_RAISED` / `DISPUTE_WON` /
`DISPUTE_LOST`; `COMPLETED` is settled), then `.failure_reason` (fall back to
`.failure_reason_description`). `type` is `"DEBIT"` (sent, carries `beneficiary` + `contract_id`) or `"CREDIT"`
(collected, carries `payment_request_id` + the flat payer fields `consumer_name`/`bank_name`/`bank_account_no` +
`status_details`) — EXCEPT a collected payment whose status becomes `REFUNDED`/`PARTIALLY_REFUNDED` or a dispute
state (`DISPUTE_RAISED`/`DISPUTE_WON`/`DISPUTE_LOST`), which reports `type` as `"DEBIT"` (money now moving, or
already moved, back out); `payment_request_id` stays present regardless, so don't use `type` to infer the original
direction once a refund or dispute has occurred. The attempt id is `.payment_idempotency_id`
(`None` until an attempt exists); lists are offset `Page` objects (`.data`, `.total_count`, `.page`, `.size`). Full
model + reason tables + retry rules: **[../ERRORS.md](../ERRORS.md)**.

**Typed inputs.** Calls accept plain dicts (snake_case or camelCase). For IDE autocomplete, the TypedDicts
`AmountInput`, `PeriodLimitInput`, `ContractLimitsInput`, `BeneficiaryInput`, `SendPaymentInput`,
`CollectCustomerInput`, `CreateCustomerInput` (in `atoa_agent_pay.core`) describe the preferred
keys.

## Give the tools to an AI agent

`create_agent_pay_tools(atoa)` returns the canonical, self-describing tool specs + a `call()` dispatcher:

```python
from atoa_agent_pay import create_agent_pay_tools

tools = create_agent_pay_tools(atoa)            # hand tools.specs to your agent runtime
out = tools.call("collect_payment", {"amount": {"amount": 45.0}, "orderId": "booking-8812"})  # {"result","text","isError"}
```

The tools are the canonical 26-tool `AGENT_PAY_TOOLS` set — availability/identity, the contract tools, both money
verbs (`collect_payment` / `send_payment`), payment reads, cancel/capture, refunds, customers + saved methods, and
reference data — the same set the MCP exposes.
Minimal example: [`examples/ai_agent.py`](examples/ai_agent.py) (optional — most integrations don't need it). Zero-code
in Claude Desktop / Cursor: [../QUICKSTART.md → Zero-code (MCP)](../QUICKSTART.md#zero-code--use-it-from-claude-desktop--cursor-mcp).

## Keys & custody

The SDK needs an elliptic-curve private key handed in — **where you keep it is your choice** (env var, file, your KMS /
Secrets Manager / Vault). Atoa builds no provider integrations and never holds your key.

```python
import os, atoa_agent_pay
private_key_pem = os.environ["AGENT_PRIVATE_KEY_PEM"]            # prod: from your secret store
atoa = atoa_agent_pay.init(environment="production", private_key_pem=private_key_pem)
```

Prefer to sign inside your HSM/KMS? Pass a `signer=` callback (with `public_key_pem=`) so the key never enters your
process. **Never** hardcode, commit, or log the key or the token. One key per environment. The three custody tiers, the
decision table, and a runnable KMS example ([`examples/kms_signer.py`](examples/kms_signer.py)):
[../CONCEPTS.md → Keys & signing](../CONCEPTS.md#keys--signing).

## What's in the box

| Module | Purpose |
|--------|---------|
| `atoa_agent_pay` | the client surface — `init` / `init_async`, `AtoaAgentPay`, `AsyncAtoaAgentPay`, tools, errors |
| `atoa_agent_pay.core` | typed models (`Amount`, `Contract`, `Payment`, `Page`, …) + the input TypedDicts |
| `atoa_agent_pay.crypto` | canonical JSON + detached-signature core (advanced; the `Signer` seam, `Es256Signer`, `verify_es256`) |
| `atoa_agent_pay.signing` | the signed subjects |
| `atoa_agent_pay.tools` | framework-agnostic tool specs + a `call()` |

## Conformance & parity

The Python crypto is checked **byte-for-byte** against vectors generated from the TypeScript SDK:

```bash
npx tsx conformance/generate-vectors.ts    # regenerate the shared golden vectors (from packages/agent-pay)
pytest                                       # prove Python parity (from packages/agent-pay/python)
```

The full TS↔Python symbol map is the appendix at the bottom of [../QUICKSTART.md](../QUICKSTART.md#appendix--every-symbol-ts--python).

## Learn more

[../QUICKSTART.md](../QUICKSTART.md) · [../CONCEPTS.md](../CONCEPTS.md) · [../ERRORS.md](../ERRORS.md) ·
[../SANDBOX.md](../SANDBOX.md) · [../MIGRATION.md](../MIGRATION.md)

---

UNLICENSED — internal Atoa SDK.
