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

# Embedded Checkout for Agent Pay

> Render Atoa's checkout inside your own product with one mount call. Your server creates the payment; the browser displays it.

Show Atoa's checkout inside your own product.

Your server creates a payment and sends its `nextAction` to the browser. Your browser passes that to
`AtoaUI.mount()` and gets a callback when the customer is done.

<Frame caption="Your thread, your container — the checkout is the only part Atoa draws.">
  <img className="block dark:hidden" src="https://mintcdn.com/atoaproddocs/i1RSfhZLVHfg2IA3/images/agent-pay/embed/placement-chat-light.png?fit=max&auto=format&n=i1RSfhZLVHfg2IA3&q=85&s=4a85aa1e6cbe4dbd85ec63f02ee20207" alt="A chat thread drawn as grey placeholder bubbles, with the Atoa checkout rendered inside one of them and outlined as your checkout div." width="2080" height="1860" data-path="images/agent-pay/embed/placement-chat-light.png" />

  <img className="hidden dark:block" src="https://mintcdn.com/atoaproddocs/i1RSfhZLVHfg2IA3/images/agent-pay/embed/placement-chat-dark.png?fit=max&auto=format&n=i1RSfhZLVHfg2IA3&q=85&s=27521485645d6cd9372dde47329711cc" alt="A chat thread drawn as grey placeholder bubbles, with the Atoa checkout rendered inside one of them and outlined as your checkout div." width="2080" height="1860" data-path="images/agent-pay/embed/placement-chat-dark.png" />
</Frame>

## Install

<CodeGroup>
  ```bash npm theme={null}
  npm install @atoapayments/pay-embed
  # Already using @atoapayments/agent-pay? Import from @atoapayments/agent-pay/ui
  # instead — same API. It's an optional peer dependency, so install this too.
  ```

  ```html Script tag theme={null}
  <script src="https://unpkg.com/@atoapayments/pay-embed@0.0.1/dist/index.global.js"></script>
  <script>
    const { AtoaUI } = window.AtoaUI;
  </script>
  ```
</CodeGroup>

Pin an exact version in production. A payment surface should not change under you between deploys.

## Create a payment

The API key creates payments, so it stays on your server and never reaches the browser. **The embed needs a
backend.** If you don't have one, send the customer a [pay-link](/agent-pay/collect) instead.

Your server creates the payment and returns its `nextAction`:

<CodeGroup>
  ```python Python theme={null}
  import atoa_agent_pay

  private_key_pem, _ = atoa_agent_pay.generate_es256_keypair()
  atoa = atoa_agent_pay.init(environment="sandbox", private_key_pem=private_key_pem)
  atoa.agent.register(name="Bookings assistant")   # required before any payment call

  @app.post("/api/checkout")
  def checkout(order_id: str):
      req = atoa.payment.collect(
          amount={"amount": 45.00},
          order_id=order_id,
      )
      return {"nextAction": req.next_action}   # this, and nothing else
  ```

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

  const atoa = createAgentPayClient({ environment: "sandbox" });   // reads ATOA_API_KEY
  await atoa.agent.register({ name: "Bookings assistant" });       // required before any payment call

  app.post("/api/checkout", async (req, res) => {
    const result = await atoa.payment.collect({
      amount: { amount: 45.00 },
      orderId: req.body.orderId,
    });

    res.json({ nextAction: result.nextAction });   // this, and nothing else
  });
  ```
</CodeGroup>

<Warning>
  Send the `nextAction`, not the result. A full Agent Pay result carries beneficiary details, contact details
  and your own metadata — none of which the checkout reads.
</Warning>

<Info>
  Approval secrets travel in the URL **fragment**. Don't copy them into query strings, logs, or your own code.
</Info>

## Mount the checkout

Fetch it and mount it. Set your container's width — the iframe sets its own height.

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { AtoaUI } from "@atoapayments/pay-embed";

  const { nextAction } = await fetch("/api/checkout", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ orderId: "booking-8812" }),
  }).then((r) => r.json());

  const handle = AtoaUI.mount(nextAction, {
    container: document.querySelector("#checkout"),
    environment: "sandbox",
    orderId: "booking-8812",                      // one live mount per order
    theme: { mode: "auto" },
    onPaymentCompleted: (e) => showReceipt(e),
    onError: (e) => console.error(e.kind, e.message),
  });
  ```

  ```html HTML theme={null}
  <div id="checkout" style="max-width: 420px"></div>

  <script src="https://unpkg.com/@atoapayments/pay-embed@0.0.1/dist/index.global.js"></script>
  <script>
    const { AtoaUI } = window.AtoaUI;

    fetch("/api/checkout", { method: "POST" })
      .then((r) => r.json())
      .then(({ nextAction }) => {
        AtoaUI.mount(nextAction, {
          container: document.getElementById("checkout"),
          environment: "sandbox",
          onPaymentCompleted: (e) => console.log("completed", e.data),
          onError: (e) => console.error(e.kind, e.message),
        });
      });
  </script>
  ```

  ```tsx React theme={null}
  import { useEffect, useRef } from "react";
  import { AtoaUI } from "@atoapayments/pay-embed";

  function Checkout({ nextAction, orderId }: { nextAction: unknown; orderId: string }) {
    const ref = useRef<HTMLDivElement>(null);

    useEffect(() => {
      if (!ref.current) return;

      const handle = AtoaUI.mount(nextAction, {
        container: ref.current,
        environment: "sandbox",
        orderId,
        onPaymentCompleted: (e) => console.log(e.data),
        onError: (e) => console.error(e.kind, e.message),
      });

      return () => handle.destroy();     // React 18 StrictMode remounts — destroy is idempotent
    }, [nextAction, orderId]);

    return <div ref={ref} style={{ maxWidth: 420 }} />;
  }
  ```

  ```vue Vue theme={null}
  <script setup lang="ts">
  import { onMounted, onUnmounted, ref } from "vue";
  import { AtoaUI, type Handle } from "@atoapayments/pay-embed";

  const props = defineProps<{ nextAction: unknown; orderId: string }>();
  const host = ref<HTMLDivElement | null>(null);
  let handle: Handle | null = null;

  onMounted(() => {
    if (!host.value) return;

    handle = AtoaUI.mount(props.nextAction, {
      container: host.value,
      environment: "sandbox",
      orderId: props.orderId,
      onPaymentCompleted: (e) => console.log(e.data),
      onError: (e) => console.error(e.kind, e.message),
    });
  });

  onUnmounted(() => handle?.destroy());
  </script>

  <template>
    <div ref="host" style="max-width: 420px" />
  </template>
  ```

  ```typescript Angular theme={null}
  import { AfterViewInit, Component, ElementRef, Input, OnDestroy, ViewChild } from "@angular/core";
  import { AtoaUI, type Handle } from "@atoapayments/pay-embed";

  @Component({
    selector: "atoa-checkout",
    standalone: true,
    template: `<div #host style="max-width: 420px"></div>`,
  })
  export class AtoaCheckoutComponent implements AfterViewInit, OnDestroy {
    @Input({ required: true }) nextAction!: unknown;
    @Input() orderId?: string;
    @ViewChild("host") host!: ElementRef<HTMLDivElement>;

    private handle?: Handle;

    ngAfterViewInit(): void {
      this.handle = AtoaUI.mount(this.nextAction, {
        container: this.host.nativeElement,
        environment: "sandbox",
        orderId: this.orderId,
        onPaymentCompleted: (e) => console.log(e.data),
        onError: (e) => console.error(e.kind, e.message),
      });
    }

    ngOnDestroy(): void {
      this.handle?.destroy();
    }
  }
  ```
</CodeGroup>

`mount()` returns a [handle](#the-handle) for unmounting and refreshing the surface. Destroy it when your
component unmounts, or the iframe outlives the view that owned it.

There is no method to create a payment from the browser, so re-rendering or reloading always shows the same
payment rather than creating a second one.

## Payment surfaces

`mount()` reads `nextAction.type` and shows the right screen — you don't branch on it.

<Frame caption="The same mount call, four of the screens it can produce.">
  <img className="block dark:hidden" src="https://mintcdn.com/atoaproddocs/i1RSfhZLVHfg2IA3/images/agent-pay/embed/surfaces-gallery-light.png?fit=max&auto=format&n=i1RSfhZLVHfg2IA3&q=85&s=b22e328c6cc02b68c3388be71611152e" alt="Four checkout screens in a grid: the bank picker, an approval with Face ID, contract setup with saved cards, and the add-a-card form." width="2080" height="2716" data-path="images/agent-pay/embed/surfaces-gallery-light.png" />

  <img className="hidden dark:block" src="https://mintcdn.com/atoaproddocs/i1RSfhZLVHfg2IA3/images/agent-pay/embed/surfaces-gallery-dark.png?fit=max&auto=format&n=i1RSfhZLVHfg2IA3&q=85&s=210aa03a02e632c40a1d512e7364f8c7" alt="Four checkout screens in a grid: the bank picker, an approval with Face ID, contract setup with saved cards, and the add-a-card form." width="2080" height="2716" data-path="images/agent-pay/embed/surfaces-gallery-dark.png" />
</Frame>

| `nextAction.type`    | Screen                                                                   |
| -------------------- | ------------------------------------------------------------------------ |
| `PAY`                | Bank picker and checkout                                                 |
| `APPROVAL`           | Approval — one-time code or passkey                                      |
| `AUTHORIZE_CONTRACT` | Contract setup — `data.kind: 'cvrp'` shows the bank-consent page instead |
| `NONE`               | Receipt                                                                  |
| Anything else        | The action's `fallback` text and link                                    |

Those four are the whole set an Agent Pay call returns. Picking a card, entering card details and enrolling a
passkey are steps **inside** those screens, on Atoa's origin — they don't come back to you as actions to render.

The last row is the forward-compatibility rule: a type this version doesn't know renders the action's
`fallback`, so Atoa can add one without breaking your build. See
[the nextAction reference](/agent-pay/next-action).

## Handle events

Domain events tell you what happened to the money. Every one arrives in the same envelope:

```typescript theme={null}
{
  specVersion: 1,                    // bumps only on a breaking shape change
  type: "payment.completed",         // resource.event — an open string, not an enum
  resource: "payment",               // always the segment before the dot
  resourceId: "prq_a81f…",           // the subject's id; null when the SDK holds none
  occurredAt: "2026-08-25T10:04:11.201Z",  // when the SDK saw it, not when money moved
  livemode: false,                   // true only against production
  data: { status: "COMPLETED" }      // per type, below
}
```

Which resource the event names follows from what you mounted: a payment surface emits `payment.*`, a contract
surface `contract.*`, an approval surface `approval.*`. The subject's id is always on the envelope as
`resourceId` — read it there rather than out of `data`.

| `type`               | Callback              | Emitted when                                                                                                                                                                     | `data`                                               |
| -------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `payment.completed`  | `onPaymentCompleted`  | The payment settled.                                                                                                                                                             | `paymentRequestId`, `status`, `iso?`                 |
| `payment.cancelled`  | `onPaymentCancelled`  | The customer cancelled.                                                                                                                                                          | `status`, `reason`                                   |
| `payment.expired`    | `onPaymentExpired`    | The payment request expired unpaid.                                                                                                                                              | `status`, `reason`                                   |
| `payment.failed`     | `onPaymentFailed`     | Only when the frame reports a failure with no specific reason. **A declined payment normally stays on screen for a retry**, so a real `FAILED` reaches you by polling, not here. | `status`                                             |
| `contract.activated` | `onContractActivated` | The contract reached `ACTIVE`.                                                                                                                                                   | `status`, `iso?` — plus `contractId` on `resourceId` |
| `contract.declined`  | `onContractDeclined`  | The customer declined the contract.                                                                                                                                              | `reason`                                             |
| `approval.approved`  | `onApprovalApproved`  | The approver passed SCA. Approval is the *first* step — the payment can still fail afterwards.                                                                                   | `status`                                             |
| `approval.declined`  | `onApprovalDeclined`  | The approver declined.                                                                                                                                                           | `reason`                                             |
| `approval.expired`   | `onApprovalExpired`   | The approval window lapsed undecided.                                                                                                                                            | `reason`                                             |

`data` is a passthrough of what the frame reported, so a settled payment carries its id, status and timestamp,
while a failure carries only the reason. `resourceId` is populated on every event except an approval's, where
the mount holds the client secret and never the id.

### Events with no named callback

These are emitted today and reach **`onEvent` only** — there is no `on…` option for them. If any matter to you,
handle them there:

`contract.expired` · `contract.revoked` · `contract.suspended` · `contract.ended` · `contract.invalid` ·
`contract.failed` · `approval.superseded` · `approval.cancelled`

```typescript theme={null}
onEvent: (e) => {
  if (e.type === "approval.superseded") replaceApprovalUi();
  if (e.type === "contract.revoked") stopChargingThisCustomer();
}
```

`type` is an open string, so `onEvent` is also where types added after your installed version arrive. Treat
`data` as additive — read the keys you know, ignore the rest.

On an approval surface `resourceId` is `null`: the mount holds the client secret, never the approval's id.

<Warning>
  These fire on a real status change recorded by Atoa, not on an optimistic UI guess — but they reach you
  through the customer's browser, which can close, lose signal, or never send them. **Confirm on your own
  backend before you fulfil an order**, with `payment.get` or `awaitSettled`. Webhooks don't fire for contract
  payments in this phase, so polling is the path.
</Warning>

### UI callbacks \[#ui-callbacks]

Separate from the events above, and these can fire many times. They describe the frame, never the money, so
never branch on them to decide an order is paid.

| Callback         | Fires when                                  | Argument                                                                                                                                                                        |
| ---------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `onReady`        | The frame connected and is ready to render. | —                                                                                                                                                                               |
| `onUiState`      | The frame's phase changed.                  | `'BOOT'` · `'PICKER'` · `'RESOLVING'` · `'HANDOFF'` · `'WAITING'` · `'SETTLING'` · `'CARD'` · `'APPROVAL'` · `'CONTRACT'` · `'PAID'` · `'FAILED'` · `'EXPIRED'` · `'CANCELLED'` |
| `onStatusChange` | The frame re-read the payment's status.     | `{ status: string, iso?: string }`                                                                                                                                              |
| `onHandoff`      | The customer is being sent to their bank.   | `{ href: string, bank: string }`                                                                                                                                                |
| `onExit`         | The customer left the surface.              | `{ reason: string }`                                                                                                                                                            |
| `onError`        | A request failed.                           | `ApiFailure` — see [Errors](#errors)                                                                                                                                            |

## Options and theming

`AtoaUI.mount(nextAction, options)` → `Handle`.

| Option             | Default            | Notes                                                                                                                                                                                                                                                                                      |
| ------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `container`        | — (required)       | An `HTMLElement` — not a selector. Set its width; the frame reports its own height and the loader applies it, clamped to 2000px. Throws synchronously if it isn't an element.                                                                                                              |
| `environment`      | — (required)       | `'sandbox'` or `'production'`. Anything else throws before an iframe is created. Must match the key that created the payment.                                                                                                                                                              |
| `paymentRequestId` | from the action    | Only needed if you have an id but no `nextAction`. There is no create verb here — mount shows a payment that already exists.                                                                                                                                                               |
| `orderId`          | —                  | De-duplication key. A second `mount` with the same `orderId` returns the **first handle** and renders nothing new, so a re-render or double-tap can't produce two surfaces.                                                                                                                |
| `theme`            | `{ mode: 'auto' }` | Bounded tokens, validated below.                                                                                                                                                                                                                                                           |
| `layout`           | `'inline'`         | `'inline'` or `'sheet'`.                                                                                                                                                                                                                                                                   |
| `device`           | UA detection       | `'desktop'` or `'mobile'`. Believed verbatim — you know your surface better than a UA string does. Drives the QR affordance: a phone is never offered a QR to scan with itself.                                                                                                            |
| `locale`           | frame default      | Locale hint, e.g. `'en-GB'`.                                                                                                                                                                                                                                                               |
| `gridColumns`      | `4`                | `3` or `4`. The bank grid's width.                                                                                                                                                                                                                                                         |
| `gridRows`         | `2`                | `1` or `2`. Tile count is `gridColumns × gridRows − 1`, the last cell being **View all** — so `gridRows: 1` gives a chat a single-row picker.                                                                                                                                              |
| `method`           | `'unknown'`        | `'bank'` · `'card'` · `'unknown'`. Which method you expect, so the loading skeleton paints the right shape. A hint only — a wrong one costs one repaint, never a wrong surface.                                                                                                            |
| `relay`            | `false`            | `true` when your page is **itself** inside an iframe, so applied heights re-emit to the page above. Resolves the parent origin from `ancestorOrigins`/referrer and stays off if neither is readable — it never posts to `*`. Pass `{ targetOrigin }` to state it explicitly.               |
| `handoff`          | `'auto'`           | `'auto'` renders the bank link inside the frame. `'native'` hands the URL out to `onHop` so your host opens it its own way.                                                                                                                                                                |
| `onHop`            | —                  | `(e: { href, bank?, reason: 'handoff' \| 'escalate' }) => boolean`. For hosts that open links through a platform API rather than an anchor. Return `true` to mean "handled"; anything else falls through to the in-frame anchor. Consulted for the bank hop only when `handoff: 'native'`. |

<Frame caption="One mount call, whatever container you give it.">
  <img className="block dark:hidden" src="https://mintcdn.com/atoaproddocs/i1RSfhZLVHfg2IA3/images/agent-pay/embed/hosts-light.svg?fit=max&auto=format&n=i1RSfhZLVHfg2IA3&q=85&s=636479a316865c073e23d144c25f2c30" alt="Four wireframe hosts: a chat thread, an in-app assistant panel, a mobile sheet, and a plain inline card, each with the checkout slot marked." width="1040" height="268" data-path="images/agent-pay/embed/hosts-light.svg" />

  <img className="hidden dark:block" src="https://mintcdn.com/atoaproddocs/i1RSfhZLVHfg2IA3/images/agent-pay/embed/hosts-dark.svg?fit=max&auto=format&n=i1RSfhZLVHfg2IA3&q=85&s=f4ac8fd95f1eef9d457bb790bcb38c94" alt="Four wireframe hosts: a chat thread, an in-app assistant panel, a mobile sheet, and a plain inline card, each with the checkout slot marked." width="1040" height="268" data-path="images/agent-pay/embed/hosts-dark.svg" />
</Frame>

### Theming

```typescript theme={null}
theme: { mode: "auto" }     // "auto" | "light" | "dark"
```

That is the whole theme surface. Anything unrecognised normalises to `"auto"`.

Colours, fonts, corner radius and density are deliberately not settable. A checkout a host can
restyle is a checkout a host can make look like something it is not, and the trust the frame carries
is what you are embedding. Brand treatment, when it ships, will be issued to your business from
Atoa's side — not accepted from the page.

<Warning>
  Don't modify the iframe's `allow` or `sandbox` attributes. Removing them breaks passkeys, one-time-code
  autofill, and the redirect to the customer's bank. The loader warns in the console if it detects either was
  altered.
</Warning>

### The handle

| Member             | Type                                                     | Notes                                                                                                                                                                                                                 |
| ------------------ | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `destroy()`        | `void`                                                   | Removes the iframe, leaves your container's styles as it found them, and releases the `orderId`. Idempotent — calling it twice does nothing.                                                                          |
| `refresh()`        | `void`                                                   | Ask the surface to re-check its money. Status watches are capped — the frame stops polling after a few minutes — so call this when your server learns something changed. Safe any time; a settled surface ignores it. |
| `getState()`       | `PhaseName`                                              | The current UI phase.                                                                                                                                                                                                 |
| `kind`             | `'payment'` · `'contract'` · `'approval'` · `'fallback'` | What was mounted. `'fallback'` means no frame exists — the action's text and link were rendered instead.                                                                                                              |
| `id`               | `string`                                                 | The subject: a `paymentRequestId`, `contractId`, or the approval's id.                                                                                                                                                |
| `paymentRequestId` | `string`                                                 | Set on payment surfaces, empty on contract and approval surfaces.                                                                                                                                                     |

## Errors

`mount()` throws straight away — before any iframe exists — if `container` isn't an `HTMLElement`, or if
`environment` isn't `'sandbox'` or `'production'`.

Everything after that reaches `onError` as an `ApiFailure`:

| Field         | Type                                                          | Notes                                   |
| ------------- | ------------------------------------------------------------- | --------------------------------------- |
| `kind`        | `'network'` · `'http'` · `'domain'` · `'abort'` · `'unknown'` | What class of failure.                  |
| `message`     | `string`                                                      | Human-readable.                         |
| `statusCode`  | `number?`                                                     | Present on `'http'`.                    |
| `name`        | `string?`                                                     | The server's error name, on `'domain'`. |
| `referenceId` | `string?`                                                     | Quote this to support.                  |

An `onError` doesn't end the surface — the frame may recover. Call `destroy()` yourself if you want to stop.

**If the container stays blank**, check the console. `AtoaUI.mount: the embed frame at <origin> never connected
within <n>ms` means the iframe loaded but never answered — usually a `frame-src` content-security policy on your
own page, or a network path that can't reach Atoa. If nothing is logged at all, `container` probably resolved to
`null`.

## Test in sandbox

Create the payment with a sandbox API key and pass `environment: 'sandbox'`. Choose **Atoa Test Bank** in the
checkout and pick the outcome you want. No money moves — see [Sandbox](/agent-pay/reference#sandbox).

To go live, switch to a production API key and pass `environment: 'production'`. Nothing else changes.

## Next

<CardGroup cols={2}>
  <Card title="Approvals SDK" icon="shield-check" href="/agent-pay/approvals">
    Show just the human sign-off step, without the whole checkout.
  </Card>

  <Card title="The nextAction contract" icon="diagram-project" href="/agent-pay/next-action">
    What `mount()` reads — for anyone rendering their own UI.
  </Card>
</CardGroup>
