Reference
Examples default to Python (snake_case) — most AI integrations are Python. TypeScript is the same surface in
camelCase (await_settled → awaitSettled), with identical parameters and byte-identical signed requests.
| Package | Registry | Version | Runtime |
|---|---|---|---|
@atoapayments/agent-pay | npm | 0.0.1 | Node 22+ |
atoa-agent-pay | PyPI | 0.0.1 | Python 3.10+ |
@atoapayments/agentic-payment-approvals-js | npm | 0.1.0 | Browser (TypeScript) |
Authentication
Two credentials on every request:
- API key (tier 1) — read from
ATOA_API_KEY; identifies your business and pins the environment. Sandbox and production keys are separate and not interchangeable. - ES256 signing key (tier 2) — the SDK signs every request; Atoa verifies against the public key you registered once. The private key is never sent. Requests can’t be tampered with or replayed.
You never sign anything yourself — you only decide where the private key lives: pass privateKeyPem loaded from
your secrets manager, or a custom signer (below) so the key never enters your process. Re-registering the same
agent with a different key is rejected. Keep both credentials out of source control, logs, and client-side code.
Three ways to supply the two credentials — pick one (Python shown; TypeScript mirrors it in camelCase):
# ATOA_API_KEY is read from the environment; the public key is derived from the PEM.
atoa = atoa_agent_pay.init(environment="sandbox", private_key_pem=private_key_pem)
atoa.agent.register(name="Bookings assistant")
KMS / custom signer
Pass a signer instead of a PEM. The SDK hands it the canonical subject string per request and expects a
detached compact JWS back: b64url(header)..b64url(rawSig), where rawSig is the 64-byte IEEE-P1363 r‖s
form (a KMS returns DER — convert it). Register the public key.
import atoa_agent_pay
from atoa_agent_pay import JwsSignature
from atoa_agent_pay.crypto import b64url_encode
class KmsSigner: # the signer only signs — it never holds the key bytes
def sign(self, subject: str, kid: str | None = None) -> JwsSignature:
header = b64url_encode(
(f'{{"alg":"ES256","kid":"{kid}"}}' if kid else '{"alg":"ES256"}').encode()
)
signing_input = f"{header}.{b64url_encode(subject.encode())}"
raw_sig = der_to_raw_es256(my_kms.sign_sha256_es256(signing_input)) # DER → 64-byte r‖s
return {"alg": "ES256", "jws": f"{header}..{b64url_encode(raw_sig)}"} # detached
atoa = atoa_agent_pay.init(
environment="sandbox", signer=KmsSigner(), public_key_pem=my_public_key_pem
)
atoa.agent.register(name="Payouts worker", public_key_pem=my_public_key_pem)
Methods
Every operation is laid out the same way — Parameters (marked required or optional), a Request example
(Python + TypeScript), then the Response type and an example. Python is snake_case; TypeScript mirrors it in
camelCase; every call is async. Amounts are decimal major units — { amount: 12.50, currency?: "GBP" }, so £12.50
is 12.50, never 1250; currency defaults to GBP.
Pagination. Every list(opts?) returns a Page<T> envelope; the paired listAll(opts?) walks every page and
returns a flat T[].
Contract / Payment / Customer / Store).Common paging inputs on every list: page (zero-based, default 0) and size (default 20).
agent
agent.register(opts)
Bootstrap your agent’s identity — the challenge → sign → register handshake in one call. Idempotent: the same
key/env/business returns the same agent; a changed name/description is a metadata update, not a conflict.
Parameters
Human-readable agent name shown in the Atoa dashboard.
Free-text description of what this agent does.
Your ES256 public key (PEM, SPKI). Pass it only with a KMS/custom signer; omit when the SDK holds the private key (it derives the public key itself).
Request
agent = atoa.agent.register(name="Bookings assistant")
Response → RegisteredAgent
me/list do not return it.{
"agentId": "agt_9f2c…",
"businessId": "biz_41a0…",
"environment": "sandbox",
"name": "Bookings assistant",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMFk…\n-----END PUBLIC KEY-----"
}
agent.me()
Read this agent’s own identity as the service sees it. Takes no parameters.
Request
self = atoa.agent.me()
Response → AgentIdentity
ACTIVE.{ "agentId": "agt_9f2c…", "businessId": "biz_41a0…", "environment": "sandbox", "name": "Bookings assistant", "status": "ACTIVE" }
agent.list()
List every agent registered under your business + environment. Authenticated by the SDK API key alone (no JWS). Takes no parameters.
Request
all = atoa.agent.list()
Response → AgentIdentity[]
An array of AgentIdentity (shape as agent.me).
contract
contract.create(input)
Create a spending contract. Returns it PENDING_AUTHORIZATION with an authorizationUrl the account holder (SEND)
or customer (COLLECT) opens to authorize; poll awaitActive until ACTIVE.
Parameters
Your label for this spending authority (e.g. “Supplier payouts”).
SEND (money out) or COLLECT (money in, off-session).
Required for COLLECT — the customer this contract charges (id from customer.create).
Ignored for SEND.
Free-text description.
The per-payment cap, ≥1 period cap, and validity window.
One or more period caps (≥1). period ∈ DAY · WEEK · FORTNIGHT · MONTH · HALF_YEAR · YEAR;
alignment is CALENDAR (default) or ANCHORED. A longer period’s cap must be ≥ a shorter one’s.
Request
contract = atoa.contract.create(
name="Supplier payouts",
limits={
"maxPerPayment": 50.0,
"periodLimits": [{"amount": 500.0, "period": "MONTH"}],
"validTo": "2026-12-31T23:59:59Z",
},
)
Response → Contract
create: PENDING_AUTHORIZATION.get/list).get — see below).limits above stay enforced until it’s authorized. null when nothing is staged.{
"contractId": "ctr_7b31…",
"type": "SEND",
"status": "PENDING_AUTHORIZATION",
"authorizationUrl": "https://pay.atoa.me/consent/ctr_7b31…",
"limits": {
"maxPerPayment": 50.0,
"periodLimits": [{ "amount": 500.0, "period": "MONTH", "alignment": "CALENDAR" }],
"currency": "GBP",
"validFrom": "2026-07-23T10:00:00Z",
"validTo": "2026-12-31T23:59:59Z"
}
}
contract.awaitActive(id, opts?)
Poll get until the contract is ACTIVE, with internal backoff. Throws AUTHORIZATION_TIMEOUT on timeout or
AUTHORIZATION_FAILED if the human declined / the link expired.
Parameters
How long to wait before throwing AUTHORIZATION_TIMEOUT.
Request
active = atoa.contract.await_active(contract.contract_id)
Response → Contract
The Contract (shape above) once status is ACTIVE.
contract.get(id)
Read a contract’s current state, including live usage. No webhooks — poll this.
Parameters
Request
contract = atoa.contract.get(id)
Response → Contract
The Contract shape (see contract.create), plus live per-period usage:
One window per period cap — remaining = cap − usedThisPeriod − reserved.
contract.list(opts?) · contract.listAll(opts?)
List this business’s contracts. list returns one page; listAll walks every page and returns a flat array. (Two
methods, one underlying list — grouped for that reason.)
Parameters
All filters optional. listAll takes the same shape minus page.
Request
page = atoa.contract.list(type="SEND", status="ACTIVE")
all = atoa.contract.list_all(type="COLLECT")
Response → Page<Contract> (listAll → Contract[])
contract.update(id, input)
Change a contract’s limits — a re-consent. Returns it PENDING_AUTHORIZATION with a new authorizationUrl;
the old consent stays live and payable until re-authorized. Poll awaitActive again.
Parameters
create).Request
reconsent = atoa.contract.update(id, limits={
"maxPerPayment": 75.0, "periodLimits": [{"amount": 750.0, "period": "MONTH"}], "validTo": "2027-01-31T23:59:59Z",
})
Response → Contract
The Contract, back at PENDING_AUTHORIZATION with a fresh authorizationUrl.
contract.revoke(id)
Terminate the contract. Payments against it are then rejected.
Parameters
Request
result = atoa.contract.revoke(id)
Response → ContractRevokeResult
{ contractId, type, status } — the lean terminal confirmation.payment
payment.collect(input)
Money in, one verb. contractId absent → a pay-link/QR the customer pays now. contractId present →
an off-session charge on the linked method of that COLLECT contract (no card details, ever). Either way, follow
with awaitSettled(paymentRequestId). One payment per call; not idempotent.
Parameters
Your order reference — echoed on the payment, webhooks, and dashboard. Not an idempotency key.
Present → off-session charge on this COLLECT contract’s linked method. Absent → a pay-link/QR the customer pays
now.
The managed Atoa customer (id from customer.create). Required for an off-session charge — pass it with
contractId, and it must match the contract’s own atoaCustomerId (a mismatch is rejected with
CONTRACT_CUSTOMER_MISMATCH). Optional for a pay-link, where it links the payment to a managed customer (and
enables savePaymentMethod). Not the same as customerId, which is your own reference.
YOUR id for the payer, echoed back everywhere. Omit for guest checkout (one is synthesized).
Prefill the checkout with the customer’s details.
The rest are optional power knobs — the simple path never needs them:
atoaCustomerId + paymentMethod: ['CARD']; incompatible with splitBill.store.list); defaults to the primary store.templateUrl.redirectUrl after payment.Request
req = atoa.payment.collect(amount={"amount": 45.0}, order_id="booking-8812")
Response → PaymentRequest
awaitSettled with this — the same id Atoa’s checkout widgets + direct API use.expiresAt.{
"paymentRequestId": "prq_a81f…",
"paymentUrl": "https://pay.atoa.me/prq_a81f…",
"qrCodeUrl": "https://pay.atoa.me/prq_a81f….svg",
"orderId": "booking-8812",
"customerId": "guest_3c…",
"amount": { "amount": 45.0, "currency": "GBP" },
"expiresAt": "2026-07-23T10:03:00Z"
}
payment.send(input)
Money out against an ACTIVE SEND contract. payments is always an array (1–20; a single payment = one
element); the result is a Payment[] in the same order. A terminal FAILED with a failureReason is a returned
per-item Payment, not a thrown error — only operational faults throw. Confirm each with
awaitSettled.
Parameters
ACTIVE SEND contract every instruction is paid against.1–20 instructions; each orderId must be unique in the batch.
Request
payments = atoa.payment.send(
contract_id=contract.contract_id,
payments=[{
"amount": {"amount": 12.5},
"beneficiary": {"name": "ACME LTD", "sortCode": "040004", "accountNumber": "12345678"},
"orderId": "order-9281",
}],
)
Response → SendResult (a Payment[] + optional nextAction)
A Payment[] in input order (iteration / result[0] work as before), with an optional nextAction (present when
an owner approval gates the batch — see approval gate). See payment.get for the
full Payment shape.
[
{
"type": "DEBIT",
"status": "PENDING",
"paidAmount": 12.5,
"currency": "GBP",
"orderId": "order-9281",
"contractId": "ctr_7b31…",
"beneficiary": { "name": "ACME LTD", "sortCode": "04****", "accountNumber": "****5678" },
"paymentIdempotencyId": "ATOA1692…"
}
]
payment.get(id)
Read one payment. One read model for both directions; accepts a paymentRequestId (CREDIT parent — current/best
state) or a paymentIdempotencyId (one attempt).
Parameters
paymentRequestId or a paymentIdempotencyId.Request
payment = atoa.payment.get(payment_request_id)
Response → Payment
CREDIT = money in (COLLECT) · DEBIT = money out (SEND). Discriminates which fields below are present.AWAITING_AUTHORIZATION · PENDING · AUTHORIZED · COMPLETED · FAILED · CANCELLED · EXPIRED · PARTIALLY_REFUNDED · REFUNDED.null until an attempt exists.FAILED/CANCELLED.{
"type": "CREDIT",
"status": "COMPLETED",
"paidAmount": 45.0,
"currency": "GBP",
"orderId": "booking-8812",
"paymentIdempotencyId": "ATOA1780…",
"paymentRequestId": "prq_a81f…"
}
payment.awaitSettled(id, opts?)
Poll a payment until it stops moving (COMPLETED/FAILED/CANCELLED, EXPIRED on CREDIT, or AUTHORIZED). Throws
SETTLEMENT_TIMEOUT on timeout. Accepts a paymentRequestId or a paymentIdempotencyId.
Parameters
paymentRequestId or a paymentIdempotencyId.SETTLEMENT_TIMEOUT.Request
settled = atoa.payment.await_settled(payment_request_id)
Response → Payment
The Payment (shape above) at a resting state.
payment.list(opts?) · payment.listAll(opts?)
List this agent’s payments across both directions. list returns one page; listAll walks every page. (Two
methods, one underlying list.)
Parameters
All filters optional. listAll takes the same shape minus page.
Request
page = atoa.payment.list(type="COLLECT", status="COMPLETED")
all = atoa.payment.list_all(type="SEND")
Response → Page<Payment> (listAll → Payment[])
payment.refund(id, input)
Refund a COMPLETED collected payment — full or partial.
Parameters
{ amount, reason? } — the amount must not exceed the paid amount. Sandbox: reason: "FAILURE TEST" forces a
FAILED refund.
Request
refund = atoa.payment.refund(payment_request_id, amount={"amount": 45.0}, reason="Order returned")
Response → Refund
cancelRefund while still INITIATED.INITIATED · COMPLETED · FAILED · CANCELLED.refund_amount, a float, in Python).{ "refundId": "ref_2b8c…", "status": "INITIATED", "refundAmount": { "amount": 45.0, "currency": "GBP" }, "paidAmount": { "amount": 45.0, "currency": "GBP" } }
payment.listRefunds(id) · payment.cancelRefund(refundId)
List the refunds of one collected payment, or cancel a still-INITIATED refund.
Parameters
Request
refunds = atoa.payment.list_refunds(payment_request_id)
atoa.payment.cancel_refund(refunds[0].refund_id)
Response
{ refundId, message? }.payment.cancel(id)
Cancel an unpaid collect (link) or a charge resting at AUTHORIZED.
Parameters
Request
cancelled = atoa.payment.cancel(payment_request_id)
Response → Payment
The CANCELLED payment.
payment.awaitDecision(approvalId)
Poll an approval (from a send/collect nextAction) to its terminal decision. See the
approval gate for the flow.
Parameters
nextAction.SETTLEMENT_TIMEOUT.Request
decision = atoa.payment.await_decision(next_action.approval_id)
if decision.status != "APPROVED":
... # handle
Response → Approval
PENDING · APPROVED · DECLINED · CANCELLED · EXPIRED · SUPERSEDED.payment.cancelApproval(contractId, approvalId)
Agent-initiated cancel of a pending approval — voids the underlying draws.
Parameters
Request
atoa.payment.cancel_approval(contract_id, approval_id)
Response
customer
customer.create(input)
Create a managed customer — needed for off-session COLLECT contracts (the customer links a card to the contract on its authorization page). Guest checkout does not require one.
Parameters
email or a phone number is required.44). Pair with phoneNumber.Request
customer = atoa.customer.create(full_name="Jane Doe", email="[email protected]")
Response → Customer
atoaCustomerId used across collect + saved cards.{ "id": "cus_1f…", "fullName": "Jane Doe", "email": "[email protected]", "type": "INDIVIDUAL", "createdAt": "2026-07-23T10:00:00Z" }
customer.get(id) · customer.update(id, input) · customer.delete(id)
Read, edit, or remove one managed customer. (Three verbs on one resource id — grouped for that reason.)
Parameters
Request
customer = atoa.customer.get(id)
atoa.customer.update(id, email="[email protected]")
atoa.customer.delete(id)
Response
Customer shape (above).{ success, message }.customer.list(opts?)
List the customers this agent created.
Parameters
Request
page = atoa.customer.list(page=0, size=20)
Response → Page<Customer>
store · client-level
store.list(opts?)
List this business’s stores — discover a storeId to tag a collect by location.
Parameters
Request
stores = atoa.store.list()
Response → Page<Store>
checkAvailability()
Unauthenticated health probe. Takes no parameters and never throws — check the returned flag.
Request
health = atoa.check_availability()
Response → AvailabilityStatus
sandboxTestAccounts()
The sandbox SEND recipients and the outcome each forces. Takes no parameters; production has none.
Request
test = atoa.sandbox_test_accounts()
Response → SandboxTestAccounts
{ sandbox, note, accounts } — the recipient accounts and their forced outcomes.Errors
Business outcomes are returned — branch on the Payment’s status and failureReason. Operational faults are
thrown AgentPayError subclasses — catch and switch on code.
Returned failureReason values:
| Direction | Reasons |
|---|---|
Send (DEBIT) | NAME_MISMATCH · LIMIT_EXCEEDED · CONTRACT_INACTIVE · SERVICE_UNAVAILABLE · SETTLEMENT_FAILED |
Collect (CREDIT) | PAYMENT_REJECTED · AUTHORIZATION_FAILED · CUSTOMER_CANCELLED · MERCHANT_CANCELLED |
| Approvals (either) | APPROVAL_DECLINED · APPROVAL_EXPIRED |
| Any | UNKNOWN — read failureReasonDescription |
Thrown AgentPayError subclasses:
code | Thrown when |
|---|---|
AUTH_ERROR | 401/403 — key invalid, not entitled, or a revoked agent. |
VALIDATION_ERROR | 400/422 — malformed request. |
NOT_FOUND | 404 — unknown or not-owned contract / payment / customer. |
CONFLICT | 409 — re-registering an agent with a different key, env, or business. |
REGISTRATION_ERROR | The register handshake failed. |
RATE_LIMIT | 429 — back off exponentially and retry. |
KEY_NOT_FOUND | No signing key available for the requested id. |
CONTRACT_CHARGE_ERROR | Base of the collect-charge ladder below — catch this to handle any charge refusal in one branch. |
CONTRACT_NOT_FOUND / CONTRACT_NOT_ACTIVE / CONTRACT_TYPE_MISMATCH / CONTRACT_CUSTOMER_MISMATCH | The contract is unknown, not chargeable (.reason says why), the wrong type, or another customer’s. |
CAP_EXCEEDED | Over a cap — .remaining is what’s left this window. |
NO_PAYMENT_METHOD | No usable method linked to the contract. |
ATOA_CUSTOMER_REQUIRED | A contract charge without atoaCustomerId. |
PARAMS_CONFLICT | Mutually exclusive options in one call. |
AUTHORIZATION_TIMEOUT / AUTHORIZATION_FAILED | awaitActive timed out / the approver declined or the link expired. |
SETTLEMENT_TIMEOUT | awaitSettled timed out. |
SERVER_ERROR | 5xx — the service received the request but failed on its side. Retryable. |
API_ERROR | An HTTP status the SDK doesn’t map to a specific class (402, 405, 408, …). status is set. |
NETWORK_ERROR | Connection failed (DNS/TLS/timeout) — the server never answered. Retryable. |
Retry guidance:
- Retry:
RATE_LIMIT(back off),SERVER_ERROR,NETWORK_ERRORand timeouts — but re-read withget/awaitSettledfirst; the operation may still be in flight, andorderIdis not a de-duplication key. - Fix first:
VALIDATION_ERROR,AUTH_ERROR,PARAMS_CONFLICT,CAP_EXCEEDED(charge ≤.remaining),CONTRACT_NOT_ACTIVE(re-approve). - Don’t retry: a returned
FAILEDis a decision, not a glitch.
Sandbox
Create the client with environment: "sandbox" and a sandbox API key. No real money moves; you choose every
outcome.
| Flow | How the outcome is decided |
|---|---|
| Send | Recipient account: 040004 / 12345678 → COMPLETED; 10000002, 10000003 → FAILED. Fetch with sandboxTestAccounts() — don’t hardcode. |
| Collect (pay-link) | Open paymentUrl, choose the Atoa Test Bank, pick COMPLETED / FAILED / PENDING. Cancel there → CUSTOMER_CANCELLED. |
| Expiry / cancel | Leave a link unpaid past expiresIn → EXPIRED; call payment.cancel → MERCHANT_CANCELLED. |
| Approvals | Open the approvalUrl and approve, decline, or let it lapse. |
| Failed refund | payment.refund(...) with reason: "FAILURE TEST". |
Production is the same code with a production key and environment: "production" — real banks, real approvers,
real money.
Approval gate
Every send and every off-session charge pauses for approval. A send result (still a Payment[]) and an
off-session collect result carry the action under nextAction (TS) / next_action (Python); a pay-link
collect has none. The approver is the party bound to the contract — the business owner for SEND, the
customer for an off-session COLLECT — deciding on Atoa’s hosted page with a one-time code or a WebAuthn passkey.
Your app never collects the credential.
"APPROVAL" today; switch on it so new action kinds don’t break you.awaitDecision / cancelApproval.Approvals browser SDK
@atoapayments/agentic-payment-approvals-js embeds Atoa’s hosted approval page as an iframe inside a container you
provide and resolves to the decision. Browser/TypeScript only; zero dependencies. Python integrators share the
approvalUrl or drive this from their web layer. Full guide: Approvals SDK.
import { confirmApproval } from "@atoapayments/agentic-payment-approvals-js";
const approval = confirmApproval({
container: "#approval", // a selector or HTMLElement you render + size
clientSecret: result.nextAction.clientSecret,
colorScheme: "light", // "light" | "dark"
onEvent: (e) => console.log(e.type), // lifecycle stream (opened, loaded, approved, …)
});
const { status } = await approval.result; // APPROVED | DECLINED | EXPIRED | SUPERSEDED | CANCELLED
approval.destroy(); // from your own close affordance; no-op after a decision
confirmApproval returns an ApprovalHandle (result · on(…) · destroy()) — it is not awaited directly. Other
options: theme (bounded, contrast-clamped tokens), labels.approve
(APPROVE | PAY | CONFIRM | AUTHORIZE), onResult, apiUrl (local-stack override). Events: opened ·
loaded · approved · declined · expired · superseded · error · closed.
The credential — code or passkey — is entered only on Atoa’s page. Never build your own form that collects it.
Go-live checklist
- Production API key as
ATOA_API_KEY, client withenvironment: "production". - Signing key from a secrets manager, or a KMS signer — never a generated throwaway.
- Branch on all three shapes: returned
COMPLETED, returnedFAILED/CANCELLEDwith afailureReason, thrownAgentPayError. - Deliver the
approvalUrlto the approver (or drive the browser SDK) on every send and off-session charge; handleAPPROVAL_DECLINED/APPROVAL_EXPIRED; observe withawaitDecision. - Register webhooks for status changes; keep
get/awaitSettledpolling as a fallback. - Run one small real payment end to end — including a declined and a cancelled path — before scaling up.
Changelog
- 0.0.1 — initial release.
Identifiers (paymentRequestId, customer ids, statuses, field names) carry straight over to Atoa’s direct API if
you outgrow the SDK.