Examples default to Python (snake_case) — most AI integrations are Python. TypeScript is the same surface in camelCase (await_settledawaitSettled), with identical parameters and byte-identical signed requests.

PackageRegistryVersionRuntime
@atoapayments/agent-paynpm0.0.1Node 22+
atoa-agent-payPyPI0.0.1Python 3.10+
@atoapayments/agentic-payment-approvals-jsnpm0.1.0Browser (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[].

Page<T>
object
data
T[]
The page of items (Contract / Payment / Customer / Store).
totalCount
number
Total matching rows across all pages.
page
number
Zero-based index of this page.
size
number
Page size (default 20).

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

name
string
required

Human-readable agent name shown in the Atoa dashboard.

description
string

Free-text description of what this agent does.

publicKeyPem
string

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")

ResponseRegisteredAgent

agentId
string
The id this client signs as from now on.
businessId
string
The business this agent belongs to.
environment
'sandbox' | 'production'
name
string
description
string
Free-text description, if you set one.
publicKeyPem
string
Echoed here onlyme/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()

ResponseAgentIdentity

agentId
string
businessId
string
environment
'sandbox' | 'production'
name
string
description
string
Free-text description, if you set one.
status
string
e.g. 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()

ResponseAgentIdentity[]

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

name
string
required

Your label for this spending authority (e.g. “Supplier payouts”).

type
'SEND' | 'COLLECT'
default: "SEND"

SEND (money out) or COLLECT (money in, off-session).

atoaCustomerId
string

Required for COLLECT — the customer this contract charges (id from customer.create). Ignored for SEND.

description
string

Free-text description.

limits
ContractLimitsInput
required

The per-payment cap, ≥1 period cap, and validity window.

maxPerPayment
number
required
Max for any single payment, in the contract currency.
periodLimits
{ amount, period, alignment? }[]
required

One or more period caps (≥1). periodDAY · WEEK · FORTNIGHT · MONTH · HALF_YEAR · YEAR; alignment is CALENDAR (default) or ANCHORED. A longer period’s cap must be ≥ a shorter one’s.

validTo
string (ISO datetime)
required
When the consent expires — must be bounded.
validFrom
string (ISO datetime)
default: "now"
When the consent starts.
currency
string
default: "GBP"
The single currency for every cap in this contract.

Request

contract = atoa.contract.create(
    name="Supplier payouts",
    limits={
        "maxPerPayment": 50.0,
        "periodLimits": [{"amount": 500.0, "period": "MONTH"}],
        "validTo": "2026-12-31T23:59:59Z",
    },
)

ResponseContract

contractId
string
agentId
string
The agent that owns this contract.
environment
'sandbox' | 'production'
type
'SEND' | 'COLLECT'
Emitted on every row so you can branch without inference.
createdAt
string (ISO datetime)
status
ContractStatus
Fresh from create: PENDING_AUTHORIZATION.
authorizationUrl
string
The page the human authorizes at (create/update only — absent on get/list).
limits
ContractLimits
The caps echoed back with defaults resolved.
usage
ContractUsageWindow[]
Live per-period headroom (populated on get — see below).
atoaCustomerId
string
COLLECT only — the customer this contract charges.
linkedMethod
{ paymentMethodId, lastFourDigits?, brand?, expiryDate? }
COLLECT only — the masked card the customer linked. Absent until they link one on the contract page.
termsVersion
number
COLLECT only — authorized terms version (1 on first approval; increments per approved update).
pendingUpdate
{ limits, requestedAt } | null
A staged limits change; the 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

contractId
string
required
opts.timeoutMs
number

How long to wait before throwing AUTHORIZATION_TIMEOUT.

Request

active = atoa.contract.await_active(contract.contract_id)

ResponseContract

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

contractId
string
required

Request

contract = atoa.contract.get(id)

ResponseContract

The Contract shape (see contract.create), plus live per-period usage:

usage
ContractUsageWindow[]

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

opts
ListContractsOptions

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")

ResponsePage<Contract> (listAllContract[])

list
Page<Contract>
The pagination envelope; data is Contract[].
listAll
Contract[]
Every page flattened.

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

contractId
string
required
input
{ limits: ContractLimitsInput }
required
The new caps (same shape as create).

Request

reconsent = atoa.contract.update(id, limits={
    "maxPerPayment": 75.0, "periodLimits": [{"amount": 750.0, "period": "MONTH"}], "validTo": "2027-01-31T23:59:59Z",
})

ResponseContract

The Contract, back at PENDING_AUTHORIZATION with a fresh authorizationUrl.

contract.revoke(id)

Terminate the contract. Payments against it are then rejected.

Parameters

contractId
string
required

Request

result = atoa.contract.revoke(id)

ResponseContractRevokeResult

ContractRevokeResult
object
{ 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

amount
{ amount, currency? }
required
The amount to collect.
orderId
string
required

Your order reference — echoed on the payment, webhooks, and dashboard. Not an idempotency key.

contractId
string

Present → off-session charge on this COLLECT contract’s linked method. Absent → a pay-link/QR the customer pays now.

atoaCustomerId
string

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.

customerId
string

YOUR id for the payer, echoed back everywhere. Omit for guest checkout (one is synthesized).

customer
{ fullName?, email?, phoneCountryCode?, phoneNumber? }

Prefill the checkout with the customer’s details.

redirectUrl
string
Where the customer lands after paying.
expiresIn
number (ms)
default: "180000"
Pay-link lifetime (default 3 minutes).

The rest are optional power knobs — the simple path never needs them:

paymentMethod
('PAY_BY_BANK' | 'CARD')[]
Restrict how the customer may pay. Both show by default.
savePaymentMethod
boolean
Save the card during checkout. Needs atoaCustomerId + paymentMethod: ['CARD']; incompatible with splitBill.
storeId
string
Charge under a specific store (see store.list); defaults to the primary store.
template
'EXTERNAL_DISPLAY' | 'EXTERNAL_DISPLAY_PNG' | 'RECEIPT' | 'RECEIPT_PNG'
Till/receipt QR template — sets templateUrl.
allowRetry
boolean
default: "true"
Allow several attempts while the link is live.
enableTips / strictExpiry / splitBill
boolean
callbackParams
object
Echoed back to your redirectUrl after payment.
notes
string
Free-text note captured on the payment.

Request

req = atoa.payment.collect(amount={"amount": 45.0}, order_id="booking-8812")

ResponsePaymentRequest

paymentRequestId
string
Poll awaitSettled with this — the same id Atoa’s checkout widgets + direct API use.
orderId
string
Your reference, echoed back.
customerId
string
Your payer reference (or the synthesized one for guest checkout).
amount
Amount
The requested amount + currency.
paymentUrl + qrCodeUrl
string
Pay-link mode — the link/QR the customer pays at. Also expiresAt.
status + contractId
PaymentStatus / string
Off-session charge mode — immediate charge state + the contract it ran under.
nextAction
NextAction
Off-session charge mode, only when the customer must approve — see approval gate. Absent for a pay-link.
{
  "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

contractId
string
required
The ACTIVE SEND contract every instruction is paid against.
payments
SendPaymentInstruction[]
required

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",
    }],
)

ResponseSendResult (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

id
string
required
A paymentRequestId or a paymentIdempotencyId.

Request

payment = atoa.payment.get(payment_request_id)

ResponsePayment

type
'CREDIT' | 'DEBIT'
CREDIT = money in (COLLECT) · DEBIT = money out (SEND). Discriminates which fields below are present.
status
PaymentStatus
AWAITING_AUTHORIZATION · PENDING · AUTHORIZED · COMPLETED · FAILED · CANCELLED · EXPIRED · PARTIALLY_REFUNDED · REFUNDED.
paidAmount + currency
number / string
What settled — flat, not nested.
orderId
string
Your reference (not a de-dup key).
paymentIdempotencyId
string | null
Settlement-attempt id; null until an attempt exists.
contractId
string
SEND (DEBIT) only — the contract this draw ran under.
beneficiary
{ name, sortCode?, accountNumber? }
SEND (DEBIT) only — who you paid, masked.
approvalId + approvalExpiresAt
string
SEND (DEBIT) only — present while the draw is gated on an approval.
paymentRequestId
string
COLLECT (CREDIT) only — the payment-request (parent) id.
customerId + atoaCustomerId
string
COLLECT (CREDIT) only — your own payer reference, and the Atoa customer id (once known).
consumerName + bankName + bankAccountNo
string
COLLECT (CREDIT) only — who paid, once an attempt exists: payer name, bank, and masked account.
failureReason + failureReasonDescription
string
Either direction — present on a terminal 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

id
string
required
A paymentRequestId or a paymentIdempotencyId.
opts.timeoutMs
number
How long to wait before throwing SETTLEMENT_TIMEOUT.

Request

settled = atoa.payment.await_settled(payment_request_id)

ResponsePayment

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

opts
ListPaymentsOptions

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")

ResponsePage<Payment> (listAllPayment[])

list
Page<Payment>
The pagination envelope; data is Payment[] (shape above).
listAll
Payment[]
Every page flattened.

payment.refund(id, input)

Refund a COMPLETED collected payment — full or partial.

Parameters

paymentRequestId
string
required
The collected payment to refund.
input
CreateRefundInput
required

{ 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")

ResponseRefund

refundId
string
The refund id — pass to cancelRefund while still INITIATED.
paymentRequestId
string
The collected payment this refund belongs to.
createdAt
string (ISO datetime)
status
RefundStatus
INITIATED · COMPLETED · FAILED · CANCELLED.
refundAmount
Amount
The refunded amount (refund_amount, a float, in Python).
paidAmount
Amount
The original paid amount.
{ "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

listRefunds: paymentRequestId
string
required
cancelRefund: refundId
string
required
Only a not-yet-processed refund can be cancelled.

Request

refunds = atoa.payment.list_refunds(payment_request_id)
atoa.payment.cancel_refund(refunds[0].refund_id)

Response

listRefunds
Refund[]
The refunds of the payment.
cancelRefund
CancelRefundResult
{ refundId, message? }.

payment.cancel(id)

Cancel an unpaid collect (link) or a charge resting at AUTHORIZED.

Parameters

paymentRequestId
string
required

Request

cancelled = atoa.payment.cancel(payment_request_id)

ResponsePayment

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

approvalId
string
required
From a nextAction.
opts.timeoutMs
number
How long to wait before throwing SETTLEMENT_TIMEOUT.

Request

decision = atoa.payment.await_decision(next_action.approval_id)
if decision.status != "APPROVED":
    ...  # handle

ResponseApproval

approvalId
string
type
'SEND' | 'COLLECT'
Which side the approval gates.
contractId
string
The contract this approval is bound to.
status
ApprovalStatus
PENDING · APPROVED · DECLINED · CANCELLED · EXPIRED · SUPERSEDED.
expiresAt
string (ISO datetime)
When the approval lapses.
createdAt
string (ISO datetime)
decidedBy + decidedAt
string
Who decided and when, once resolved.

payment.cancelApproval(contractId, approvalId)

Agent-initiated cancel of a pending approval — voids the underlying draws.

Parameters

contractId
string
required
approvalId
string
required

Request

atoa.payment.cancel_approval(contract_id, approval_id)

Response

result
{ approvalId, status }
The cancelled approval id + its new status.

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

fullName
string
required
2–30 characters.
email
string
Valid email. Either email or a phone number is required.
phoneCountryCode
string
Digits only (e.g. 44). Pair with phoneNumber.
phoneNumber
string
Digits only, without country code.
type
'INDIVIDUAL' | 'BUSINESS'
default: "INDIVIDUAL"
vatNumber
string
Business customers only.
address, city, postcode
string
Optional postal details.

Request

customer = atoa.customer.create(full_name="Jane Doe", email="[email protected]")

ResponseCustomer

id
string
The atoaCustomerId used across collect + saved cards.
fullName
string
email / phoneNumber
string
type
'INDIVIDUAL' | 'BUSINESS'
createdAt
string (ISO datetime)
{ "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

customerId
string
required
update: input
Partial<CreateCustomerInput>
required
Any subset of the create fields.

Request

customer = atoa.customer.get(id)
atoa.customer.update(id, email="[email protected]")
atoa.customer.delete(id)

Response

get / update
Customer
The Customer shape (above).
delete
DeleteResult
{ success, message }.

customer.list(opts?)

List the customers this agent created.

Parameters

opts
{ page?, size? }

Request

page = atoa.customer.list(page=0, size=20)

ResponsePage<Customer>

Page<Customer>
object
The pagination envelope; data is Customer[].

store · client-level

store.list(opts?)

List this business’s stores — discover a storeId to tag a collect by location.

Parameters

opts
{ page?, size? }

Request

stores = atoa.store.list()

ResponsePage<Store>

Page<Store>
object
The pagination envelope; data is Store[]. Pass a store’s id as storeId on a collect.

checkAvailability()

Unauthenticated health probe. Takes no parameters and never throws — check the returned flag.

Request

health = atoa.check_availability()

ResponseAvailabilityStatus

AvailabilityStatus
object
Whether the backend is reachable — inspect the flag rather than catching an error.

sandboxTestAccounts()

The sandbox SEND recipients and the outcome each forces. Takes no parameters; production has none.

Request

test = atoa.sandbox_test_accounts()

ResponseSandboxTestAccounts

SandboxTestAccounts
object
{ 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:

DirectionReasons
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
AnyUNKNOWN — read failureReasonDescription

Thrown AgentPayError subclasses:

codeThrown when
AUTH_ERROR401/403 — key invalid, not entitled, or a revoked agent.
VALIDATION_ERROR400/422 — malformed request.
NOT_FOUND404 — unknown or not-owned contract / payment / customer.
CONFLICT409 — re-registering an agent with a different key, env, or business.
REGISTRATION_ERRORThe register handshake failed.
RATE_LIMIT429 — back off exponentially and retry.
KEY_NOT_FOUNDNo signing key available for the requested id.
CONTRACT_CHARGE_ERRORBase 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_MISMATCHThe contract is unknown, not chargeable (.reason says why), the wrong type, or another customer’s.
CAP_EXCEEDEDOver a cap — .remaining is what’s left this window.
NO_PAYMENT_METHODNo usable method linked to the contract.
ATOA_CUSTOMER_REQUIREDA contract charge without atoaCustomerId.
PARAMS_CONFLICTMutually exclusive options in one call.
AUTHORIZATION_TIMEOUT / AUTHORIZATION_FAILEDawaitActive timed out / the approver declined or the link expired.
SETTLEMENT_TIMEOUTawaitSettled timed out.
SERVER_ERROR5xx — the service received the request but failed on its side. Retryable.
API_ERRORAn HTTP status the SDK doesn’t map to a specific class (402, 405, 408, …). status is set.
NETWORK_ERRORConnection failed (DNS/TLS/timeout) — the server never answered. Retryable.

Retry guidance:

  • Retry: RATE_LIMIT (back off), SERVER_ERROR, NETWORK_ERROR and timeouts — but re-read with get / awaitSettled first; the operation may still be in flight, and orderId is 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 FAILED is 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.

FlowHow the outcome is decided
SendRecipient account: 040004 / 12345678COMPLETED; 10000002, 10000003FAILED. 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 / cancelLeave a link unpaid past expiresInEXPIRED; call payment.cancelMERCHANT_CANCELLED.
ApprovalsOpen the approvalUrl and approve, decline, or let it lapse.
Failed refundpayment.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.

type
string
Always "APPROVAL" today; switch on it so new action kinds don’t break you.
approvalId
string
Pass to awaitDecision / cancelApproval.
clientSecret
string
Bearer secret for the hosted page / the approvals browser SDK.
approvalUrl
string
The hosted approval page to hand to the approver.
expiresAt
string (ISO datetime)
When the approval lapses.

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

  1. Production API key as ATOA_API_KEY, client with environment: "production".
  2. Signing key from a secrets manager, or a KMS signer — never a generated throwaway.
  3. Branch on all three shapes: returned COMPLETED, returned FAILED/CANCELLED with a failureReason, thrown AgentPayError.
  4. Deliver the approvalUrl to the approver (or drive the browser SDK) on every send and off-session charge; handle APPROVAL_DECLINED / APPROVAL_EXPIRED; observe with awaitDecision.
  5. Register webhooks for status changes; keep get / awaitSettled polling as a fallback.
  6. 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.