> ## Documentation Index
> Fetch the complete documentation index at: https://devdocs.paywithatoa.co.uk/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent Pay SDK Reference: Python & TypeScript

> Full Atoa Agent Pay SDK reference: authentication, KMS and custom signers, and every method for collecting and sending money in Python and TypeScript.

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

<CodeGroup>
  ```python Env key + PEM (default) theme={null}
  # 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")
  ```

  ```python Explicit API key theme={null}
  # Pass the key in (e.g. pulled from a secrets manager) instead of the env var.
  atoa = atoa_agent_pay.init(api_key=api_key, environment="sandbox", private_key_pem=private_key_pem)
  atoa.agent.register(name="Bookings assistant")
  ```

  ```python KMS / custom signer theme={null}
  # The signing key never enters the SDK, so you register its public key yourself.
  atoa = atoa_agent_pay.init(environment="sandbox", signer=signer)
  atoa.agent.register(name="Payouts worker", public_key_pem=public_key_pem)
  ```
</CodeGroup>

## 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.

<CodeGroup>
  ```python Python theme={null}
  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)
  ```

  ```typescript TypeScript theme={null}
  import { createAgentPayClient, type Signer } from "@atoapayments/agent-pay";

  const b64url = (b: Buffer | string) => Buffer.from(b).toString("base64url");

  const kmsSigner: Signer = {                     // the signer only signs — it never holds the key bytes
    sign(subject, kid) {
      const header = b64url(JSON.stringify(kid ? { alg: "ES256", kid } : { alg: "ES256" }));
      const input = `${header}.${b64url(subject)}`;
      const rawSig = derToRawEs256(myKms.signSha256Es256(input));   // DER → 64-byte r‖s
      return { jws: `${header}..${b64url(rawSig)}` };               // detached: empty middle segment
    },
  };

  const atoa = createAgentPayClient({ environment: "sandbox", signer: kmsSigner });
  await atoa.agent.register({ name: "Payouts worker", publicKeyPem: myPublicKeyPem });   // register the public key once
  ```
</CodeGroup>

## 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[]`.

<ResponseField name="Page<T>" type="object">
  <Expandable title="fields" defaultOpen>
    <ResponseField name="data" type="T[]">The page of items (`Contract` / `Payment` / `Customer` / `Store`).</ResponseField>
    <ResponseField name="totalCount" type="number">Total matching rows across all pages.</ResponseField>
    <ResponseField name="page" type="number">Zero-based index of this page.</ResponseField>
    <ResponseField name="size" type="number">Page size (default 20).</ResponseField>
  </Expandable>
</ResponseField>

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**

<ParamField body="name" type="string" required>
  Human-readable agent name shown in the Atoa dashboard.
</ParamField>

<ParamField body="description" type="string">
  Free-text description of what this agent does.
</ParamField>

<ParamField body="publicKeyPem" type="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).
</ParamField>

**Request**

<CodeGroup>
  ```python Python theme={null}
  agent = atoa.agent.register(name="Bookings assistant")
  ```

  ```typescript TypeScript theme={null}
  const agent = await atoa.agent.register({ name: "Bookings assistant" });
  ```
</CodeGroup>

**Response** → `RegisteredAgent`

<ResponseField name="agentId" type="string">The id this client signs as from now on.</ResponseField>
<ResponseField name="businessId" type="string">The business this agent belongs to.</ResponseField>

<ResponseField name="environment" type="'sandbox' | 'production'" />

<ResponseField name="name" type="string" />

<ResponseField name="description" type="string">Free-text description, if you set one.</ResponseField>
<ResponseField name="publicKeyPem" type="string">Echoed here **only** — `me`/`list` do not return it.</ResponseField>

<CodeGroup>
  ```json Example theme={null}
  {
    "agentId": "agt_9f2c…",
    "businessId": "biz_41a0…",
    "environment": "sandbox",
    "name": "Bookings assistant",
    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMFk…\n-----END PUBLIC KEY-----"
  }
  ```
</CodeGroup>

### `agent.me()`

Read this agent's own identity as the service sees it. Takes no parameters.

**Request**

<CodeGroup>
  ```python Python theme={null}
  self = atoa.agent.me()
  ```

  ```typescript TypeScript theme={null}
  const self = await atoa.agent.me();
  ```
</CodeGroup>

**Response** → `AgentIdentity`

<ResponseField name="agentId" type="string" />

<ResponseField name="businessId" type="string" />

<ResponseField name="environment" type="'sandbox' | 'production'" />

<ResponseField name="name" type="string" />

<ResponseField name="description" type="string">Free-text description, if you set one.</ResponseField>
<ResponseField name="status" type="string">e.g. `ACTIVE`.</ResponseField>

<CodeGroup>
  ```json Example theme={null}
  { "agentId": "agt_9f2c…", "businessId": "biz_41a0…", "environment": "sandbox", "name": "Bookings assistant", "status": "ACTIVE" }
  ```
</CodeGroup>

### `agent.list()`

List every agent registered under your business + environment. Authenticated by the SDK API key alone (no JWS).
Takes no parameters.

**Request**

<CodeGroup>
  ```python Python theme={null}
  all = atoa.agent.list()
  ```

  ```typescript TypeScript theme={null}
  const all = await atoa.agent.list();
  ```
</CodeGroup>

**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**

<ParamField body="name" type="string" required>
  Your label for this spending authority (e.g. "Supplier payouts").
</ParamField>

<ParamField body="type" type="'SEND' | 'COLLECT'" default="SEND">
  `SEND` (money out) or `COLLECT` (money in, off-session).
</ParamField>

<ParamField body="atoaCustomerId" type="string">
  **Required for `COLLECT`** — the customer this contract charges (`id` from `customer.create`).
  Ignored for SEND.
</ParamField>

<ParamField body="description" type="string">
  Free-text description.
</ParamField>

<ParamField body="limits" type="ContractLimitsInput" required>
  The per-payment cap, ≥1 period cap, and validity window.

  <Expandable title="ContractLimitsInput" defaultOpen>
    <ParamField body="maxPerPayment" type="number" required>Max for any single payment, in the contract currency.</ParamField>

    <ParamField body="periodLimits" type="{ amount, period, alignment? }[]" required>
      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.
    </ParamField>

    <ParamField body="validTo" type="string (ISO datetime)" required>When the consent expires — must be bounded.</ParamField>
    <ParamField body="validFrom" type="string (ISO datetime)" default="now">When the consent starts.</ParamField>
    <ParamField body="currency" type="string" default="GBP">The single currency for every cap in this contract.</ParamField>
  </Expandable>
</ParamField>

**Request**

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

  ```typescript TypeScript theme={null}
  const contract = await atoa.contract.create({
    name: "Supplier payouts",
    limits: {
      maxPerPayment: 50.0,
      periodLimits: [{ amount: 500.0, period: "MONTH" }],
      validTo: "2026-12-31T23:59:59Z",
    },
  });
  // → open contract.authorizationUrl, then contract.awaitActive(contract.contractId)
  ```
</CodeGroup>

**Response** → `Contract`

<ResponseField name="contractId" type="string" />

<ResponseField name="agentId" type="string">The agent that owns this contract.</ResponseField>

<ResponseField name="environment" type="'sandbox' | 'production'" />

<ResponseField name="type" type="'SEND' | 'COLLECT'">Emitted on every row so you can branch without inference.</ResponseField>

<ResponseField name="createdAt" type="string (ISO datetime)" />

<ResponseField name="status" type="ContractStatus">Fresh from `create`: `PENDING_AUTHORIZATION`.</ResponseField>
<ResponseField name="authorizationUrl" type="string">The page the human authorizes at (create/update only — **absent** on `get`/`list`).</ResponseField>
<ResponseField name="limits" type="ContractLimits">The caps echoed back with defaults resolved.</ResponseField>
<ResponseField name="usage" type="ContractUsageWindow[]">Live per-period headroom (populated on `get` — see below).</ResponseField>
<ResponseField name="atoaCustomerId" type="string">**COLLECT only** — the customer this contract charges.</ResponseField>
<ResponseField name="linkedMethod" type="{ paymentMethodId, lastFourDigits?, brand?, expiryDate? }">**COLLECT only** — the masked card the customer linked. Absent until they link one on the contract page.</ResponseField>
<ResponseField name="termsVersion" type="number">**COLLECT only** — authorized terms version (1 on first approval; increments per approved update).</ResponseField>
<ResponseField name="pendingUpdate" type="{ limits, requestedAt } | null">A staged limits change; the `limits` above stay enforced until it's authorized. `null` when nothing is staged.</ResponseField>

<CodeGroup>
  ```json Example theme={null}
  {
    "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"
    }
  }
  ```
</CodeGroup>

### `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**

<ParamField body="contractId" type="string" required />

<ParamField body="opts.timeoutMs" type="number">
  How long to wait before throwing `AUTHORIZATION_TIMEOUT`.
</ParamField>

**Request**

<CodeGroup>
  ```python Python theme={null}
  active = atoa.contract.await_active(contract.contract_id)
  ```

  ```typescript TypeScript theme={null}
  const active = await atoa.contract.awaitActive(contract.contractId);
  ```
</CodeGroup>

**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**

<ParamField body="contractId" type="string" required />

**Request**

<CodeGroup>
  ```python Python theme={null}
  contract = atoa.contract.get(id)
  ```

  ```typescript TypeScript theme={null}
  const contract = await atoa.contract.get(id);
  ```
</CodeGroup>

**Response** → `Contract`

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

<ResponseField name="usage" type="ContractUsageWindow[]">
  One window per period cap — `remaining = cap − usedThisPeriod − reserved`.

  <Expandable title="ContractUsageWindow">
    <ResponseField name="period" type="PeriodUnit">`DAY` · `WEEK` · `FORTNIGHT` · `MONTH` · `HALF_YEAR` · `YEAR`.</ResponseField>
    <ResponseField name="cap" type="number">The limit for this window.</ResponseField>
    <ResponseField name="usedThisPeriod + reserved + remaining" type="number">Settled spend, in-flight (initiated, unsettled) spend, and headroom left.</ResponseField>
    <ResponseField name="periodEnd" type="string (ISO datetime)">When this window and its headroom reset.</ResponseField>
  </Expandable>
</ResponseField>

### `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**

<ParamField body="opts" type="ListContractsOptions">
  All filters optional. `listAll` takes the same shape minus `page`.

  <Expandable title="ListContractsOptions">
    <ParamField body="type" type="'SEND' | 'COLLECT'">Narrow to one direction.</ParamField>
    <ParamField body="status" type="ContractStatus">Narrow to one status.</ParamField>
    <ParamField body="atoaCustomerId" type="string">**COLLECT only** — narrow to one customer's contracts.</ParamField>

    <ParamField body="environment" type="'sandbox' | 'production'" />

    <ParamField body="page" type="number" default="0">Zero-based page index.</ParamField>
    <ParamField body="size" type="number" default="20">Page size.</ParamField>
  </Expandable>
</ParamField>

**Request**

<CodeGroup>
  ```python Python theme={null}
  page = atoa.contract.list(type="SEND", status="ACTIVE")
  all = atoa.contract.list_all(type="COLLECT")
  ```

  ```typescript TypeScript theme={null}
  const page = await atoa.contract.list({ type: "SEND", status: "ACTIVE" });
  const all = await atoa.contract.listAll({ type: "COLLECT" });
  ```
</CodeGroup>

**Response** → `Page<Contract>` (`listAll` → `Contract[]`)

<ResponseField name="list" type="Page<Contract>">The [pagination envelope](#methods); `data` is `Contract[]`.</ResponseField>
<ResponseField name="listAll" type="Contract[]">Every page flattened.</ResponseField>

### `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**

<ParamField body="contractId" type="string" required />

<ParamField body="input" type="{ limits: ContractLimitsInput }" required>The new caps (same shape as `create`).</ParamField>

**Request**

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

  ```typescript TypeScript theme={null}
  const reconsent = await atoa.contract.update(id, {
    limits: { maxPerPayment: 75.0, periodLimits: [{ amount: 750.0, period: "MONTH" }], validTo: "2027-01-31T23:59:59Z" },
  });
  ```
</CodeGroup>

**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**

<ParamField body="contractId" type="string" required />

**Request**

<CodeGroup>
  ```python Python theme={null}
  result = atoa.contract.revoke(id)
  ```

  ```typescript TypeScript theme={null}
  const result = await atoa.contract.revoke(id);
  ```
</CodeGroup>

**Response** → `ContractRevokeResult`

<ResponseField name="ContractRevokeResult" type="object">`{ contractId, type, status }` — the lean terminal confirmation.</ResponseField>

## 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**

<ParamField body="amount" type="{ amount, currency? }" required>The amount to collect.</ParamField>

<ParamField body="orderId" type="string" required>
  Your order reference — echoed on the payment, webhooks, and dashboard. Not an idempotency key.
</ParamField>

<ParamField body="contractId" type="string">
  Present → off-session charge on this `COLLECT` contract's linked method. Absent → a pay-link/QR the customer pays
  now.
</ParamField>

<ParamField body="atoaCustomerId" type="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.
</ParamField>

<ParamField body="customerId" type="string">
  YOUR id for the payer, echoed back everywhere. Omit for guest checkout (one is synthesized).
</ParamField>

<ParamField body="customer" type="{ fullName?, email?, phoneCountryCode?, phoneNumber? }">
  Prefill the checkout with the customer's details.
</ParamField>

<ParamField body="redirectUrl" type="string">Where the customer lands after paying.</ParamField>
<ParamField body="expiresIn" type="number (ms)" default="180000">Pay-link lifetime (default 3 minutes).</ParamField>

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

<ParamField body="paymentMethod" type="('PAY_BY_BANK' | 'CARD')[]">Restrict how the customer may pay. Both show by default.</ParamField>
<ParamField body="savePaymentMethod" type="boolean">Save the card during checkout. Needs `atoaCustomerId` + `paymentMethod: ['CARD']`; incompatible with `splitBill`.</ParamField>
<ParamField body="storeId" type="string">Charge under a specific store (see `store.list`); defaults to the primary store.</ParamField>
<ParamField body="template" type="'EXTERNAL_DISPLAY' | 'EXTERNAL_DISPLAY_PNG' | 'RECEIPT' | 'RECEIPT_PNG'">Till/receipt QR template — sets `templateUrl`.</ParamField>
<ParamField body="allowRetry" type="boolean" default="true">Allow several attempts while the link is live.</ParamField>

<ParamField body="enableTips / strictExpiry / splitBill" type="boolean" />

<ParamField body="callbackParams" type="object">Echoed back to your `redirectUrl` after payment.</ParamField>
<ParamField body="notes" type="string">Free-text note captured on the payment.</ParamField>

**Request**

<CodeGroup>
  ```python Pay-link theme={null}
  req = atoa.payment.collect(amount={"amount": 45.0}, order_id="booking-8812")
  ```

  ```typescript Pay-link theme={null}
  const req = await atoa.payment.collect({ amount: { amount: 45.0 }, orderId: "booking-8812" });
  // req.paymentUrl → give the customer the link or QR
  ```

  ```typescript Off-session theme={null}
  const req = await atoa.payment.collect({
    amount: { amount: 9.99 },
    orderId: "sub-2026-07",
    contractId: "ctr_col_5a…",
    atoaCustomerId: "cus_1f…",
  });
  ```
</CodeGroup>

**Response** → `PaymentRequest`

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

<CodeGroup>
  ```json Pay-link theme={null}
  {
    "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"
  }
  ```
</CodeGroup>

### `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**

<ParamField body="contractId" type="string" required>The `ACTIVE` SEND contract every instruction is paid against.</ParamField>

<ParamField body="payments" type="SendPaymentInstruction[]" required>
  1–20 instructions; each `orderId` must be unique in the batch.

  <Expandable title="SendPaymentInstruction">
    <ParamField body="amount" type="{ amount, currency? }" required />

    <ParamField body="beneficiary" type="{ name, sortCode, accountNumber }" required>`name` is checked by Confirmation of Payee; `sortCode` 6-digit, `accountNumber` 8-digit.</ParamField>
    <ParamField body="orderId" type="string" required>Your reference, passed through. **Not** an idempotency key.</ParamField>
    <ParamField body="reference" type="string">Shown to the beneficiary (1–35 chars).</ParamField>
  </Expandable>
</ParamField>

**Request**

<CodeGroup>
  ```python Python theme={null}
  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",
      }],
  )
  ```

  ```typescript TypeScript theme={null}
  const [payment] = await atoa.payment.send({
    contractId: contract.contractId,
    payments: [
      {
        amount: { amount: 12.5 },
        beneficiary: { name: "ACME LTD", sortCode: "040004", accountNumber: "12345678" },
        orderId: "order-9281",
      },
    ],
  });
  const settled = await atoa.payment.awaitSettled(payment.paymentIdempotencyId);
  ```
</CodeGroup>

**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](#approval-gate))*. See `payment.get` for the
full `Payment` shape.

<CodeGroup>
  ```json Example theme={null}
  [
    {
      "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…"
    }
  ]
  ```
</CodeGroup>

### `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**

<ParamField body="id" type="string" required>A `paymentRequestId` or a `paymentIdempotencyId`.</ParamField>

**Request**

<CodeGroup>
  ```python Python theme={null}
  payment = atoa.payment.get(payment_request_id)
  ```

  ```typescript TypeScript theme={null}
  const payment = await atoa.payment.get(paymentRequestId);
  ```
</CodeGroup>

**Response** → `Payment`

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

<CodeGroup>
  ```json Example theme={null}
  {
    "type": "CREDIT",
    "status": "COMPLETED",
    "paidAmount": 45.0,
    "currency": "GBP",
    "orderId": "booking-8812",
    "paymentIdempotencyId": "ATOA1780…",
    "paymentRequestId": "prq_a81f…"
  }
  ```
</CodeGroup>

### `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**

<ParamField body="id" type="string" required>A `paymentRequestId` or a `paymentIdempotencyId`.</ParamField>
<ParamField body="opts.timeoutMs" type="number">How long to wait before throwing `SETTLEMENT_TIMEOUT`.</ParamField>

**Request**

<CodeGroup>
  ```python Python theme={null}
  settled = atoa.payment.await_settled(payment_request_id)
  ```

  ```typescript TypeScript theme={null}
  const settled = await atoa.payment.awaitSettled(paymentRequestId);
  ```
</CodeGroup>

**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**

<ParamField body="opts" type="ListPaymentsOptions">
  All filters optional. `listAll` takes the same shape minus `page`.

  <Expandable title="ListPaymentsOptions">
    <ParamField body="type" type="'SEND' | 'COLLECT'">Direction — `SEND` (money out) or `COLLECT` (money in). Rows come back with `type` `DEBIT`/`CREDIT` respectively.</ParamField>
    <ParamField body="status" type="PaymentStatus">Narrow to one status.</ParamField>
    <ParamField body="contractId" type="string">**SEND only** — narrow to one contract's draws.</ParamField>
    <ParamField body="paymentRequestId" type="string">**COLLECT only** — the attempts of one payment request (parent → children).</ParamField>
    <ParamField body="atoaCustomerIds" type="string[]">**COLLECT only** — narrow to specific payers by their Atoa customer id(s).</ParamField>
    <ParamField body="page" type="number" default="0">Zero-based page index.</ParamField>
    <ParamField body="size" type="number" default="20">Page size.</ParamField>
  </Expandable>
</ParamField>

**Request**

<CodeGroup>
  ```python Python theme={null}
  page = atoa.payment.list(type="COLLECT", status="COMPLETED")
  all = atoa.payment.list_all(type="SEND")
  ```

  ```typescript TypeScript theme={null}
  const page = await atoa.payment.list({ type: "COLLECT", status: "COMPLETED" });
  const all = await atoa.payment.listAll({ type: "SEND" });
  ```
</CodeGroup>

**Response** → `Page<Payment>` (`listAll` → `Payment[]`)

<ResponseField name="list" type="Page<Payment>">The [pagination envelope](#methods); `data` is `Payment[]` (shape above).</ResponseField>
<ResponseField name="listAll" type="Payment[]">Every page flattened.</ResponseField>

### `payment.refund(id, input)`

Refund a `COMPLETED` collected payment — full or partial.

**Parameters**

<ParamField body="paymentRequestId" type="string" required>The collected payment to refund.</ParamField>

<ParamField body="input" type="CreateRefundInput" required>
  `{ amount, reason? }` — the amount must not exceed the paid amount. Sandbox: `reason: "FAILURE TEST"` forces a
  `FAILED` refund.
</ParamField>

**Request**

<CodeGroup>
  ```python Python theme={null}
  refund = atoa.payment.refund(payment_request_id, amount={"amount": 45.0}, reason="Order returned")
  ```

  ```typescript TypeScript theme={null}
  const refund = await atoa.payment.refund(paymentRequestId, { amount: { amount: 45.0 }, reason: "Order returned" });
  ```
</CodeGroup>

**Response** → `Refund`

<ResponseField name="refundId" type="string">The refund id — pass to `cancelRefund` while still `INITIATED`.</ResponseField>
<ResponseField name="paymentRequestId" type="string">The collected payment this refund belongs to.</ResponseField>

<ResponseField name="createdAt" type="string (ISO datetime)" />

<ResponseField name="status" type="RefundStatus">`INITIATED` · `COMPLETED` · `FAILED` · `CANCELLED`.</ResponseField>
<ResponseField name="refundAmount" type="Amount">The refunded amount (`refund_amount`, a float, in Python).</ResponseField>
<ResponseField name="paidAmount" type="Amount">The original paid amount.</ResponseField>

<CodeGroup>
  ```json Example theme={null}
  { "refundId": "ref_2b8c…", "status": "INITIATED", "refundAmount": { "amount": 45.0, "currency": "GBP" }, "paidAmount": { "amount": 45.0, "currency": "GBP" } }
  ```
</CodeGroup>

### `payment.listRefunds(id)` · `payment.cancelRefund(refundId)`

List the refunds of one collected payment, or cancel a still-`INITIATED` refund.

**Parameters**

<ParamField body="listRefunds: paymentRequestId" type="string" required />

<ParamField body="cancelRefund: refundId" type="string" required>Only a not-yet-processed refund can be cancelled.</ParamField>

**Request**

<CodeGroup>
  ```python Python theme={null}
  refunds = atoa.payment.list_refunds(payment_request_id)
  atoa.payment.cancel_refund(refunds[0].refund_id)
  ```

  ```typescript TypeScript theme={null}
  const refunds = await atoa.payment.listRefunds(paymentRequestId);
  await atoa.payment.cancelRefund(refunds[0].refundId);
  ```
</CodeGroup>

**Response**

<ResponseField name="listRefunds" type="Refund[]">The refunds of the payment.</ResponseField>
<ResponseField name="cancelRefund" type="CancelRefundResult">`{ refundId, message? }`.</ResponseField>

### `payment.cancel(id)`

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

**Parameters**

<ParamField body="paymentRequestId" type="string" required />

**Request**

<CodeGroup>
  ```python Python theme={null}
  cancelled = atoa.payment.cancel(payment_request_id)
  ```

  ```typescript TypeScript theme={null}
  const cancelled = await atoa.payment.cancel(paymentRequestId);
  ```
</CodeGroup>

**Response** → `Payment`

The `CANCELLED` payment.

### `payment.awaitDecision(approvalId)`

Poll an approval (from a `send`/`collect` `nextAction`) to its terminal decision. See the
[approval gate](#approval-gate) for the flow.

**Parameters**

<ParamField body="approvalId" type="string" required>From a `nextAction`.</ParamField>
<ParamField body="opts.timeoutMs" type="number">How long to wait before throwing `SETTLEMENT_TIMEOUT`.</ParamField>

**Request**

<CodeGroup>
  ```python Python theme={null}
  decision = atoa.payment.await_decision(next_action.approval_id)
  if decision.status != "APPROVED":
      ...  # handle
  ```

  ```typescript TypeScript theme={null}
  const decision = await atoa.payment.awaitDecision(nextAction.approvalId);
  if (decision.status !== "APPROVED") { /* handle */ }
  ```
</CodeGroup>

**Response** → `Approval`

<ResponseField name="approvalId" type="string" />

<ResponseField name="type" type="'SEND' | 'COLLECT'">Which side the approval gates.</ResponseField>
<ResponseField name="contractId" type="string">The contract this approval is bound to.</ResponseField>
<ResponseField name="status" type="ApprovalStatus">`PENDING` · `APPROVED` · `DECLINED` · `CANCELLED` · `EXPIRED` · `SUPERSEDED`.</ResponseField>
<ResponseField name="expiresAt" type="string (ISO datetime)">When the approval lapses.</ResponseField>

<ResponseField name="createdAt" type="string (ISO datetime)" />

<ResponseField name="decidedBy + decidedAt" type="string">Who decided and when, once resolved.</ResponseField>

### `payment.cancelApproval(contractId, approvalId)`

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

**Parameters**

<ParamField body="contractId" type="string" required />

<ParamField body="approvalId" type="string" required />

**Request**

<CodeGroup>
  ```python Python theme={null}
  atoa.payment.cancel_approval(contract_id, approval_id)
  ```

  ```typescript TypeScript theme={null}
  await atoa.payment.cancelApproval(contractId, approvalId);
  ```
</CodeGroup>

**Response**

<ResponseField name="result" type="{ approvalId, status }">The cancelled approval id + its new status.</ResponseField>

## 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**

<ParamField body="fullName" type="string" required>2–30 characters.</ParamField>
<ParamField body="email" type="string">Valid email. **Either `email` or a phone number is required.**</ParamField>
<ParamField body="phoneCountryCode" type="string">Digits only (e.g. `44`). Pair with `phoneNumber`.</ParamField>
<ParamField body="phoneNumber" type="string">Digits only, without country code.</ParamField>

<ParamField body="type" type="'INDIVIDUAL' | 'BUSINESS'" default="INDIVIDUAL" />

<ParamField body="vatNumber" type="string">Business customers only.</ParamField>
<ParamField body="address, city, postcode" type="string">Optional postal details.</ParamField>

**Request**

<CodeGroup>
  ```python Python theme={null}
  customer = atoa.customer.create(full_name="Jane Doe", email="jane@example.com")
  ```

  ```typescript TypeScript theme={null}
  const customer = await atoa.customer.create({ fullName: "Jane Doe", email: "jane@example.com" });
  ```
</CodeGroup>

**Response** → `Customer`

<ResponseField name="id" type="string">The `atoaCustomerId` used across collect + saved cards.</ResponseField>

<ResponseField name="fullName" type="string" />

<ResponseField name="email / phoneNumber" type="string" />

<ResponseField name="type" type="'INDIVIDUAL' | 'BUSINESS'" />

<ResponseField name="createdAt" type="string (ISO datetime)" />

<CodeGroup>
  ```json Example theme={null}
  { "id": "cus_1f…", "fullName": "Jane Doe", "email": "jane@example.com", "type": "INDIVIDUAL", "createdAt": "2026-07-23T10:00:00Z" }
  ```
</CodeGroup>

### `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**

<ParamField body="customerId" type="string" required />

<ParamField body="update: input" type="Partial<CreateCustomerInput>" required>Any subset of the create fields.</ParamField>

**Request**

<CodeGroup>
  ```python Python theme={null}
  customer = atoa.customer.get(id)
  atoa.customer.update(id, email="new@example.com")
  atoa.customer.delete(id)
  ```

  ```typescript TypeScript theme={null}
  const customer = await atoa.customer.get(id);
  await atoa.customer.update(id, { email: "new@example.com" });
  await atoa.customer.delete(id);
  ```
</CodeGroup>

**Response**

<ResponseField name="get / update" type="Customer">The `Customer` shape (above).</ResponseField>
<ResponseField name="delete" type="DeleteResult">`{ success, message }`.</ResponseField>

### `customer.list(opts?)`

List the customers this agent created.

**Parameters**

<ParamField body="opts" type="{ page?, size? }">
  <Expandable title="fields">
    <ParamField body="page" type="number" default="0">Zero-based page index.</ParamField>
    <ParamField body="size" type="number" default="20">Page size.</ParamField>
  </Expandable>
</ParamField>

**Request**

<CodeGroup>
  ```python Python theme={null}
  page = atoa.customer.list(page=0, size=20)
  ```

  ```typescript TypeScript theme={null}
  const page = await atoa.customer.list({ page: 0, size: 20 });
  ```
</CodeGroup>

**Response** → `Page<Customer>`

<ResponseField name="Page<Customer>" type="object">The [pagination envelope](#methods); `data` is `Customer[]`.</ResponseField>

## store · client-level

### `store.list(opts?)`

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

**Parameters**

<ParamField body="opts" type="{ page?, size? }">
  <Expandable title="fields">
    <ParamField body="page" type="number" default="0">Zero-based page index.</ParamField>
    <ParamField body="size" type="number" default="20">Page size.</ParamField>
  </Expandable>
</ParamField>

**Request**

<CodeGroup>
  ```python Python theme={null}
  stores = atoa.store.list()
  ```

  ```typescript TypeScript theme={null}
  const stores = await atoa.store.list();
  ```
</CodeGroup>

**Response** → `Page<Store>`

<ResponseField name="Page<Store>" type="object">The [pagination envelope](#methods); `data` is `Store[]`. Pass a store's `id` as `storeId` on a collect.</ResponseField>

### `checkAvailability()`

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

**Request**

<CodeGroup>
  ```python Python theme={null}
  health = atoa.check_availability()
  ```

  ```typescript TypeScript theme={null}
  const health = await atoa.checkAvailability();
  ```
</CodeGroup>

**Response** → `AvailabilityStatus`

<ResponseField name="AvailabilityStatus" type="object">Whether the backend is reachable — inspect the flag rather than catching an error.</ResponseField>

### `sandboxTestAccounts()`

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

**Request**

<CodeGroup>
  ```python Python theme={null}
  test = atoa.sandbox_test_accounts()
  ```

  ```typescript TypeScript theme={null}
  const test = atoa.sandboxTestAccounts();
  ```
</CodeGroup>

**Response** → `SandboxTestAccounts`

<ResponseField name="SandboxTestAccounts" type="object">`{ sandbox, note, accounts }` — the recipient accounts and their forced outcomes.</ResponseField>

## 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_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.

| 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.

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

## 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**](/agent-pay/approvals).

```typescript theme={null}
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`.

<Warning>
  The credential — code or passkey — is entered only on Atoa's page. Never build your own form that collects it.
</Warning>

## 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](#kms-custom-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](/api-reference/Webhook/introduction) 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.
