# Bank Feed | Getting Started
Source: https://docs.paywithatoa.co.uk/accounts
Get started with Atoa Bank Feed: authorise a bank, then fetch account details, balances and transactions via Open Banking.
## Step 1: Request API Access
To get started, follow these steps:
1. **Contact Us**: Email [hello@paywithatoa.co.uk](mailto:hello@paywithatoa.co.uk) with your use case to request access.
2. **Receive Credentials**: Weβll provide you with both Production and Sandbox Secret Keys, along with your merchant ID.
3. **Authenticate Requests**: Include your API key in the header of all requests:
In the sandbox environment, we return static data. Make sure to switch to the
production environment before going live.
```
Authorization: Bearer sk_live_8aa94380efb94298aa4fe932ad78b22b
```
## Consent & Data Flow
### Step 2: Initiate the Consent Journey
Your app must initiate a consent request that allows users to connect their bank account(s). This will return a URL where users can authenticate securely via their bank.
**Endpoint**
```bash theme={null}
POST api.atoa.me/api/bank/auth/initiate
```
**Example Request**
```javascript theme={null}
{
"redirectUrl": "https://your-app.com",
"callbackParams": { //Optional: Pass any key-value pairs
"userId": "user_123",
}
}
```
**Example Response**
```javascript theme={null}
{
"url": "https://atoa.me/link-bank?accountAuthId=ed016c93-68c2-4394-8e9c-1e700f8a1b15&expiresAt=2025-04-16T17:53:48.174Z&env=PRODUCTION",
"accountAuthId": "ed016c93-68c2-4394-8e9c-1e700f8a1b15",
"expiresAt": "2025-04-16T17:53:48.174Z"
}
```
**Key Considerations**
* `accountAuthId` is essential for tracking user consent and is used in subsequent requests.
* `callbackParams` are passed transparently to your redirect URL along with additional provided parameters.
* `bankAccountIds` returned in the callback represent the unique identifiers for user bank accounts. These remain consistent throughout the user's lifecycle.
Once the user completes consent, they'll be redirected to your redirectUrl:
```
https://{{your-redirect-url}}?status=SUCCESS&accountAuthId=ed016c93-68c2-4394-8e9c-1e700f8a1b15&bankAccountIds=acc_001,acc_002&userId=abc_123
```
**Important Notes**
* Users may authorize multiple bank accounts; each bank account id will be appended in the callback as comma-separted value `bankAccountIds`.
* If the user unchecks the consent renewal checkbox, consent will not auto-renew after 90 days and must be reinitiated.
* Consent can also be manually revoked by the user via their banking app at any time.
## Step 3: Retrieve Transactions
Use the **bankAccountId** from the callback to fetch transaction data.
**Endpoint**
```bash theme={null}
GET api.atoa.me/api/bank/accounts/{accountId}/transactions?from=2024-01-01&before=2024-03-01
```
**Example Response**
```javascript theme={null}
{
"data": [
{
"id": "txn_001",
"date": "2024-01-14T11:45:00Z",
"type": "DEBIT",
"amount": 89.99,
"currency": "GBP",
"description": "Amazon Marketplace",
"reference": "INV-4321-A",
"transactionCategory": "E-commerce",
"status": "BOOKED",
"balance": {"amount": 2.37, "currency": "GBP"} //Optional
}
],
"meta": { "count": 1 },
"links": {
"prev": "https://api.atoa.me/api/bank/accounts/{accountId}/transactions?cursor=aaa",
"self": "https://api.atoa.me/api/bank/accounts/{accountId}/transactions?cursor=bbb",
"next": "https://api.atoa.me/api/bank/accounts/{accountId}/transactions?cursor=ccc"
}
}
```
Results are cursor-paginated β follow `links.next` in the response to fetch subsequent pages. See [Cursor-based pagination](/api-reference/account-initiation-services/fetch-account-transactions#cursor-based-pagination) on the Transaction API reference for the full contract, field descriptions, and worked examples.
## Step 4: Retrieve Bank Account Metadata
Use this API to fetch account-level metadata such as account number and sort code for the authorized account.
**Endpoint**
```bash theme={null}
POST api.atoa.me/api/bank/accounts/{accountId}
```
**Example Response**
```javascript theme={null}
{
"bankName": "HSBC UK",
"accountNumber": "12345678",
"sortCode": "112233"
}
```
**Why This Matters**
* Useful for matching and linking bank accounts to internal user profiles.
* Ensures clarity when a user has multiple accounts with similar names.
* Always validate this information securely before displaying it to users.
Refer to the [Get Bank account](/api-reference/account-initiation-services/fetch-account-details) API to know more about the response data and error handling.
## Step 5: Revoke Consent
If you wish to terminate access programmatically, you can revoke access the following way:
**Revoke a Specific Account**
```bash theme={null}
POST api.atoa.me/api/bank/auth/revoke?accountId=0011223344
```
Deletes consent for the specific bank account from Atoa's records. If this is the last bank account associated with that consent, it will also revoke consent at the user's bank.
**Revoke All Accounts**
```bash theme={null}
POST api.atoa.me/api/bank/auth/revoke
```
Use this to fully revoke the consent. This deletes all bank accounts consent from the user's bank and from Atoa.
## Consent Lifecycle & Expiry
* All consents are valid for 90 days.
* If auto-renew is enabled, we will attempt background renewal before expiry.
* Once expired, any call to transactions or account endpoints will return: 404 Consent not found
Below is a screenshot showing the bank selection screen and the auto-renewal checkbox for reference.
π§ **Recommendation**: Prompt users to re-authenticate as consent expiration nears (e.g., 7 days before expiry).
# Bank Feed | Introduction
Source: https://docs.paywithatoa.co.uk/accounts-introduction
Introduction to Atoa Bank Feed: securely retrieve customer bank account details, balances and transactions via Open Banking.
**Bank Feed** is a standalone product that gives your platform secure, consented access to **bank account** dataβincluding balances, transactions, and account metadata. It is designed purely for **data access**, not money movement.
## Bank Feed capabilities
* **Reconciliation**: Match incoming payments against customer bank statements
* **Cash Flow Insights**: Analyze inflows, outflows, and spending patterns
* **Payer/Payee Matching**: Extract and compare names, references, and notes to accurately identify transaction parties
* **Account Matching**: Fetch sort code and account number to link user accounts
* **ERP & Dealer Systems**: Sync verified bank data with your internal tools
## How It Works
Users connect their bank account through a **secure consent flow**. Once authorized, you can fetch their transaction history and account details using Atoa's API. Consent is valid for 90 days and can be renewed or revoked at any time.
# AI Agents & Tools with Atoa Agent Pay
Source: https://docs.paywithatoa.co.uk/agent-pay/ai-agents
Turn the Atoa Agent Pay SDK into a tool surface any model can use. Wire up the tools, run the loop and keep every capability enforced server-side.
`createAgentPayTools(atoa)` returns tool definitions you can hand to any model, plus a `call()` dispatcher that
runs whatever the model picks.
The tools can only do what your API key permits. Spending limits live on the contract and are enforced by Atoa's
servers, so the model can't exceed them.
## Wire-up
```python Python theme={null}
from atoa_agent_pay import init, create_agent_pay_tools
atoa = init(environment="sandbox") # reads ATOA_API_KEY
atoa.agent.register(name="Checkout copilot")
tools = create_agent_pay_tools(atoa)
tools.specs # tool definitions β hand these to your model
```
```typescript TypeScript theme={null}
import { createAgentPayClient, createAgentPayTools } from "@atoapayments/agent-pay";
const atoa = createAgentPayClient({ environment: "sandbox" }); // reads ATOA_API_KEY
await atoa.agent.register({ name: "Checkout copilot" });
const tools = createAgentPayTools(atoa);
tools.tools; // tool definitions β hand these to your model
```
The definitions carry the usage guidance a model needs β required fields, defaults, when to ask the user β so you
don't write tool prompts yourself.
## The loop
The loop works with any model provider: hand the model the definitions, run what it calls, feed the result back.
```python Python theme={null}
# 1 β give the model tools.specs as its available tools
# 2 β when it returns a tool call, execute it:
out = tools.call(tool_call.name, tool_call.arguments)
# out: { "result", "text", "isError" } β text is model-ready; errors come back in-band, not raised
# 3 β return out["text"] to the model and continue until it's done
```
```typescript TypeScript theme={null}
// 1 β give the model tools.tools as its available tools
// 2 β when it returns a tool call, execute it:
const out = await tools.call(toolCall.name, toolCall.arguments);
// out: { result, text, isError } β text is model-ready; errors come back in-band, not thrown
// 3 β return out.text to the model and continue until it's done
```
## The tools
**23 tools.** Each maps to the SDK method of the same name β parameters and behaviour are identical, so the
[reference](/agent-pay/reference#methods) covers both.
### Setup & sandbox
| Tool | Purpose |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `check_availability` | Health probe β never errors. |
| `get_agent` | Which persona this session operates as β call it **before** `register_agent`; if it reports `registered: true`, reuse that persona instead of registering again. |
| `register_agent` | Register the agent (idempotent on the same key). Only call it when `get_agent` reports no agent yet. |
| `get_sandbox_test_accounts` | The sandbox SEND accounts and the outcome each forces. |
### Contracts
| Tool | Purpose |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `create_contract` | Create a `SEND` or `COLLECT` contract β returns the `authorizationUrl` a human approves. COLLECT can bundle a [first charge](/agent-pay/collect#charge-on-approval) into that approval (`initialChargeAmount` + `initialChargeOrderId`). |
| `get_contract` | Read a contract, including live `usage` headroom. |
| `list_contracts` | Filter by `type`, `status`, or customer. |
| `update_contract` | Change `limits` β returns a new `authorizationUrl` to re-approve. |
| `revoke_contract` | Revoke (terminal). |
### Payments
| Tool | Purpose |
| ----------------- | -------------------------------------------------------------------- |
| `collect_payment` | Money in β a pay-link, or an off-session charge with a `contractId`. |
| `send_payment` | Money out under an `ACTIVE` SEND contract. |
| `get_payment` | Read one payment. |
| `list_payments` | Filter by `type`, `status`, contract, or customer. |
| `cancel_payment` | Cancel an unpaid collect. |
### Refunds
| Tool | Purpose |
| ---------------- | ---------------------------------------------- |
| `refund_payment` | Refund a `COMPLETED` collect, full or partial. |
| `list_refunds` | All refunds on a payment. |
| `cancel_refund` | Cancel a pending refund. |
### Customers & stores
| Tool | Purpose |
| --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `create_customer` Β· `get_customer` Β· `list_customers` Β· `update_customer` Β· `delete_customer` | Manage customers (page `list_customers` to find one by name/email). |
| `list_stores` | Discover your business locations. Tool-driven payments always use the primary store. |
## Approvals: the human stays in charge
There is no approval tool. When `send_payment` or an off-session `collect_payment` returns a `nextAction`, show
`nextAction.approvalUrl` to the right person and wait for their decision β the business owner for a payout, the
customer for a charge. The model can't approve its own payment, and the one-time code or passkey is only ever
entered on Atoa's page.
Every payment an agent makes is a normal `Payment` β `list_payments` and `get_payment` give it (and you) the full
audit trail.
## No AI required
The tools are a thin layer over the SDK. Without a model in the loop, call the same operations directly β
`atoa.payment.collect(...)`, `atoa.payment.send(...)` β for identical behaviour.
## Prefer MCP?
The hosted [Atoa MCP server](/mcp-server) exposes Atoa's tools to Claude, Cursor, VS Code, and any other
MCP-compatible client β no SDK integration needed.
## Rendering results
Tool results that need a human β a pay-link, a contract authorisation, an approval β carry a
[`nextAction`](/agent-pay/next-action). Pass that action to `AtoaUI.mount()` in a web UI with
[embedded checkout](/agent-pay/embed), or post `nextAction.fallback.text` in a text channel.
# Agent Pay Approvals SDK for Human Sign-Off
Source: https://docs.paywithatoa.co.uk/agent-pay/approvals
Embed Atoa's hosted approval page in your own web UI so a human approves gated agent payouts and charges with a one-time code or a passkey.
Every gated payment β a [payout](/agent-pay/send#sca-on-a-payout) or an
[off-session charge](/agent-pay/collect#sca-on-a-charge) β needs a human to approve it before money moves. The approval
happens on **Atoa's hosted page**, where the approver enters a one-time code or uses a passkey. Your app never sees the
credential.
`@atoapayments/agentic-payment-approvals-js` embeds that hosted page **inside a container you provide** and resolves to the
decision, so the approver stays in your own web UI instead of following a link away. It's the browser-side partner of the
`nextAction` you get back from `payment.send` / `payment.collect` β an alternative to sending the raw `approvalUrl`.
Browser/TypeScript only, zero dependencies. If you're on the Python SDK β or have no web layer at all β deliver the
`approvalUrl` from `nextAction` instead (see [SCA on a payout](/agent-pay/send#sca-on-a-payout)); this SDK is just a
nicer surface for the same approval, not a separate one.
## Install
```bash theme={null}
npm i @atoapayments/agentic-payment-approvals-js
```
## Use
When a gated call returns a `nextAction`, pass its `clientSecret` to `confirmApproval` along with a container you
render. You get back an **`ApprovalHandle`**: `await handle.result` for the decision, `handle.on(β¦)` to follow the
lifecycle, and `handle.destroy()` to close it.
```typescript theme={null}
import { confirmApproval } from "@atoapayments/agentic-payment-approvals-js";
const result = await atoa.payment.send({
contractId,
payments: [{ /* β¦ */ }],
});
if (result.nextAction) {
const approval = confirmApproval({
container: "#approval", // a selector or HTMLElement you render + size
clientSecret: result.nextAction.clientSecret,
colorScheme: "light", // "light" (default) | "dark"
onEvent: (e) => console.log(e.type), // opened, loaded, approved, declined, β¦
});
const { status } = await approval.result; // APPROVED | DECLINED | EXPIRED | SUPERSEDED | CANCELLED
// From YOUR close affordance (backdrop click, sheet dismiss, route change):
approval.destroy(); // cancels if still pending (β CANCELLED); no-op after a decision
}
```
## Options
`confirmApproval(options)` β `ApprovalHandle`.
| Option | Default | Notes |
| -------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `container` | β (required) | A CSS selector or an `HTMLElement`. You render and size it; the iframe fills it. Throws synchronously if it doesn't resolve to an element. |
| `clientSecret` | β (required) | From `nextAction`. Environment is read from its prefix (`ap_live_β¦` / `ap_test_β¦`) β no publishable key. A malformed secret throws before any iframe is created. |
| `colorScheme` | `light` | `"light"` or `"dark"`. Anything else (incl. `undefined`) normalizes to `light`. Independent of `theme`. |
| `theme` | β | Bounded branding tokens β hex/length-validated and **contrast-clamped**. See [Theming](#theming). Amounts, warnings, and the decline button are never restyled. |
| `onEvent` | β | Fires for every [lifecycle event](#events) (same stream as `on(β¦)`). |
| `onResult` | β | Terminal-only convenience β called **once** with the final `{ status }`. |
### Theming
`theme` accepts a small, bounded set of branding tokens applied as CSS custom properties β **not** freeform CSS. Each
token is hex/length-validated; malformed tokens are dropped. The two critical contrast pairs (button-text vs primary,
text vs background) are contrast-clamped to \~4.5:1 β a failing pair falls back to the accessible default, so you can
never render an invisible decline button or white-on-white amount.
```typescript theme={null}
theme: {
primaryColor: "#E42646",
buttonTextColor: "#FFFFFF",
textColor: "#111111",
headingColor: "#111111",
backgroundColor: "#FFFFFF",
borderRadius: "12px",
fontFamily: "system-ui, sans-serif", // system font stacks only β no remote fonts
}
```
The amount, warnings, masked-contact copy, and layout are never customisable.
## Events
Every event flows through both `onEvent` and every `on(β¦)` listener as the same `{ type, β¦payload }` object β filter on
`e.type`. Payloads are **enumerated/masked only**: never an OTP, a full contact, or an account number. A listener that
throws is swallowed and never breaks the stream.
| `type` | Emitted when | Payload | Terminal? |
| ------------ | ------------------------------------------------------------------------------------------ | ----------------------------------------------------- | ---------------------- |
| `opened` | Synchronously, the instant the handle is created. SDK-emitted. | β | no |
| `loaded` | The page loaded and resolved which CTA it will show. | `method?: "PASSKEY" \| "SETUP_OFFERED" \| "OTP_ONLY"` | no |
| `approved` | The human approved. | `decidedBy?: string` | **yes** β `APPROVED` |
| `declined` | The human declined. | `decidedBy?: string` | **yes** β `DECLINED` |
| `expired` | The approval window lapsed before a decision. | β | **yes** β `EXPIRED` |
| `superseded` | A newer approval for the same action replaced this one. | β | **yes** β `SUPERSEDED` |
| `error` | A recoverable page error (bad secret state, network, β¦). | `reason: string` | no |
| `closed` | You called `destroy()` (or it auto-cancelled). Precedes a `CANCELLED` result. SDK-emitted. | β | no |
`opened` and `closed` are emitted by the SDK; everything else originates on the Atoa page. `error` does **not** settle
`result` β the page may recover β so it's up to you to `destroy()` if you want to give up.
## Result & terminal statuses
`result` (and `onResult`) resolve **exactly once**, with `{ status }`:
| Status | Source |
| ------------ | ----------------------------------------------------------------------- |
| `APPROVED` | `approved` event |
| `DECLINED` | `declined` event |
| `EXPIRED` | `expired` event |
| `SUPERSEDED` | `superseded` event |
| `CANCELLED` | You called `destroy()` before any of the above (never a page decision). |
A declined or expired approval also comes back on the `Payment` itself β `failureReason: APPROVAL_DECLINED` /
`APPROVAL_EXPIRED` β so a purely server-side integration behaves identically. Use `onEvent` / `on(β¦)` for everything
between `opened` and the terminal status.
## When to call `destroy()`
Call `destroy()` when the approver closes your UI **without deciding** β they tap the backdrop, swipe the sheet away,
or your component unmounts. It removes the iframe and resolves `result` as `CANCELLED`.
After a real decision you don't need it: the SDK cleans up on its own, and calling `destroy()` then does nothing.
# Collect β money in
Source: https://docs.paywithatoa.co.uk/agent-pay/collect
Collect money with Agent Pay: take a customer-present pay-link, or charge off-session under a COLLECT contract the customer approved once.
Two ways in. The customer is present and pays a link, or they're away and you charge under a **COLLECT contract**
they approved once. You never pass card or method details in either β Atoa charges the method the customer saved.
## Pay-link β customer present
```python Python theme={null}
req = atoa.payment.collect(
amount={"amount": 45.00}, # pounds, not pence (45.00 = Β£45); GBP default
order_id="order-1099", # your own reference
)
print(req.payment_url) # hand over the link
print(req.qr_code_url) # or the QR
settled = atoa.payment.await_settled(req.payment_request_id)
print(settled.status) # COMPLETED
```
```typescript TypeScript theme={null}
const req = await atoa.payment.collect({
amount: { amount: 45.00 }, // pounds, not pence (45.00 = Β£45); GBP default
orderId: "order-1099", // your own reference
});
console.log(req.paymentUrl); // hand over the link
console.log(req.qrCodeUrl); // or the QR
const settled = await atoa.payment.awaitSettled(req.paymentRequestId);
console.log(settled.status); // COMPLETED
```
* `expiresIn` (milliseconds) shortens the link's window; an unpaid link reads back `EXPIRED`.
* Cancel an unpaid link with `payment.cancel(paymentRequestId)` β `CANCELLED`, reason `MERCHANT_CANCELLED`.
**Sandbox:** open the link, choose the **Atoa Test Bank**, and pick the outcome β `COMPLETED`, `FAILED`, or
`PENDING`. Cancelling on the bank page gives `CUSTOMER_CANCELLED`.
## Off-session β under a COLLECT contract \[#off-session]
The customer approves the contract (and links a payment method) once at its `authorizationUrl`. After that you
charge under the contract while they're away β pass the `contractId`, never a payment method.
```python Python theme={null}
customer = atoa.customer.create(
full_name="Ada Lovelace",
email="ada@example.com", # email or phone required
)
contract = atoa.contract.create(
type="COLLECT",
name="Monthly membership",
atoa_customer_id=customer.id,
limits={
"max_per_payment": 50.00,
"period_limits": [{"amount": 100.00, "period": "MONTH"}],
"valid_to": "2026-12-31T23:59:59Z",
},
)
print(contract.authorization_url) # customer links a method + approves the caps here
atoa.contract.await_active(contract.contract_id) # β ACTIVE
charge = atoa.payment.collect(
contract_id=contract.contract_id,
atoa_customer_id=customer.id,
amount={"amount": 20.00},
order_id="membership-jan",
)
# charge.next_action β the customer approves before it settles (next section)
```
```typescript TypeScript theme={null}
const customer = await atoa.customer.create({
fullName: "Ada Lovelace",
email: "ada@example.com", // email or phone required
});
const contract = await atoa.contract.create({
type: "COLLECT",
name: "Monthly membership",
atoaCustomerId: customer.id,
limits: {
maxPerPayment: 50.00,
periodLimits: [{ amount: 100.00, period: "MONTH" }],
validTo: "2026-12-31T23:59:59Z",
},
});
console.log(contract.authorizationUrl); // customer links a method + approves the caps here
await atoa.contract.awaitActive(contract.contractId); // β ACTIVE
const charge = await atoa.payment.collect({
contractId: contract.contractId,
atoaCustomerId: customer.id,
amount: { amount: 20.00 },
orderId: "membership-jan",
});
// charge.nextAction β the customer approves before it settles (next section)
```
Atoa enforces the `limits` on every charge. Check what's left in the window before charging, rather than finding
out from a failed charge:
```python Python theme={null}
usage = atoa.contract.get(contract.contract_id).usage or []
month = next((u for u in usage if u.period == "MONTH"), None)
print(month.remaining) # what is left this window; over-cap charges fail LIMIT_EXCEEDED
```
```typescript TypeScript theme={null}
const { usage } = await atoa.contract.get(contract.contractId);
const month = usage?.find((u) => u.period === "MONTH");
console.log(month?.remaining); // what is left this window; over-cap charges fail LIMIT_EXCEEDED
```
## Charge on approval \[#charge-on-approval]
A COLLECT contract can bundle a **first charge into the customer's single approval** β one page both activates the
contract *and* takes that payment (a joining fee, a deposit, the first bill). Pass `initialCharge` to
`contract.create`; don't call `payment.collect` separately for charge #1.
```python Python theme={null}
contract = atoa.contract.create(
type="COLLECT",
name="Gym membership",
atoa_customer_id=customer.id,
limits={
"max_per_payment": 50.00,
"period_limits": [{"amount": 50.00, "period": "MONTH"}],
"valid_to": "2027-01-31T23:59:59Z",
},
initial_charge={
"amount": 25.00, # β₯ Β£1 and β€ max_per_payment
"order_id": "joining-1001", # your reference (β€ 50 chars)
"notes": "Joining fee", # optional (β€ 30 chars)
},
)
# One approval at contract.authorization_url activates the contract AND takes the charge.
atoa.contract.await_active(contract.contract_id)
first = atoa.contract.get(contract.contract_id).initial_charge
print(first.status) # AWAITING_APPROVAL β PENDING β COMPLETED | FAILED
```
```typescript TypeScript theme={null}
const contract = await atoa.contract.create({
type: "COLLECT",
name: "Gym membership",
atoaCustomerId: customer.id,
limits: {
maxPerPayment: 50.00,
periodLimits: [{ amount: 50.00, period: "MONTH" }],
validTo: "2027-01-31T23:59:59Z",
},
initialCharge: {
amount: 25.00, // β₯ Β£1 and β€ maxPerPayment
orderId: "joining-1001", // your reference (β€ 50 chars)
notes: "Joining fee", // optional (β€ 30 chars)
},
});
// One approval at contract.authorizationUrl activates the contract AND takes the charge.
await atoa.contract.awaitActive(contract.contractId);
const { initialCharge } = await atoa.contract.get(contract.contractId);
console.log(initialCharge?.status); // AWAITING_APPROVAL β PENDING β COMPLETED | FAILED
```
Read the outcome from the contract's `initialCharge`.
`initialCharge.paymentRequestId` only exists once the customer approves. After that, poll `payment.get` with it;
before it, match on your own `orderId`. A status of `INITIATION_FAILED` means the charge never started β the
contract still becomes `ACTIVE`.
Only take the first charge with `payment.collect` when the contract was created **without** an `initialCharge`.
Doing both charges the customer twice.
Pass `storeId` to attribute the charge to one of your stores. That store's name and logo appear on the
customer's contract page.
## SCA on a charge
An off-session charge doesn't settle until the customer approves it.
`payment.collect` returns a `nextAction` (`next_action` in Python) holding an `approvalUrl`. Send the customer
there. They approve with a one-time code or a passkey on a page hosted by Atoa. Your app never sees the
credential.
```python Python theme={null}
share_with_customer(charge.next_action.approval_url) # your own delivery β email, push, β¦
decision = atoa.payment.await_decision(charge.next_action.approval_id)
print(decision.status) # APPROVED / DECLINED / EXPIRED
settled = atoa.payment.await_settled(charge.payment_request_id)
print(settled.status) # COMPLETED
```
```typescript TypeScript theme={null}
shareWithCustomer(charge.nextAction.approvalUrl); // your own delivery β email, push, β¦
const decision = await atoa.payment.awaitDecision(charge.nextAction.approvalId);
console.log(decision.status); // APPROVED / DECLINED / EXPIRED
const settled = await atoa.payment.awaitSettled(charge.paymentRequestId);
console.log(settled.status); // COMPLETED
```
To keep the customer on your own site instead of sending a URL, pass `nextAction.clientSecret` to
`confirmApproval` from the [Approvals SDK](/agent-pay/approvals).
A declined charge comes back as `FAILED` with `failureReason: APPROVAL_DECLINED`. One nobody approved in time
comes back as `APPROVAL_EXPIRED`. Neither throws β read them off the `Payment`. In sandbox, open the
`approvalUrl` yourself and pick the decision you want to test.
## Refunds
Refund a `COMPLETED` collect, in full or in part. A refund moves `INITIATED β COMPLETED` (or `FAILED`).
```python Python theme={null}
refund = atoa.payment.refund(
req.payment_request_id,
amount={"amount": 10.00}, # omit for a full refund
reason="Damaged item",
)
print(refund.status, refund.refund_amount)
atoa.payment.list_refunds(req.payment_request_id) # all refunds on this payment
```
```typescript TypeScript theme={null}
const refund = await atoa.payment.refund(req.paymentRequestId, {
amount: { amount: 10.00 }, // omit for a full refund
reason: "Damaged item",
});
console.log(refund.status, refund.refundAmount.amount);
await atoa.payment.listRefunds(req.paymentRequestId); // all refunds on this payment
```
A pending refund can be cancelled with `cancelRefund(refundId)`. **Sandbox:** set `reason` to `"FAILURE TEST"` to
force a `FAILED` refund and exercise that branch.
# Embedded Checkout for Agent Pay
Source: https://docs.paywithatoa.co.uk/agent-pay/embed
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.
## Install
```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}
```
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`:
```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
});
```
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.
Approval secrets travel in the URL **fragment**. Don't copy them into query strings, logs, or your own code.
## Mount the checkout
Fetch it and mount it. Set your container's width β the iframe sets its own height.
```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}
```
```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(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 ;
}
```
```vue Vue theme={null}
```
```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: ``,
})
export class AtoaCheckoutComponent implements AfterViewInit, OnDestroy {
@Input({ required: true }) nextAction!: unknown;
@Input() orderId?: string;
@ViewChild("host") host!: ElementRef;
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();
}
}
```
`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.
| `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.
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.
### 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'`. |
### 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.
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.
### 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 never connected
within 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
Show just the human sign-off step, without the whole checkout.
What `mount()` reads β for anyone rendering their own UI.
# The nextAction Contract
Source: https://docs.paywithatoa.co.uk/agent-pay/next-action
The object on every Agent Pay result that tells you what has to happen next, and who has to do it.
**You don't need this page to ship.** [Embedded checkout](/agent-pay/embed) reads `nextAction` for you, and so
does the [Approvals SDK](/agent-pay/approvals). Read on if you're rendering payment UI yourself, relaying actions
through a channel Atoa doesn't know about, or want to see what the embed switches on.
Every Agent Pay result carries one `nextAction`: the single thing that must happen next. It holds the ids,
amounts and links a renderer needs, so you never pick fields out of a raw response. With no UI, poll
`nextAction.resolve` until the status is terminal. In a chat, post `nextAction.fallback.text`.
## The shape
| Field | Type | Meaning |
| -------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `v` | `1` | Envelope version. |
| `type` | string | The action kind β the [catalogue](#the-catalogue) below. New types can appear: switch on it and fall back on anything unknown. |
| `state` | string? | Where the flow is β the underlying payment/contract status when there is one. |
| `for` | `'customer' \| 'owner' \| 'none'` | **Who must act.** `'none'` tells an agent NOT to prompt a human. |
| `expiresAt` | ISO datetime? | When the action lapses. |
| `mustEscalate` | boolean | Reserved. Always `false` today β no action currently needs a top-level window. |
| `data` | object | Everything a surface needs, by type β **additive-only**: ignore fields you don't know. |
| `fallback` | `{ text, url }` | **Present on every action.** One sentence and a link β the plain-text path when you render nothing. Never carries a secret. |
| `resolve` | `{ poll, id, until }`? | How to learn it finished: poll `'payment' \| 'contract' \| 'approval'` with `id` until a status in `until`. |
Read approval fields from `data`: `data.approvalId`, `data.approvalUrl`, `data.clientSecret`. The same three are
also mirrored at the top level of `nextAction` for backwards compatibility.
## The catalogue \[#the-catalogue]
Four types. That's the whole set an Agent Pay call can return today.
| `type` | Who acts | What it asks for |
| -------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PAY` | customer | Pay an open pay-link β `data.paymentRequestId`, `amount`, `hostedUrl`. |
| `APPROVAL` | customer or owner | The SCA [approval gate](/agent-pay/reference#approval-gate) (SCA: the approver's one-time code or passkey check) β `data.approvalId`, `approvalUrl`, `clientSecret`. |
| `AUTHORIZE_CONTRACT` | customer or owner | Authorise a contract β `data.kind` says which page: `'collect'` (customer links a method + approves) or `'cvrp'` (a commercial Variable Recurring Payment β the account holder consents at their bank). |
| `NONE` | none | Terminal. State the outcome, ask for nothing. |
Choosing a card, entering card details, verifying a contact, enrolling a passkey β those are **steps inside** a
`PAY`, `APPROVAL` or `AUTHORIZE_CONTRACT` flow, handled on Atoa's pages. They never arrive as a `nextAction` you
have to render.
## Handling a type you don't know
**When you get a `type` you don't recognise, render `fallback.text` and `fallback.url`.**
`type` is a plain string, not an enum, so Atoa can add action types without an SDK release. Every action type has
a usable `fallback`. The embed does this for you.
`fallback.text` never contains a `clientSecret`, so it's safe to post to a chat or an email.
## Amounts are pre-formatted
`data.amount` is `{ value, currency, display }` β `value` is a decimal **string**, exactly as sent on the wire;
`display` is pre-formatted (`"Β£45.00"`). Print `display`; never format money yourself.
```json theme={null}
{
"v": 1,
"type": "PAY",
"state": "PENDING",
"for": "customer",
"mustEscalate": false,
"data": {
"paymentRequestId": "prq_a81fβ¦",
"amount": { "value": "45.00", "currency": "GBP", "display": "Β£45.00" },
"orderId": "booking-8812",
"hostedUrl": "https://pay.atoa.me/prq_a81fβ¦"
},
"fallback": {
"text": "Pay Β£45.00 securely from your bank: https://pay.atoa.me/prq_a81fβ¦",
"url": "https://pay.atoa.me/prq_a81fβ¦"
},
"resolve": { "poll": "payment", "id": "prq_a81fβ¦", "until": ["COMPLETED", "FAILED", "CANCELLED", "EXPIRED"] },
"source": "sdk"
}
```
# Agent Pay: Payments for AI Agents
Source: https://docs.paywithatoa.co.uk/agent-pay/overview
Let an AI agent take payments and make payouts within spending limits a human approved. Atoa enforces the limits server-side.
**Agent Pay** lets your AI agent take payments and make payouts, inside limits a human approved once. Atoa
enforces those limits on its own servers, so nothing the model does can go past them.
## How it works
Two calls. Your server creates a payment and sends its `nextAction` to the browser; your browser shows it.
```python Server Β· Python theme={null}
req = atoa.payment.collect(
amount={"amount": 45.00},
order_id="booking-8812",
)
# send req.next_action to your front end
```
```typescript Server Β· TypeScript theme={null}
const result = await atoa.payment.collect({
amount: { amount: 45.00 },
orderId: "booking-8812",
});
// send result.nextAction to your front end
```
```javascript Browser theme={null}
AtoaUI.mount(nextAction, { // fetched from your own endpoint
container: document.querySelector("#checkout"),
environment: "sandbox",
onPaymentCompleted: (e) => showReceipt(e),
});
```
The same result works without a browser: it carries a pay-link you can send, or a
[`nextAction`](/agent-pay/next-action) your agent can read.
## Contracts and limits
A **contract** is a human's standing approval. They set it up once β a cap per payment, a cap per period, an
expiry date, and a direction β and after that your agent can move money inside it without asking again.
Atoa checks every charge against the contract on its own servers. A payment over the cap is refused there, not in
your code. Two things still need a person: setting the contract up, and Strong Customer Authentication (SCA) on
payouts and off-session charges, where the approver enters a one-time code or uses a passkey on Atoa's page.
## Collect or send
* **Customer present** β `payment.collect` returns a pay-link/QR. No contract. β [Collect](/agent-pay/collect)
* **Customer not present** β `payment.collect` with a `contractId`, under a **COLLECT contract** the customer approved once. β [Collect](/agent-pay/collect#off-session)
* **Paying out** β `payment.send` under a **SEND contract** the account owner approved once. β [Send](/agent-pay/send)
## Get started
Get a **sandbox** API key from the [Atoa dashboard](https://dashboard.paywithatoa.co.uk/) and set it in your
environment. Sandbox and production keys are separate.
```bash npm theme={null}
npm install @atoapayments/agent-pay # Node 22+
```
```bash pip theme={null}
pip install atoa-agent-pay # Python 3.10+
```
```bash theme={null}
export ATOA_API_KEY="your_sandbox_api_key"
```
Create a client and register your agent. Registering twice with the same key returns the same agent, so it's safe
to run on every start. The example generates a signing key in memory β in production, load it from your secrets
manager or keep it out of your process entirely with a [KMS signer](/agent-pay/reference#kms-custom-signer). See
[Authentication](/agent-pay/reference#authentication) for the other ways to pass credentials.
```python Python theme={null}
import atoa_agent_pay
private_key_pem, _ = atoa_agent_pay.generate_es256_keypair()
# api_key is read from ATOA_API_KEY; the public key is derived from private_key_pem.
atoa = atoa_agent_pay.init(environment="sandbox", private_key_pem=private_key_pem)
atoa.agent.register(name="Bookings assistant") # registers the derived public key
```
```typescript TypeScript theme={null}
import { createAgentPayClient, generateEs256KeyPair } from "@atoapayments/agent-pay";
const { privateKeyPem } = generateEs256KeyPair();
// apiKey is read from ATOA_API_KEY; the public key is derived from privateKeyPem.
const atoa = createAgentPayClient({ environment: "sandbox", privateKeyPem });
await atoa.agent.register({ name: "Bookings assistant" }); // registers the derived public key
```
## Collect your first payment
Create a payment on your server, then show it. In sandbox, choose the **Atoa Test Bank** in the checkout and pick
the outcome yourself.
```python Python theme={null}
req = atoa.payment.collect(
amount={"amount": 45.00}, # pounds, not pence (45.00 = Β£45); GBP default
order_id="booking-8812", # your own reference
)
print(req.payment_url) # open this and choose an outcome
settled = atoa.payment.await_settled(req.payment_request_id)
print(settled.status) # COMPLETED
```
```typescript TypeScript theme={null}
const req = await atoa.payment.collect({
amount: { amount: 45.00 }, // pounds, not pence (45.00 = Β£45); GBP default
orderId: "booking-8812", // your own reference
});
console.log(req.paymentUrl); // open this and choose an outcome
const settled = await atoa.payment.awaitSettled(req.paymentRequestId);
console.log(settled.status); // COMPLETED
```
To show the checkout inside your own product instead of sending a link, send `req.next_action` from your
server to the browser and mount it:
```javascript Browser theme={null}
import { AtoaUI } from "@atoapayments/pay-embed";
AtoaUI.mount(nextAction, { // fetched from your own endpoint
container: document.querySelector("#checkout"),
environment: "sandbox",
onPaymentCompleted: (e) => showReceipt(e),
});
```
Full walkthrough: [Embedded checkout](/agent-pay/embed).
## Send your first payout
Create a SEND contract, approve it once at the `authorizationUrl`, then send. Every payout pauses for the
business owner's approval before money moves. In sandbox the recipient account decides the outcome β list the
accounts with `atoa.sandboxTestAccounts()`.
```python Python theme={null}
contract = atoa.contract.create(
type="SEND",
name="Supplier payouts",
limits={
"max_per_payment": 500.00,
"period_limits": [{"amount": 2000.00, "period": "MONTH"}],
"valid_to": "2026-12-31T23:59:59Z",
},
)
print(contract.authorization_url) # the account owner approves here
atoa.contract.await_active(contract.contract_id)
result = atoa.payment.send(
contract_id=contract.contract_id,
payments=[{
"amount": {"amount": 250.00},
"beneficiary": {"name": "ACME LTD", "sortCode": "040004", "accountNumber": "12345678"}, # sandbox: settles COMPLETED
"orderId": "payout-3001",
}],
)
print(result.next_action.approval_url) # the owner approves the payout here β in sandbox, open it yourself
atoa.payment.await_decision(result.next_action.approval_id) # APPROVED β the payment executes
print(result[0].status) # COMPLETED
```
```typescript TypeScript theme={null}
const contract = await atoa.contract.create({
type: "SEND",
name: "Supplier payouts",
limits: {
maxPerPayment: 500.00,
periodLimits: [{ amount: 2000.00, period: "MONTH" }],
validTo: "2026-12-31T23:59:59Z",
},
});
console.log(contract.authorizationUrl); // the account owner approves here
await atoa.contract.awaitActive(contract.contractId);
const result = await atoa.payment.send({
contractId: contract.contractId,
payments: [{
amount: { amount: 250.00 },
beneficiary: { name: "ACME LTD", sortCode: "040004", accountNumber: "12345678" }, // sandbox: settles COMPLETED
orderId: "payout-3001",
}],
});
console.log(result.nextAction.approvalUrl); // the owner approves the payout here β in sandbox, open it yourself
await atoa.payment.awaitDecision(result.nextAction.approvalId); // APPROVED β the payment executes
console.log(result[0].status); // COMPLETED
```
That's a payout in sandbox. Every payout and every off-session charge returns an approval step like this one β
see [Collect](/agent-pay/collect#sca-on-a-charge) and [Send](/agent-pay/send#sca-on-a-payout).
## Next steps
Render the payment inside your own product.
Pay-links, off-session charges, SCA, refunds.
Payouts, SCA, batches, contract limits.
Hand the SDK to your model as tools.
Auth, methods, types, errors, sandbox, go-live.
The full flow as a runnable Jupyter notebook β `pip install atoa-agent-pay`, add a sandbox `ATOA_API_KEY`, run.
# Agent Pay SDK Reference: Python & TypeScript
Source: https://docs.paywithatoa.co.uk/agent-pay/reference
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.3 | Node 22+ |
| `atoa-agent-pay` | PyPI | 0.0.3 | Python 3.10+ |
| `@atoapayments/agentic-payment-approvals-js` | npm | 0.0.3 | Browser (TypeScript) |
| `@atoapayments/pay-embed` | npm | 0.0.1 | Browser β [embedded checkout](/agent-pay/embed) |
## 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 checks the signature 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):
```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)
```
## KMS / custom signer
Keep the private key in a KMS (key management service) so it never enters your process: pass a `signer` instead of
a PEM. Per request, the SDK hands your signer the canonical subject string and expects a **detached compact JWS**
(JSON Web Signature) 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.
```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
```
## 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 in pounds, not pence β `{ amount: 12.50, currency?: "GBP" }`, so Β£12.50
is `12.50`, never `1250`; `currency` defaults to GBP.
**Pagination.** Every `list(opts?)` returns a `Page` envelope; the paired `listAll(opts?)` walks every page and
returns a flat `T[]`.
The page of items (`Contract` / `Payment` / `Customer` / `Store`).
Total matching rows across all pages.
Zero-based index of this page.
Page size (default 20).
Common paging inputs on every `list`: `page` (zero-based, default `0`) and `size` (default `20`).
## agent
### `agent.register(opts)`
Set up your agent's identity β the challenge β sign β register handshake in one call. **Idempotent** (safe to call
again): the same key/env/business returns the same agent; a changed `name`/`description` is a metadata update, not
a conflict.
**Parameters**
Human-readable agent name shown in the Atoa dashboard.
Free-text description of what this agent does.
Your ES256 public key (PEM, SPKI). Pass it **only** with a KMS/custom signer; omit when the SDK holds the private
key (it derives the public key itself).
**Request**
```python Python theme={null}
agent = atoa.agent.register(name="Bookings assistant")
```
```typescript TypeScript theme={null}
const agent = await atoa.agent.register({ name: "Bookings assistant" });
```
**Response** β `RegisteredAgent`
The id this client signs as from now on.
The business this agent belongs to.
Free-text description, if you set one.
Echoed here **only** β `me`/`list` do not return it.
```json Example theme={null}
{
"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**
```python Python theme={null}
self = atoa.agent.me()
```
```typescript TypeScript theme={null}
const self = await atoa.agent.me();
```
**Response** β `AgentIdentity`
Free-text description, if you set one.
e.g. `ACTIVE`.
```json Example theme={null}
{ "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**
```python Python theme={null}
all = atoa.agent.list()
```
```typescript TypeScript theme={null}
const all = await atoa.agent.list();
```
**Response** β `AgentIdentity[]`
An array of `AgentIdentity` (shape as `agent.me`).
## contract
### `contract.create(input)`
Create a spending contract. Returns it `PENDING_AUTHORIZATION` with an `authorizationUrl` the account holder (SEND)
or customer (COLLECT) opens to authorize; poll `awaitActive` until `ACTIVE`.
**Parameters**
Your label for this spending authority (e.g. "Supplier payouts").
`SEND` (money out) or `COLLECT` (money in, off-session).
**Required for `COLLECT`** β the customer this contract charges (`id` from `customer.create`).
Ignored for SEND.
Free-text description.
The per-payment cap, β₯1 period cap, and validity window.
Max for any single payment, in the contract currency.
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.
When the consent expires β must be bounded.
When the consent starts.
The single currency for every cap in this contract.
**COLLECT only** β take a first charge inside the customer's approval: their single approval activates the contract
**and** pays this amount, so there is no separate approval (or `payment.collect` call) for charge #1. See
[Charge on approval](/agent-pay/collect#charge-on-approval).
In pounds, not pence, in the contract currency β β₯ Β£1 and β€ `maxPerPayment`.
Your order reference for the charge (β€ 50 chars).
Optional short note for your own tracking (β€ 30 chars).
Attribute the charge to a store (from `store.list`); defaults to your primary store. The store's name/logo shows on the customer's contract page.
**Request**
```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)
```
**Response** β `Contract`
The agent that owns this contract.
Emitted on every row so you can branch without inference.
Fresh from `create`: `PENDING_AUTHORIZATION`.
The page the human authorizes at (create/update only β **absent** on `get`/`list`).
The caps echoed back with defaults resolved.
Live per-period headroom (populated on `get` β see below).
**COLLECT only** β the customer this contract charges.
**COLLECT only** β the masked card the customer linked. Absent until they link one on the contract page.
**COLLECT only** β authorized terms version (1 on first approval; increments per approved update).
**COLLECT only, when one was set at create** β the bundled first charge:
`{ amount, currency, orderId, storeId?, status, paymentRequestId?, failureReason? }`.
`status` runs `AWAITING_APPROVAL` β `PENDING` β `COMPLETED` / `FAILED` (`INITIATION_FAILED` if the charge could not
start β the contract stays `ACTIVE` regardless). `paymentRequestId` is minted when the customer approves; once
present, poll `payment.get` with it. Before that, `orderId` is your correlation handle.
A staged limits change; the `limits` above stay enforced until it's authorized. `null` when nothing is staged.
```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"
}
}
```
### `contract.awaitActive(id, opts?)`
Poll `get` until the contract is `ACTIVE`, with internal backoff. Throws `AUTHORIZATION_TIMEOUT` on timeout or
`AUTHORIZATION_FAILED` if the human declined / the link expired.
**Parameters**
How long to wait before throwing `AUTHORIZATION_TIMEOUT`.
**Request**
```python Python theme={null}
active = atoa.contract.await_active(contract.contract_id)
```
```typescript TypeScript theme={null}
const active = await atoa.contract.awaitActive(contract.contractId);
```
**Response** β `Contract`
The `Contract` (shape above) once `status` is `ACTIVE`.
### `contract.get(id)`
Read a contract's current state, including live `usage`. No webhooks β poll this.
**Parameters**
**Request**
```python Python theme={null}
contract = atoa.contract.get(id)
```
```typescript TypeScript theme={null}
const contract = await atoa.contract.get(id);
```
**Response** β `Contract`
The `Contract` shape (see `contract.create`), plus live per-period usage:
One window per period cap β `remaining = cap β usedThisPeriod β reserved`.
`DAY` Β· `WEEK` Β· `FORTNIGHT` Β· `MONTH` Β· `HALF_YEAR` Β· `YEAR`.
The limit for this window.
Settled spend, in-flight (initiated, unsettled) spend, and headroom left.
When this window and its headroom reset.
### `contract.list(opts?)` Β· `contract.listAll(opts?)`
List this business's contracts. `list` returns one page; `listAll` walks every page and returns a flat array. (Two
methods, one underlying list β grouped for that reason.)
**Parameters**
All filters optional. `listAll` takes the same shape minus `page`.
Narrow to one direction.
Narrow to one status.
**COLLECT only** β narrow to one customer's contracts.
Zero-based page index.
Page size.
**Request**
```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" });
```
**Response** β `Page` (`listAll` β `Contract[]`)
The [pagination envelope](#methods); `data` is `Contract[]`.
Every page flattened.
### `contract.update(id, input)`
Change a contract's `limits` β the human must approve the new caps. Returns it `PENDING_AUTHORIZATION` with a
**new** `authorizationUrl`; the old consent stays live and payable until the new one is approved. Poll
`awaitActive` again.
**Parameters**
The new caps (same shape as `create`).
**Request**
```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" },
});
```
**Response** β `Contract`
The `Contract`, back at `PENDING_AUTHORIZATION` with a fresh `authorizationUrl`.
### `contract.revoke(id)`
Terminate the contract. Payments against it are then rejected.
**Parameters**
**Request**
```python Python theme={null}
result = atoa.contract.revoke(id)
```
```typescript TypeScript theme={null}
const result = await atoa.contract.revoke(id);
```
**Response** β `ContractRevokeResult`
`{ contractId, type, status }` β the lean terminal confirmation.
## payment
### `payment.collect(input)`
Money **in**, one verb. `contractId` **absent** β a pay-link/QR the customer pays now. `contractId` **present** β
an off-session charge on the linked method of that `COLLECT` contract (no card details, ever). Either way, follow
with `awaitSettled(paymentRequestId)`. One payment per call; not idempotent.
**Parameters**
The amount to collect.
Your order reference β echoed on the payment and the dashboard (and on webhooks, when they arrive in a later
phase). Not an idempotency key.
Present β off-session charge on this `COLLECT` contract's linked method. Absent β a pay-link/QR the customer pays
now.
The managed Atoa customer (`id` from `customer.create`). **Required for an off-session charge** β pass it with
`contractId`, and it **must match the contract's own `atoaCustomerId`** (a mismatch is rejected with
`CONTRACT_CUSTOMER_MISMATCH`). Optional for a pay-link, where it links the payment to a managed customer (and
enables `savePaymentMethod`). Not the same as `customerId`, which is your own reference.
YOUR id for the payer, echoed back everywhere. Omit for guest checkout (one is synthesized).
Prefill the checkout with the customer's details.
Where the customer lands after paying.
Pay-link lifetime (default 3 minutes).
The rest are **optional power knobs** β the simple path never needs them:
Restrict how the customer may pay. Both show by default.
Save the card during checkout. Needs `atoaCustomerId` + `paymentMethod: ['CARD']`; incompatible with `splitBill`.
Charge under a specific store (see `store.list`); defaults to the primary store.
Till/receipt QR template β sets `templateUrl`.
Allow several attempts while the link is live.
Echoed back to your `redirectUrl` after payment.
Free-text note captured on the payment.
**Request**
```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β¦",
});
```
**Response** β `PaymentRequest`
Poll `awaitSettled` with this β the same id Atoa's checkout widgets + direct API use.
Your reference, echoed back.
Your payer reference (or the synthesized one for guest checkout).
The requested amount + currency.
Pay-link mode β the link/QR the customer pays at. Also `expiresAt`.
Off-session charge mode β immediate charge state + the contract it ran under.
Off-session charge mode, only when the customer must approve β see [approval gate](#approval-gate). Absent for a pay-link.
```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"
}
```
### `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**
The `ACTIVE` SEND contract every instruction is paid against.
1β20 instructions; each `orderId` must be unique in the batch.
`name` is checked by Confirmation of Payee; `sortCode` 6-digit, `accountNumber` 8-digit.
Your reference, passed through. **Not** an idempotency key.
Shown to the beneficiary (1β35 chars).
**Request**
```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);
```
**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.
```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β¦"
}
]
```
### `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**
A `paymentRequestId` or a `paymentIdempotencyId`.
**Request**
```python Python theme={null}
payment = atoa.payment.get(payment_request_id)
```
```typescript TypeScript theme={null}
const payment = await atoa.payment.get(paymentRequestId);
```
**Response** β `Payment`
`CREDIT` = money in (COLLECT) Β· `DEBIT` = money out (SEND). Discriminates which fields below are present.
`AWAITING_AUTHORIZATION` Β· `PENDING` Β· `AUTHORIZED` Β· `COMPLETED` Β· `FAILED` Β· `CANCELLED` Β· `EXPIRED` Β· `PARTIALLY_REFUNDED` Β· `REFUNDED`.
What settled β flat, not nested.
Your reference (not a de-dup key).
Settlement-attempt id; `null` until an attempt exists.
**SEND (DEBIT) only** β the contract this draw ran under.
**SEND (DEBIT) only** β who you paid, masked.
**SEND (DEBIT) only** β present while the draw is gated on an approval.
**COLLECT (CREDIT) only** β the payment-request (parent) id.
**COLLECT (CREDIT) only** β your own payer reference, and the Atoa customer id (once known).
**COLLECT (CREDIT) only** β who paid, once an attempt exists: payer name, bank, and masked account.
Either direction β present on a terminal `FAILED`/`CANCELLED`.
```json Example theme={null}
{
"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**
A `paymentRequestId` or a `paymentIdempotencyId`.
How long to wait before throwing `SETTLEMENT_TIMEOUT`.
**Request**
```python Python theme={null}
settled = atoa.payment.await_settled(payment_request_id)
```
```typescript TypeScript theme={null}
const settled = await atoa.payment.awaitSettled(paymentRequestId);
```
**Response** β `Payment`
The `Payment` (shape above) at a resting state.
### `payment.list(opts?)` Β· `payment.listAll(opts?)`
List this agent's payments across both directions. `list` returns one page; `listAll` walks every page. (Two
methods, one underlying list.)
**Parameters**
All filters optional. `listAll` takes the same shape minus `page`.
Direction β `SEND` (money out) or `COLLECT` (money in). Rows come back with `type` `DEBIT`/`CREDIT` respectively.
Narrow to one status.
**SEND only** β narrow to one contract's draws.
**COLLECT only** β the attempts of one payment request (parent β children).
**COLLECT only** β narrow to specific payers by their Atoa customer id(s).
Zero-based page index.
Page size.
**Request**
```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" });
```
**Response** β `Page` (`listAll` β `Payment[]`)
The [pagination envelope](#methods); `data` is `Payment[]` (shape above).
Every page flattened.
### `payment.refund(id, input)`
Refund a `COMPLETED` collected payment β full or partial.
**Parameters**
The collected payment to refund.
`{ amount, reason? }` β the amount must not exceed the paid amount. Sandbox: `reason: "FAILURE TEST"` forces a
`FAILED` refund.
**Request**
```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" });
```
**Response** β `Refund`
The refund id β pass to `cancelRefund` while still `INITIATED`.
The collected payment this refund belongs to.
`INITIATED` Β· `COMPLETED` Β· `FAILED` Β· `CANCELLED`.
The refunded amount (`refund_amount`, a float, in Python).
The original paid amount.
```json Example theme={null}
{ "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**
Only a not-yet-processed refund can be cancelled.
**Request**
```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);
```
**Response**
The refunds of the payment.
`{ refundId, message? }`.
### `payment.cancel(id)`
Cancel an unpaid collect (link) or a charge resting at `AUTHORIZED`.
**Parameters**
**Request**
```python Python theme={null}
cancelled = atoa.payment.cancel(payment_request_id)
```
```typescript TypeScript theme={null}
const cancelled = await atoa.payment.cancel(paymentRequestId);
```
**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**
From a `nextAction`.
How long to wait before throwing `SETTLEMENT_TIMEOUT`.
**Request**
```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 */ }
```
**Response** β `Approval`
Which side the approval gates.
The contract this approval is bound to.
`PENDING` Β· `APPROVED` Β· `DECLINED` Β· `CANCELLED` Β· `EXPIRED` Β· `SUPERSEDED`.
When the approval lapses.
Who decided and when, once resolved.
### `payment.cancelApproval(contractId, approvalId)`
Agent-initiated cancel of a pending approval β voids the underlying draws.
**Parameters**
**Request**
```python Python theme={null}
atoa.payment.cancel_approval(contract_id, approval_id)
```
```typescript TypeScript theme={null}
await atoa.payment.cancelApproval(contractId, approvalId);
```
**Response**
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**
2β30 characters.
Valid email. **Either `email` or a phone number is required.**
Digits only (e.g. `44`). Pair with `phoneNumber`.
Digits only, without country code.
Business customers only.
Optional postal details.
**Request**
```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" });
```
**Response** β `Customer`
The `atoaCustomerId` used across collect + saved cards.
```json Example theme={null}
{ "id": "cus_1fβ¦", "fullName": "Jane Doe", "email": "jane@example.com", "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**
Any subset of the create fields.
**Request**
```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);
```
**Response**
The `Customer` shape (above).
`{ success, message }`.
### `customer.list(opts?)`
List the customers this agent created.
**Parameters**
Zero-based page index.
Page size.
**Request**
```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 });
```
**Response** β `Page`
The [pagination envelope](#methods); `data` is `Customer[]`.
## store Β· client-level
### `store.list(opts?)`
List this business's stores β discover a `storeId` to tag a collect by location.
**Parameters**
Zero-based page index.
Page size.
**Request**
```python Python theme={null}
stores = atoa.store.list()
```
```typescript TypeScript theme={null}
const stores = await atoa.store.list();
```
**Response** β `Page`
The [pagination envelope](#methods); `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**
```python Python theme={null}
health = atoa.check_availability()
```
```typescript TypeScript theme={null}
const health = await atoa.checkAvailability();
```
**Response** β `AvailabilityStatus`
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**
```python Python theme={null}
test = atoa.sandbox_test_accounts()
```
```typescript TypeScript theme={null}
const test = atoa.sandboxTestAccounts();
```
**Response** β `SandboxTestAccounts`
`{ sandbox, note, accounts }` β the recipient accounts and their forced outcomes.
## Errors
Business outcomes are **returned** β branch on the `Payment`'s `status` and `failureReason`. Operational faults are
**thrown** `AgentPayError` subclasses β catch and switch on `code`.
**Returned `failureReason` values:**
| Direction | Reasons |
| ------------------ | ------------------------------------------------------------------------------------------------------ |
| Send (`DEBIT`) | `NAME_MISMATCH` Β· `LIMIT_EXCEEDED` Β· `CONTRACT_INACTIVE` Β· `SERVICE_UNAVAILABLE` Β· `SETTLEMENT_FAILED` |
| Collect (`CREDIT`) | `PAYMENT_REJECTED` Β· `AUTHORIZATION_FAILED` Β· `CUSTOMER_CANCELLED` Β· `MERCHANT_CANCELLED` |
| Approvals (either) | `APPROVAL_DECLINED` Β· `APPROVAL_EXPIRED` |
| Any | `UNKNOWN` β read `failureReasonDescription` |
**Thrown `AgentPayError` subclasses:**
| `code` | Thrown when |
| ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `AUTH_ERROR` | 401/403 β key invalid, not entitled, or a revoked agent. |
| `VALIDATION_ERROR` | 400/422 β malformed request. |
| `NOT_FOUND` | 404 β unknown or not-owned contract / payment / customer. |
| `CONFLICT` | 409 β re-registering an agent with a different key, env, or business. |
| `REGISTRATION_ERROR` | The register handshake failed. |
| `RATE_LIMIT` | 429 β back off exponentially and retry. |
| `KEY_NOT_FOUND` | No signing key available for the requested id. |
| `CONTRACT_CHARGE_ERROR` | Base of the collect-charge ladder below β catch this to handle any charge refusal in one branch. |
| `CONTRACT_NOT_FOUND` / `CONTRACT_NOT_ACTIVE` / `CONTRACT_TYPE_MISMATCH` / `CONTRACT_CUSTOMER_MISMATCH` | The contract is unknown, not chargeable (`.reason` says why), the wrong type, or another customer's. |
| `CAP_EXCEEDED` | Over a cap β `.remaining` is what's left this window. |
| `NO_PAYMENT_METHOD` | No usable method linked to the contract. |
| `ATOA_CUSTOMER_REQUIRED` | A contract charge without `atoaCustomerId`. |
| `PARAMS_CONFLICT` | Mutually exclusive options in one call. |
| `AUTHORIZATION_TIMEOUT` / `AUTHORIZATION_FAILED` | `awaitActive` timed out / the approver declined or the link expired. |
| `SETTLEMENT_TIMEOUT` | `awaitSettled` timed out. |
| `SERVER_ERROR` | 5xx β the service received the request but failed on its side. Retryable. |
| `API_ERROR` | An HTTP status the SDK doesn't map to a specific class (402, 405, 408, β¦). `status` is set. |
| `NETWORK_ERROR` | Connection failed (DNS/TLS/timeout) β the server never answered. Retryable. |
Retry guidance:
* **Retry:** `RATE_LIMIT` (back off), `SERVER_ERROR`, `NETWORK_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 β this is SCA (Strong Customer Authentication). 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 person 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 passkey. Your app never collects the credential.
`"APPROVAL"` for this gate. The full action vocabulary β every type, `for`, `mustEscalate`, and the fallback rule β is [the nextAction contract](/agent-pay/next-action); switch on `type` so new kinds don't break you.
Pass to `awaitDecision` / `cancelApproval`.
Bearer secret for the hosted page / the approvals browser SDK.
The hosted approval page to hand to the approver.
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**](/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`.
The credential β code or passkey β is entered only on Atoa's page. Never build your own form that collects it.
## FAQ
No. There's no origin allowlist and nothing to register β any page can host the checkout, `localhost`
included.
What protects the payment is the API key your server used to create it, and the customer's own one-time code
or passkey, entered inside the frame on Atoa's origin.
No. The API key creates payments and must stay on your server, and there is no method to create a payment
from the browser. `AtoaUI.mount()` shows a payment that already exists.
With no backend, send the customer a pay-link from `payment.collect` instead.
Check the browser console. `AtoaUI.mount: the embed frame at never connected within 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, confirm `container` resolved to a real element and that you passed
`environment`.
Not today. `theme` takes `mode` β `'auto'`, `'light'` or `'dark'` β and nothing else.
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 rather than accepted from the page.
Not in this phase. Merchant webhooks do not fire for contract payments β poll from your server with
`payment.get` or `awaitSettled` for settlement truth. Webhooks arrive in a later phase.
## 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. Poll from your server β `get` / `awaitSettled` β for settlement truth. Merchant webhooks do not fire for
agentic contract payments in this phase; webhooks arrive in a later phase.
6. Run one small real payment end to end β including a declined and a cancelled path β before scaling up.
## Changelog
* **0.0.3** β `initialCharge` on COLLECT contracts ([charge on approval](/agent-pay/collect#charge-on-approval)), the
`get_agent` tool, and the [`nextAction`](/agent-pay/next-action) envelope on verb results.
* **0.0.2** β first MIT-licensed release.
* **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.
# Agent Pay Send: Pay Money Out to Payees
Source: https://docs.paywithatoa.co.uk/agent-pay/send
Send payouts with Agent Pay under a SEND contract. The account owner authorises once at their bank and Atoa enforces per-payment and period caps.
Every send happens under a **SEND contract**: the account owner approves it once at their bank, and Atoa checks
its per-payment and period caps on every payout drawn from it.
## Set up the contract
```python Python theme={null}
contract = atoa.contract.create(
type="SEND",
name="Supplier payouts",
limits={
"max_per_payment": 500.00,
"period_limits": [{"amount": 2000.00, "period": "MONTH"}],
"valid_to": "2026-12-31T23:59:59Z",
},
)
print(contract.authorization_url) # the account OWNER approves here, at their bank
atoa.contract.await_active(contract.contract_id) # β ACTIVE
```
```typescript TypeScript theme={null}
const contract = await atoa.contract.create({
type: "SEND",
name: "Supplier payouts",
limits: {
maxPerPayment: 500.00,
periodLimits: [{ amount: 2000.00, period: "MONTH" }],
validTo: "2026-12-31T23:59:59Z",
},
});
console.log(contract.authorizationUrl); // the account OWNER approves here, at their bank
await atoa.contract.awaitActive(contract.contractId); // β ACTIVE
```
## Send a payout
`payment.send` takes an array of instructions β one element for a single payout β and returns one `Payment` per
instruction, in order, plus a **`nextAction`** (TS) / **`next_action`** (Python) for the owner's approval.
```python Python theme={null}
result = atoa.payment.send(
contract_id=contract.contract_id,
payments=[{
"amount": {"amount": 250.00},
"beneficiary": {"name": "ACME LTD", "sortCode": "040004", "accountNumber": "12345678"},
"orderId": "payout-3001",
"reference": "Invoice 2043",
}],
)
# result.next_action β the owner approves before money moves (next section)
```
```typescript TypeScript theme={null}
const result = await atoa.payment.send({
contractId: contract.contractId,
payments: [{
amount: { amount: 250.00 },
beneficiary: { name: "ACME LTD", sortCode: "040004", accountNumber: "12345678" },
orderId: "payout-3001",
reference: "Invoice 2043",
}],
});
// result.nextAction β the owner approves before money moves (next section)
```
**Sandbox:** the recipient account decides the outcome β `040004` / `12345678` settles `COMPLETED`; `10000002` and
`10000003` fail with a `failureReason`. Fetch the authoritative list with `atoa.sandboxTestAccounts()` rather than
hardcoding.
## SCA on a payout
SCA (Strong Customer Authentication) here is the **business owner's** sign-off: they approve every payout on Atoa's
page with a one-time code or a passkey before money moves. After the decision, read each payment's outcome β
business failures come back as `FAILED` payments you branch on, not thrown errors.
```python Python theme={null}
send_to_owner(result.next_action.approval_url) # your own delivery
decision = atoa.payment.await_decision(result.next_action.approval_id)
print(decision.status) # APPROVED / DECLINED / EXPIRED
for p in result: # result is still a list[Payment]
print(p.order_id, p.status, p.failure_reason or "") # e.g. NAME_MISMATCH, LIMIT_EXCEEDED
```
```typescript TypeScript theme={null}
sendToOwner(result.nextAction.approvalUrl); // your own delivery
const decision = await atoa.payment.awaitDecision(result.nextAction.approvalId);
console.log(decision.status); // APPROVED / DECLINED / EXPIRED
for (const p of result) { // result is still a Payment[]
console.log(p.orderId, p.status, p.failureReason ?? ""); // e.g. NAME_MISMATCH, LIMIT_EXCEEDED
}
```
To keep the owner in your own web UI, embed the approval with the
[**Approvals SDK**](/agent-pay/approvals) instead of sharing the URL β pass `nextAction.clientSecret` to
`confirmApproval`. A declined payout reads back `FAILED` with `failureReason: APPROVAL_DECLINED`; a lapsed one,
`APPROVAL_EXPIRED`. In sandbox, open the `approvalUrl` and force either decision.
## Batches
Up to **20** instructions per call; each `orderId` unique within the batch; results in the order you sent. A batch
is one `send` call, so it carries one `nextAction` β the owner approves it once.
```python Python theme={null}
payments = atoa.payment.send(
contract_id=contract.contract_id,
payments=[
{"amount": {"amount": 120.00}, "beneficiary": {"name": "ACME LTD", "sortCode": "040004", "accountNumber": "12345678"}, "orderId": "batch-1"},
{"amount": {"amount": 80.00}, "beneficiary": {"name": "GLOBEX", "sortCode": "040004", "accountNumber": "12345678"}, "orderId": "batch-2"},
],
)
for p in payments:
print(p.order_id, p.status)
```
```typescript TypeScript theme={null}
const payments = await atoa.payment.send({
contractId: contract.contractId,
payments: [
{ amount: { amount: 120.00 }, beneficiary: { name: "ACME LTD", sortCode: "040004", accountNumber: "12345678" }, orderId: "batch-1" },
{ amount: { amount: 80.00 }, beneficiary: { name: "GLOBEX", sortCode: "040004", accountNumber: "12345678" }, orderId: "batch-2" },
],
});
for (const p of payments) {
console.log(p.orderId, p.status);
}
```
## Update or revoke
`contract.update` returns a fresh `authorizationUrl` β the existing limits stay in force until the owner
approves the new ones. `contract.revoke` is final; further sends are rejected.
```python Python theme={null}
updated = atoa.contract.update(
contract.contract_id,
limits={"max_per_payment": 750.00, "period_limits": [{"amount": 3000.00, "period": "MONTH"}], "valid_to": "2027-06-30T23:59:59Z"},
)
print(updated.authorization_url) # owner re-approves the new caps
atoa.contract.revoke(contract.contract_id)
```
```typescript TypeScript theme={null}
const updated = await atoa.contract.update(contract.contractId, {
limits: { maxPerPayment: 750.00, periodLimits: [{ amount: 3000.00, period: "MONTH" }], validTo: "2027-06-30T23:59:59Z" },
});
console.log(updated.authorizationUrl); // owner re-approves the new caps
await atoa.contract.revoke(contract.contractId);
```
On **production** these calls move real money to real accounts. Keep amounts small and beneficiaries verified
while testing, and switch to `environment: "production"` only when you're ready.
# Fetch Account Balance
Source: https://docs.paywithatoa.co.uk/api-reference/account-initiation-services/fetch-account-balance
GET https://api.atoa.me/api/bank/accounts/{accountId}/balance
Retrieve account balance via the Atoa Bank Feed API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Use this to fetch the current balance and currency information of a linked bank account.
### Authorization
Bearer ``
**Request Body Schema**
ID of the user's bank account. This can be obtained from the redirect URL
after account linking or by using the [Fetch All Accounts API
Reference](/api-reference/account-initiation-services/fetch-all-accounts)
endpoint.
Refer:- [Accounts](/accounts)
**Response**
The current balance amount in the bank account.
The currency code of the account balance (e.g., GBP, EUR, USD).
```json 200 theme={null}
{
"balance": 100.45,
"currency": "GBP"
}
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": "[]"
}
```
```json 404 theme={null}
{
"name": "NOT FOUND",
"message": "Consent not found",
"status": 404,
"errors": "[]"
}
```
# Fetch Account Details
Source: https://docs.paywithatoa.co.uk/api-reference/account-initiation-services/fetch-account-details
GET https://api.atoa.me/api/bank/accounts/{accountId}
Retrieve account details via the Atoa Bank Feed API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Use this to fetch basic information about a linked bank account.
### Authorization
Bearer ``
**Request Body Schema**
ID of the user's bank account. This can be obtained from the redirect URL
after account linking or by using the [Fetch All Accounts API
Reference](/api-reference/account-initiation-services/fetch-all-accounts)
endpoint.
Refer:- [Accounts](/accounts)
**Response**
The ID of the user's bank account from the bank.
The name of the bank associated with the account. Useful for display purposes
or internal logging.
The bank account number for which the user has provided consent.
The sort code linked to the bank account.
```json 200 theme={null}
{
"accountId": "7d4c6e07-78e6-4ad4-8b08-7ac183a5e750",
"bankName": "mock-sandbox-v1",
"accountNumber": "50978130",
"sortCode": "726908"
}
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": "[]"
}
```
```json 404 theme={null}
{
"name": "NOT FOUND",
"message": "Consent not found",
"status": 404,
"errors": "[]"
}
```
# Fetch Account Transactions
Source: https://docs.paywithatoa.co.uk/api-reference/account-initiation-services/fetch-account-transactions
GET https://api.atoa.me/api/bank/accounts/{accountId}/transactions?from=YYYY-MM-DD&before=YYYY-MM-DD&cursor={cursor}
Retrieve account transactions via the Atoa Bank Feed API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Once bank account access is authorized, use the following API to fetch transaction data.
This endpoint uses [**cursor-based pagination**](#cursor-based-pagination), returning one bank page per call. Follow `links.next` exactly as returned until it is no longer present to page through all transactions. Responses use the `{ data, meta, links }` envelope shown in the sample response below.
### Authorization
Bearer ``
**Request Body Schema**
ID of the user's bank account. This can be obtained from the redirect URL
after account linking or by using the [Fetch All Accounts API
Reference](/api-reference/account-initiation-services/fetch-all-accounts)
endpoint.
Refer:- [Accounts](/accounts)
Inclusive lower bound on the booking date. Accepts either a plain
date (e.g. `2025-08-17`) or a full ISO 8601 date-time (e.g.
`2026-01-01T00:00:00Z`). **Only applies to the first call.** Once
pagination has started, the date range is held inside the cursor,
so `from` should not be sent again alongside a cursor.
Exclusive upper bound on the booking date. Accepts either a plain
date (e.g. `2025-08-17`) or a full ISO 8601 date-time.
**Only applies to the first call.** Should not be sent again
alongside a cursor. `before` requires `from` to also be set.
Cursor token taken from a previous response's `links.next` or `links.prev`.
Pass it back **unchanged** β do not read, decode, or modify it. A stale,
tampered, or reused cursor returns `410 Gone`, signalling that the caller
should restart pagination without a cursor.
**Response**
Array of transactions. Each transaction has the following fields:
A **unique identifier** for the transaction. Use this to deduplicate or
track individual transactions.
The **most recent date associated with the transaction.** This may be the
booking date or value date depending on the bank.
Indicates whether the transaction is a **CREDIT** (money received) or
**DEBIT** (money sent).
The **monetary value** of the transaction. This can be positive or
negative based on the type.
The **currency code** (e.g., GBP, EUR) in which the transaction was made.
Any **additional information or context** provided by the bank. This could
include metadata, internal codes, or bank-provided notes.
The **name of the merchant** involved in the transaction, if provided by
the bank.
A **high-level category** indicating the nature of the transaction (e.g.,
Payments, Transfers, ATM, etc.). Helps in transaction classification.
The **current state** of the transaction. Can be **BOOKED** (processed and
settled) or **PENDING** (initiated but not yet fully processed).
The payer object provides information about the sender of the funds.
The name of the payer.
Account details of the payer. **Note: This field might not be
available for some banks.**
Sort code of the payer's account.
Account number of the payer.
The payee object provides information about the recipient of the funds.
The name of the payee.
Account details of the payee. **Note: This field might not be
available for some banks.**
Sort code of the payee's account.
Account number of the payee.
The account balance **immediately after** the transaction. **Note: This
field might not be available for some banks.** Includes:
The available balance at that point in time.
The currency in which the balance is held.
Metadata for this page.
Number of transactions in `data`.
Pagination links. Follow the URLs exactly as returned.
URL of the current request.
URL of the next page. Absent when there is no next page.
URL of the previous page. Only present if the bank provides it.
### Cursor-based pagination
Atoa's `Get Account Transactions` endpoint uses cursor-based pagination. It accepts `from` and `before` as optional query parameters when sending the initial request, and accepts `cursor` as an optional query parameter in subsequent requests.
#### How it works
Atoa doesn't set a `limit` for the maximum number of transactions returned per page, but an individual bank can set a limit.
The `meta` object in the response includes `count` that represents the number of transactions in that page, but doesn't return a field for the total number of transactions.
The `data` object in the response contains an array of transactions.
The `links` object in the response returns cursor tokens that can be fed into the subsequent request:
* `prev` - a cursor or link to the previous page
* `self` - a cursor or link to the current page
* `next` - a cursor or link to the next page
#### Examples
`GET /accounts/{accountId}/transactions` returns `self` and `next`.
```json theme={null}
"links": {
"self": "https://api.atoa.me/api/bank/accounts/{accountId}/transactions?cursor=",
"next": "https://api.atoa.me/api/bank/accounts/{accountId}/transactions?cursor="
}
```
`GET /accounts/{accountId}/transactions?cursor=` returns `prev`, `self` and `next`.
```json theme={null}
"links": {
"prev": "https://api.atoa.me/api/bank/accounts/{accountId}/transactions?cursor=",
"self": "https://api.atoa.me/api/bank/accounts/{accountId}/transactions?cursor=",
"next": "https://api.atoa.me/api/bank/accounts/{accountId}/transactions?cursor="
}
```
`GET /accounts/{accountId}/transactions?cursor=` returns `prev` and `self`.
```json theme={null}
"links": {
"prev": "https://api.atoa.me/api/bank/accounts/{accountId}/transactions?cursor=",
"self": "https://api.atoa.me/api/bank/accounts/{accountId}/transactions?cursor="
}
```
Pass the cursor back unchanged. An expired or tampered cursor returns `410 Gone`, at which point restart pagination without a cursor. Because the date range is held inside the cursor, later calls should not re-send `from` or `before` alongside it.
```json 200 theme={null}
{
"data": [
{
"id": "txn_001",
"date": "2024-01-14T11:45:00Z",
"type": "DEBIT",
"amount": 89.99,
"currency": "GBP",
"description": "Amazon Marketplace",
"reference": "INV-4321-A",
"transactionCategory": "E-commerce",
"status": "BOOKED",
"payee": {
"name": "Amazon UK Ltd",
"accountDetails": {
"sortCode": "123456",
"accountNumber": "87654321"
}
},
"balance": { "amount": 2.37, "currency": "GBP" }
},
{
"id": "txn_002",
"date": "2024-01-12T09:30:00Z",
"type": "CREDIT",
"amount": 1500.0,
"currency": "GBP",
"description": "Salary Payment",
"reference": "SALARY-JAN",
"transactionCategory": "Income",
"status": "BOOKED",
"payer": {
"name": "ABC Company Ltd",
"accountDetails": {
"sortCode": "654321",
"accountNumber": "12345678"
}
},
"balance": { "amount": 1592.36, "currency": "GBP" }
}
],
"meta": { "count": 2 },
"links": {
"prev": "https://api.atoa.me/api/bank/accounts/{accountId}/transactions?cursor=",
"self": "https://api.atoa.me/api/bank/accounts/{accountId}/transactions?cursor=",
"next": "https://api.atoa.me/api/bank/accounts/{accountId}/transactions?cursor="
}
}
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": []
}
```
```json 404 theme={null}
{
"name": "NOT FOUND",
"message": "Consent not found",
"status": 404,
"errors": []
}
```
```json 410 theme={null}
{
"name": "GONE",
"message": "The pagination cursor is expired or invalid. Restart pagination without a cursor.",
"status": 410,
"errors": []
}
```
# Fetch All Accounts
Source: https://docs.paywithatoa.co.uk/api-reference/account-initiation-services/fetch-all-accounts
GET https://api.atoa.me/api/bank/{accountAuthId}/accounts
Retrieve all accounts via the Atoa Bank Feed API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Use this to fetch basic information about all linked bank accounts for a user.
### Authorization
Bearer ``
**Request Body Schema**
ID of the authorization/consent used to access the user's bank accounts. This
can be obtained from the redirect URL after [account linking](/accounts)
**Response**
Returns an array of bank account objects, each containing:
The ID of the user's bank account.
The name of the bank associated with the account. Useful for display purposes
or internal logging.
The bank account number for which the user has provided consent.
The sort code linked to the bank account.
```json 200 theme={null}
[
{
"accountId": "7d4c6e07-78e6-4ad4-8b08-7ac183a5e750",
"bankName": "mock-sandbox-v1",
"accountNumber": "50978130",
"sortCode": "726908"
}
]
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": "[]"
}
```
```json 404 theme={null}
{
"name": "NOT FOUND",
"message": "Consent not found",
"status": 404,
"errors": "[]"
}
```
# Initiate Authorization Flow
Source: https://docs.paywithatoa.co.uk/api-reference/account-initiation-services/initiate-account-authorization
POST https://api.atoa.me/api/bank/auth/initiate
Initiate account authorization via the Atoa Bank Feed API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Before retrieving transactions, your users must authorize access to their bank accounts.
When using a sandbox secret key, all bank feed endpoints return static data and do not reflect real-time transactions.
### Authorization
Bearer ``
**Request Body Schema**
The URL your users will be redirected to after completing the account
authorization flow.
Custom parameters you wish to pass through the flow. These will be appended as
query parameters to the redirectUrl once the user completes the account
authorization process.
**Response**
Use this to redirect your user to begin account authorization.
A unique identifier for tracking the authorization session. This ID will be
appended to the `redirectUrl` after the account authorization flow is
completed.
If the link expires while in use, users will be redirected to the
`redirectUrl`.
```json 200 theme={null}
{
"url": "https://atoa-link.com/link-bank?accountAuthId=ed016c93...&expiresAt=2025-04-16T17:53:48.174Z&env=PRODUCTION",
"accountAuthId": "ed016c93-68c2-4394-8e9c-1e700f8a1b15",
"expiresAt": "2025-04-16T17:53:48.174Z"
}
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": "[]"
}
```
# Revoke Account Access
Source: https://docs.paywithatoa.co.uk/api-reference/account-initiation-services/revoke-account-access
POST https://api.atoa.me/api/bank/auth/revoke
Revoke account access via the Atoa Bank Feed API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Revoke bank account access for a specific account, multiple accounts, or using an account authorization ID.
## Authorization
Bearer ``
**Request Body Schema**
ID of the user's bank account. This can be obtained from the redirect URL
after account linking or by using the [Fetch All Accounts API
Reference](/api-reference/account-initiation-services/fetch-all-accounts)
endpoint.
Refer:- [Accounts](/accounts)
Account authorization ID to revoke access for.
Comma-separated list of account IDs to revoke access for (e.g.,
"id1,id2,id3"). This can be obtained from the redirect URL
after account linking or by using the [Fetch All Accounts API
Reference](/api-reference/account-initiation-services/fetch-all-accounts)
endpoint.
Refer:- [Accounts](/accounts)
**Note**: Only one parameter can be provided: accountId, accountIds, or accountAuthId. Multiple parameters are not allowed.
## Consent Lifecycle & Expiry
* All consents are valid for 90 days.
* If auto-renew is enabled, we will attempt background renewal before expiry.
* Once expired, any call to transactions or account endpoints will return: 404 Consent not found
π§ **Recommendation**: Prompt users to re-authenticate as consent expiration nears (e.g., 7 days before expiry).
```json 200 Single Account theme={null}
{
"success": true,
"message": "Consent successfully revoked for the specified account",
"revokedDetails": {
"accountId": "account-id-example"
}
}
```
```json 200 Multiple Accounts theme={null}
{
"success": true,
"message": "Consent successfully revoked for all specified accounts",
"revokedDetails": {
"accountIds": [
{
"id": "account-id-1",
"status": "success"
},
{
"id": "account-id-2",
"status": "success"
}
]
}
}
```
```json 200 Partial Success theme={null}
{
"success": false,
"message": "Consent revocation completed with 1 successes and 1 failures",
"revokedDetails": {
"accountIds": [
{
"id": "account-id-1",
"status": "success"
},
{
"id": "account-id-2",
"status": "failed",
"error": "Consent not found"
}
]
}
}
```
```json 200 Account Auth ID theme={null}
{
"success": true,
"message": "Consent successfully revoked for the specified reference",
"revokedDetails": {
"accountAuthId": "auth-id-example"
}
}
```
```json 400 Multiple Parameters theme={null}
{
"name": "BAD_REQUEST",
"message": "Only one parameter can be provided: accountId, accountIds, or accountAuthId. Multiple parameters are not allowed.",
"status": 400,
"errors": "[]"
}
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": "[]"
}
```
```json 404 theme={null}
{
"name": "NOT FOUND",
"message": "Consent not found",
"status": 404,
"errors": "[]"
}
```
# Authorize Card | Card on File API
Source: https://docs.paywithatoa.co.uk/api-reference/card-on-file/authorize-card
POST https://api.atoa.me/api/customers/{customerId}/cards/authorize
Use Atoaβs Authorize Card API to securely save customer cards for future payments. Complete 3DS verification via a PCI-compliant hosted page.
Save a customer's card for future charges **without taking a payment**. Atoa returns a secure hosted page link β the customer enters their card details and completes 3DS verification, and the card is stored against their customer profile, ready to charge later.
No amount is involved and no money moves. The customer sees a 3DS verification, not a payment. Money moves only when you call [Charge Saved Card](./charge-saved-card).
Card details are captured exclusively on Atoa's PCI-compliant hosted page β they never touch your systems. Each call generates a fresh single-purpose link; request a new one rather than reusing old links.
### Authorization
Bearer ``
**Path Parameters**
The Atoa customer UUID returned by the [Create Customer API](/api-reference/customers/create-customer). This is **not** your own merchant reference for the customer.
**Request Body Schema**
All fields are optional β an empty body `{}` is valid.
When `true`, Atoa delivers the link to the customer by email; if the customer has no email, by SMS. The link is also returned in the response either way. Fails with `400` if the customer has neither an email address nor a phone number.
Where the hosted page redirects the customer after the card is saved successfully.
Where the hosted page redirects the customer if saving the card fails.
Your own key-value pairs to attach to the saved card.
* Up to **50** key-value pairs.
* Each key must be **1β40** characters and must not contain `[` or `]`.
* Each value must be a **string** of at most **500** characters.
Returned against the card in [List Payment Methods](/api-reference/payment-methods/list-payment-methods) so you can identify it.
**Response**
Secure hosted page URL. Open it in a browser, embed it in a WebView, or share it with the customer. Once they complete card entry and 3DS, the card is saved β there is no separate confirmation call to make.
The metadata you supplied on this request, echoed back so you can confirm what will be attached to the saved card.
After the customer completes the hosted page flow, retrieve the saved card with [List Payment Methods](/api-reference/payment-methods/list-payment-methods) and use its `id` as `paymentMethodId` when charging.
```bash curl theme={null}
curl --request POST \
--url https://api.atoa.me/api/customers/{customerId}/cards/authorize \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"sendLinkToCustomer": false,
"successRedirectUrl": "https://yoursite.com/cards/success",
"failureRedirectUrl": "https://yoursite.com/cards/failure",
"metadata": { "internalId": "42" }
}'
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/customers/{customerId}/cards/authorize"
payload = {
"sendLinkToCustomer": False,
"successRedirectUrl": "https://yoursite.com/cards/success",
"failureRedirectUrl": "https://yoursite.com/cards/failure",
"metadata": {"internalId": "42"}
}
headers = {
"Authorization": "Bearer ",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.atoa.me/api/customers/{customerId}/cards/authorize",
{
method: "POST",
headers: {
Authorization: "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
sendLinkToCustomer: false,
successRedirectUrl: "https://yoursite.com/cards/success",
failureRedirectUrl: "https://yoursite.com/cards/failure",
metadata: { internalId: "42" },
}),
}
);
const data = await response.json();
console.log(data);
```
```php PHP theme={null}
"https://api.atoa.me/api/customers/{customerId}/cards/authorize",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
"sendLinkToCustomer" => false,
"successRedirectUrl" => "https://yoursite.com/cards/success",
"failureRedirectUrl" => "https://yoursite.com/cards/failure",
"metadata" => ["internalId" => "42"]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer ",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
```
```go Go theme={null}
package main
import (
"fmt"
"strings"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.atoa.me/api/customers/{customerId}/cards/authorize"
payload := strings.NewReader(`{
"sendLinkToCustomer": false,
"successRedirectUrl": "https://yoursite.com/cards/success",
"failureRedirectUrl": "https://yoursite.com/cards/failure",
"metadata": { "internalId": "42" }
}`)
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer ")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.post("https://api.atoa.me/api/customers/{customerId}/cards/authorize")
.header("Authorization", "Bearer ")
.header("Content-Type", "application/json")
.body("{\"sendLinkToCustomer\":false,\"successRedirectUrl\":\"https://yoursite.com/cards/success\",\"failureRedirectUrl\":\"https://yoursite.com/cards/failure\",\"metadata\":{\"internalId\":\"42\"}}")
.asString();
```
```json 200 theme={null}
{
"hostedPageUrl": "https://atoa.me/add-card?...",
"metadata": { "internalId": "42" }
}
```
```json 400 theme={null}
{
"name": "BAD_REQUEST",
"message": "customerId must be a valid UUID.",
"status": 400,
"errors": []
}
```
```json 404 theme={null}
{
"name": "NOT_FOUND",
"message": "Customer not found",
"status": 404,
"errors": []
}
```
# Manual Cancel Payment
Source: https://docs.paywithatoa.co.uk/api-reference/card-on-file/cancel-payment
POST https://api.atoa.me/api/payments/card/payment-request/{paymentRequestId}/cancel
Cancel payment via the Atoa Card on File API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Cancel a previously authorized card payment. This releases the hold on the customer's card.
Only applicable for `MANUAL_CAPTURE` and `CAPTURE_BEFORE_EXPIRY` payments that have **not yet been captured**. Cannot cancel `AUTO_CAPTURE` or already captured payments.
### Authorization
Bearer ``
**Path Parameters**
The payment request ID from the [Charge Saved Card](./charge-saved-card) response.
**Request Body Schema**
Reason for cancellation.
* `Duplicate` β Duplicate payment
* `Abandoned` β Payment was abandoned
* `Requested by customer` β Customer requested cancellation
* `Other` β Other reason (requires `cancelledReasonDescription`)
Additional cancellation notes. Max 60 characters. **Required** when `cancelledReasonCode` is `Other`.
**Response**
`true` if the payment was cancelled successfully.
Confirmation message.
```bash curl theme={null}
curl --request POST \
--url https://api.atoa.me/api/payments/card/payment-request/{paymentRequestId}/cancel \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"cancelledReasonCode": "Requested by customer",
"cancelledReasonDescription": "Customer no longer needs the item"
}'
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/payments/card/payment-request/{paymentRequestId}/cancel"
payload = {
"cancelledReasonCode": "Requested by customer",
"cancelledReasonDescription": "Customer no longer needs the item"
}
headers = {
"Authorization": "Bearer ",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.atoa.me/api/payments/card/payment-request/{paymentRequestId}/cancel",
{
method: "POST",
headers: {
Authorization: "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
cancelledReasonCode: "Requested by customer",
cancelledReasonDescription: "Customer no longer needs the item",
}),
}
);
const data = await response.json();
console.log(data);
```
```php PHP theme={null}
"https://api.atoa.me/api/payments/card/payment-request/{paymentRequestId}/cancel",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
"cancelledReasonCode" => "Requested by customer",
"cancelledReasonDescription" => "Customer no longer needs the item"
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer ",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
```
```go Go theme={null}
package main
import (
"fmt"
"strings"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.atoa.me/api/payments/card/payment-request/{paymentRequestId}/cancel"
payload := strings.NewReader(`{
"cancelledReasonCode": "Requested by customer",
"cancelledReasonDescription": "Customer no longer needs the item"
}`)
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer ")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.post("https://api.atoa.me/api/payments/card/payment-request/{paymentRequestId}/cancel")
.header("Authorization", "Bearer ")
.header("Content-Type", "application/json")
.body("{\"cancelledReasonCode\":\"Requested by customer\",\"cancelledReasonDescription\":\"Customer no longer needs the item\"}")
.asString();
```
```json 200 theme={null}
{
"success": true,
"message": "Payment cancelled successfully"
}
```
```json 400 theme={null}
{
"name": "BAD_REQUEST",
"message": "Cannot cancel an AUTO_CAPTURE or already captured payment",
"status": 400,
"errors": []
}
```
```json 404 theme={null}
{
"name": "NOT_FOUND",
"message": "Payment not found",
"status": 404,
"errors": []
}
```
# Manual Capture Payment
Source: https://docs.paywithatoa.co.uk/api-reference/card-on-file/capture-payment
POST https://api.atoa.me/api/payments/card/payment-request/{paymentRequestId}/capture
Capture payment via the Atoa Card on File API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Capture a previously authorized card payment. This collects the funds from the customer's card.
Only applicable for `MANUAL_CAPTURE` and `CAPTURE_BEFORE_EXPIRY` payments that have not yet been captured. Cannot capture `AUTO_CAPTURE` payments (they are captured immediately on creation).
### Authorization
Bearer ``
**Path Parameters**
The payment request ID from the [Charge Saved Card](./charge-saved-card) response.
**Response**
`true` if the payment was captured successfully.
Confirmation message.
```bash curl theme={null}
curl --request POST \
--url https://api.atoa.me/api/payments/card/payment-request/{paymentRequestId}/capture \
--header 'Authorization: Bearer '
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/payments/card/payment-request/{paymentRequestId}/capture"
headers = {"Authorization": "Bearer "}
response = requests.post(url, headers=headers)
print(response.json())
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.atoa.me/api/payments/card/payment-request/{paymentRequestId}/capture",
{
method: "POST",
headers: { Authorization: "Bearer " },
}
);
const data = await response.json();
console.log(data);
```
```php PHP theme={null}
"https://api.atoa.me/api/payments/card/payment-request/{paymentRequestId}/capture",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer "
],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
```
```go Go theme={null}
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.atoa.me/api/payments/card/payment-request/{paymentRequestId}/capture"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("Authorization", "Bearer ")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.post("https://api.atoa.me/api/payments/card/payment-request/{paymentRequestId}/capture")
.header("Authorization", "Bearer ")
.asString();
```
```json 200 theme={null}
{
"success": true,
"message": "Payment captured successfully"
}
```
```json 400 theme={null}
{
"name": "BAD_REQUEST",
"message": "Cannot capture an AUTO_CAPTURE payment",
"status": 400,
"errors": []
}
```
```json 404 theme={null}
{
"name": "NOT_FOUND",
"message": "Payment not found",
"status": 404,
"errors": []
}
```
# Charge Saved Card | Card on File API
Source: https://docs.paywithatoa.co.uk/api-reference/card-on-file/charge-saved-card
POST https://api.atoa.me/api/payments/card/process-payment
Charge saved card via the Atoa Card on File API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Charge a customer's saved card without them being present (off-session). The customer must have a previously saved payment method (see [List Payment Methods](/api-reference/payment-methods/list-payment-methods)).
The response always returns `status: "PENDING"` to confirm that the payment processing has started. The final status (`COMPLETED`, `AUTHORIZED`, `FAILED`) is updated asynchronously once the card network confirms the transaction. Use [Webhooks](/api-reference/webhook/process-payment-webhook-response) to receive real-time status updates, or poll the [Get Payment Status](/api-reference/payment/get-payment-status) API.
For `MANUAL_CAPTURE` or `CAPTURE_BEFORE_EXPIRY`, the payment will move to `AUTHORIZED` once confirmed β you must then call [Manual Capture Payment](./capture-payment) to collect the funds. For `AUTO_CAPTURE`, the payment moves directly to `COMPLETED`.
### Authorization
Bearer ``
**Request Body Schema**
The customer UUID to charge. Must be a valid UUID.
The saved card ID from [List Payment Methods](/api-reference/payment-methods/list-payment-methods) response (`id` field). 1-64 characters.
How to capture the payment.
* `AUTO_CAPTURE` β Capture immediately once confirmed. Final status: `COMPLETED`.
* `MANUAL_CAPTURE` β Authorize only. Final status: `AUTHORIZED`. Call [Manual Capture Payment](./capture-payment) later to collect funds.
* `CAPTURE_BEFORE_EXPIRY` β Authorize and auto-capture before expiry if not manually captured or cancelled. Final status: `AUTHORIZED` until captured.
Payment amount in pounds. Example: `10.50` for ten pounds fifty pence. Minimum: Β£1.
Your merchant order reference for tracking. 1-50 characters, must not be blank.
Short payment description or notes. Max 30 characters.
Store UUID. If not provided, the merchant's primary store is used. Refer [Get Stores API](../payment/get-stores).
**Response**
Unique payment identifier (UUID). Use this for [Capture](./capture-payment), [Cancel](./cancel-payment), and status queries.
Always `PENDING` in the initial response. Indicates that payment processing has been initiated. The final status is updated asynchronously via [webhook](/api-reference/webhook/process-payment-webhook-response).
Payment amount in pounds.
Currency code (e.g., `GBP`).
Your merchant order reference.
The charged customer's UUID.
The card used for payment.
The capture type used.
ISO 8601 timestamp.
When the authorization expires (only for MANUAL\_CAPTURE / CAPTURE\_BEFORE\_EXPIRY).
```bash curl theme={null}
curl --request POST \
--url https://api.atoa.me/api/payments/card/process-payment \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"customerId": "550e8400-e29b-41d4-a716-446655440000",
"paymentMethodId": "card_abc123def456",
"captureType": "AUTO_CAPTURE",
"amount": 10.50,
"orderId": "ORDER-001",
"notes": "Payment for order #001"
}'
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/payments/card/process-payment"
payload = {
"customerId": "550e8400-e29b-41d4-a716-446655440000",
"paymentMethodId": "card_abc123def456",
"captureType": "AUTO_CAPTURE",
"amount": 10.50,
"orderId": "ORDER-001",
"notes": "Payment for order #001"
}
headers = {
"Authorization": "Bearer ",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.atoa.me/api/payments/card/process-payment",
{
method: "POST",
headers: {
Authorization: "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
customerId: "550e8400-e29b-41d4-a716-446655440000",
paymentMethodId: "card_abc123def456",
captureType: "AUTO_CAPTURE",
amount: 10.50,
orderId: "ORDER-001",
notes: "Payment for order #001",
}),
}
);
const data = await response.json();
console.log(data);
```
```php PHP theme={null}
"https://api.atoa.me/api/payments/card/process-payment",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
"customerId" => "550e8400-e29b-41d4-a716-446655440000",
"paymentMethodId" => "card_abc123def456",
"captureType" => "AUTO_CAPTURE",
"amount" => 10.50,
"orderId" => "ORDER-001",
"notes" => "Payment for order #001"
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer ",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
```
```go Go theme={null}
package main
import (
"fmt"
"strings"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.atoa.me/api/payments/card/process-payment"
payload := strings.NewReader(`{
"customerId": "550e8400-e29b-41d4-a716-446655440000",
"paymentMethodId": "card_abc123def456",
"captureType": "AUTO_CAPTURE",
"amount": 10.50,
"orderId": "ORDER-001",
"notes": "Payment for order #001"
}`)
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer ")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.post("https://api.atoa.me/api/payments/card/process-payment")
.header("Authorization", "Bearer ")
.header("Content-Type", "application/json")
.body("{\"customerId\":\"550e8400-e29b-41d4-a716-446655440000\",\"paymentMethodId\":\"card_abc123def456\",\"captureType\":\"AUTO_CAPTURE\",\"amount\":10.50,\"orderId\":\"ORDER-001\",\"notes\":\"Payment for order #001\"}")
.asString();
```
```json 201 (AUTO_CAPTURE) theme={null}
{
"paymentRequestId": "9baa68d8-362a-4127-994d-2ea622ef35ee",
"status": "PENDING",
"amount": 10.50,
"currency": "GBP",
"orderId": "ORDER-001",
"customerId": "550e8400-e29b-41d4-a716-446655440000",
"paymentMethodId": "card_abc123def456",
"captureType": "AUTO_CAPTURE",
"createdAt": "2025-06-15T10:30:00.000Z"
}
```
```json 201 (MANUAL_CAPTURE) theme={null}
{
"paymentRequestId": "9baa68d8-362a-4127-994d-2ea622ef35ee",
"status": "PENDING",
"amount": 10.50,
"currency": "GBP",
"orderId": "ORDER-001",
"customerId": "550e8400-e29b-41d4-a716-446655440000",
"paymentMethodId": "card_abc123def456",
"captureType": "MANUAL_CAPTURE",
"createdAt": "2025-06-15T10:30:00.000Z",
"expiresAt": "2025-06-22T10:30:00.000Z"
}
```
```json 400 theme={null}
{
"name": "BAD_REQUEST",
"message": "Amount should not be less than Β£1.",
"status": 400,
"errors": []
}
```
```json 404 theme={null}
{
"name": "NOT_FOUND",
"message": "Customer or card not found",
"status": 404,
"errors": []
}
```
# Introduction | Card on File API
Source: https://docs.paywithatoa.co.uk/api-reference/card-on-file/introduction
Atoa Card on File: securely save a customer's card during checkout and charge it later for repeat or subscription payments.
Card on File lets you securely save customer card details and charge them later β for recurring, on-demand, or subscription payments β without the customer re-entering their card.
Card on File requires card payments to be activated on your merchant account. Contact support if not enabled.
## Save During Checkout
For most merchants, saving cards during the checkout flow is the simplest and most effective approach.
Use the [Create Customer API](/api-reference/customers/create-customer) to create a customer before checkout. This enables better tracking and customer management.
```bash theme={null}
curl --request POST \
--url https://api.atoa.me/api/customers \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"fullName": "John Doe",
"email": "john@example.com",
"phoneNumber": "7598570522",
"phoneCountryCode": "44"
}'
```
Response includes `id` (customer UUID) to use as `atoaCustomerId` in the next step.
Call [Process Payment](/api-reference/payment/process-payment) with `savePaymentMethod=true` and `atoaCustomerId` from step 1:
```bash theme={null}
curl --request POST \
--url https://api.atoa.me/api/payments/process-payment \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"customerId": "merchant-ref-123",
"orderId": "order-456",
"amount": 1000,
"storeId": "store-uuid",
"paymentMethod": ["CARD"],
"atoaCustomerId": "", // UUID returned from Create Customer API
"savePaymentMethod": true,
"redirectUrl": "https://yoursite.com/payment-callback"
}'
```
`atoaCustomerId` is the Atoa customer UUID returned from step 1 (Create Customer API). This is different from `customerId`, which is your own merchant reference for the customer.
The card is automatically saved after successful 3DS authentication.
Use [List Payment Methods](/api-reference/payment-methods/list-payment-methods) to see all cards saved for a customer:
```bash theme={null}
curl --request GET \
--url https://api.atoa.me/api/customers/{customerId}/cards \
--header 'Authorization: Bearer '
```
**Option A: Faster Checkout (One-Click)**
Pass `atoaCustomerId` in process-payment to automatically show saved cards at checkout:
```bash theme={null}
curl --request POST \
--url https://api.atoa.me/api/payments/process-payment \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"customerId": "merchant-ref-123",
"orderId": "order-789",
"amount": 2000,
"storeId": "store-uuid",
"paymentMethod": ["CARD"],
"atoaCustomerId": "customer-uuid",
"redirectUrl": "https://yoursite.com/payment-callback"
}'
```
Customer sees their saved cards and can complete payment with one click.
**Option B: Charge Saved Card**
Use [Charge Saved Card](/api-reference/card-on-file/charge-saved-card) to charge a saved card without customer interaction:
```bash theme={null}
curl --request POST \
--url https://api.atoa.me/api/payments/card/process-payment \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"customerId": "merchant-ref-123",
"amount": 2000,
"paymentMethodId": "", // ID from List Payment Methods API
"capture": "AUTO_CAPTURE"
}'
```
## Save a Card Without a Payment
If you want to store a card **before** any money moves β for example when onboarding a customer, setting up a subscription, or collecting card details over a link β use the standalone [Authorize Card](/api-reference/card-on-file/authorize-card) API instead of `process-payment`.
Use the [Create Customer API](/api-reference/customers/create-customer) and keep the returned `id` β this is the `customerId` used in the next step.
Call [Authorize Card](/api-reference/card-on-file/authorize-card). Atoa returns a secure hosted page URL. No amount is involved and no money moves.
```bash theme={null}
curl --request POST \
--url https://api.atoa.me/api/customers/{customerId}/cards/authorize \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"sendLinkToCustomer": false,
"successRedirectUrl": "https://yoursite.com/cards/success",
"failureRedirectUrl": "https://yoursite.com/cards/failure"
}'
```
Response includes a `hostedPageUrl`. Present it in your app or WebView, or set `sendLinkToCustomer=true` to have Atoa deliver it by email (or SMS if the customer has no email).
The customer enters their card details on Atoa's PCI-compliant hosted page and completes 3DS authentication. Card details never touch your systems, and there is no separate confirmation call to make.
Retrieve the card with [List Payment Methods](/api-reference/payment-methods/list-payment-methods), then charge it at any time with [Charge Saved Card](/api-reference/card-on-file/charge-saved-card) using the returned `paymentMethodId`.
## Which Flow Should I Use?
Both flows end with a card on file β the difference is whether a payment happens at the same time.
| | `savePaymentMethod` in Process Payment | Standalone Authorize Card |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **When to use** | Customer is already paying at checkout and you want to save their card for next time | You want to save a card upfront, with no payment β e.g. subscription setup, onboarding |
| **Endpoint** | [Process Payment](/api-reference/payment/process-payment) with `savePaymentMethod=true` | [Authorize Card](/api-reference/card-on-file/authorize-card) |
| **Money moves?** | Yes β the payment amount is charged | No β authorization only, Β£0 |
| **Customer experience** | Pays as normal at checkout; card is saved automatically after successful 3DS | Opens a hosted page link, enters card details, completes 3DS β no payment shown |
| **Link delivery** | Not applicable β happens inside checkout | Return the `hostedPageUrl` yourself, or set `sendLinkToCustomer=true` for email/SMS delivery |
| **What you get back** | A normal payment response; the saved card appears in [List Payment Methods](/api-reference/payment-methods/list-payment-methods) | A `hostedPageUrl`; the saved card appears in [List Payment Methods](/api-reference/payment-methods/list-payment-methods) once completed |
Either way, charge the saved card later with [Charge Saved Card](/api-reference/card-on-file/charge-saved-card).
## Authentication
All Card on File endpoints use Bearer token authentication. Generate your `accessSecret` from the **Atoa Business App** or **Web Dashboard** (Profile icon > Settings > API Access).
```
Authorization: Bearer
```
## Capture Types
When processing a card payment, you choose a capture type that determines how and when funds are collected:
| Capture Type | Behaviour |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AUTO_CAPTURE` | Payment is captured immediately. No further action needed. Status goes to `COMPLETED`. |
| `MANUAL_CAPTURE` | Payment is authorized but not captured. You must call [Manual Capture Payment](/api-reference/card-on-file/capture-payment) to collect the funds. Status is `AUTHORIZED` until captured. |
| `CAPTURE_BEFORE_EXPIRY` | Payment is authorized and will be automatically captured before the authorization expires, unless you manually capture or cancel it first. |
## Payment Status Lifecycle
```mermaid theme={null}
---
config:
flowchart:
curve: linear
---
flowchart LR
PP["Process Payment"] --> AC["AUTO_CAPTURE"]
PP --> MC["MANUAL_CAPTURE"]
PP --> CBE["CAPTURE_BEFORE_EXPIRY"]
AC -->|"Immediate"| COMP1["β
COMPLETED"]
MC --> AUTH1["AUTHORIZED"]
AUTH1 -->|"Capture"| COMP2["β
COMPLETED"]
AUTH1 -->|"Cancel"| CANC1["β CANCELLED"]
CBE --> AUTH2["AUTHORIZED"]
AUTH2 -->|"Capture"| COMP3["β
COMPLETED"]
AUTH2 -->|"Cancel"| CANC2["β CANCELLED"]
AUTH2 -->|"Auto-capture before expiry"| COMP4["β
COMPLETED"]
```
## Webhooks
Card on File payments trigger the same `PAYMENTS_STATUS` webhook event as Pay by Bank payments. Subscribe to webhooks to receive real-time notifications when payment status changes.
For card payments, the webhook payload includes additional statuses:
| Status | Description |
| ------------ | --------------------------------------------------------------------------------------------- |
| `COMPLETED` | Payment has been captured successfully. Funds will be settled to the merchant. |
| `PENDING` | Payment is being processed. |
| `AUTHORIZED` | Payment has been authorized but not yet captured (MANUAL\_CAPTURE / CAPTURE\_BEFORE\_EXPIRY). |
| `FAILED` | Payment failed. The `errorDescription` field contains details. |
| `CANCELLED` | An authorized payment was cancelled before capture. |
See [Payment Status Webhook Payload](/api-reference/webhook/process-payment-webhook-response) for the full payload structure.
Subscribe to `PAYMENT_METHOD_STATUS` as well to be told when a customer saves or removes a card β see [Payment Method Status Webhook Payload](/api-reference/webhook/payment-method-status-webhook-response).
## Checking Payment Status
Use the existing [Get Payment Status](/api-reference/payment/get-payment-status) API (`GET /api/payments/v1/payment-status/{paymentRequestId}`) to check the status of card payments. For card transactions, the response includes a `cardPaymentDetails` object in each transaction detail with card-specific information such as card type, capture type, and masked card number.
## Error Handling
All endpoints return errors in this format:
```json theme={null}
{
"name": "BAD_REQUEST",
"message": "Invalid atoaCustomerId. Customer not found or does not belong to this business.",
"status": 400,
"errors": []
}
```
| Status Code | Description |
| ----------- | -------------------------------------------------------- |
| `400` | Validation error β check the `message` field for details |
| `401` | Invalid or missing authentication token |
| `404` | Resource not found (customer, card, or payment) |
| `409` | Conflict (e.g., duplicate customer email) |
| `500` | Internal server error |
# Create Customer | Customers API
Source: https://docs.paywithatoa.co.uk/api-reference/customers/create-customer
POST https://api.atoa.me/api/customers
Create customer via the Atoa Customers API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Create a customer if you want to save card details and make future payments. Guest checkout or one-off payment doesn't mandate you to create a customer.
### Authorization
Bearer ``
**Request Body Schema**
Customer's full name. Must be 2-30 characters and not blank.
Customer's email address. Must be a valid email format. Either email or phone number is required.
Customer type.
* `INDIVIDUAL` - Individual customer (default)
* `BUSINESS` - Business customer
Phone country code, digits only. Max 5 characters. Example: `44` for UK.
Phone number without country code, digits only. Max 15 characters.
Customer's address. Max 100 characters.
Customer's city. Max 30 characters.
Customer's postcode. 4-8 characters, letters, numbers and spaces only. Example: `SW1A 1AA`.
VAT number for business customers. Must be 9 digits, optionally prefixed with `GB`. Example: `123456789` or `GB123456789`.
Set of key-value pairs that you can attach to a customer. This can be useful for storing additional information about the customer in a structured format.
* Up to **50** key-value pairs.
* Each key must be **1-40** characters, must not contain `[` or `]`, and must not be blank or have leading or trailing whitespace.
* Each value must be a **string** of at most **500** characters.
* A key sent with an empty-string value (`""`) is dropped.
Once set, metadata is returned in the **Get Customer**, **List Customers**, and **Update Customer** responses.
**Response**
Unique identifier for the customer (UUID).
Customer's full name.
Customer's email address.
Customer type (`INDIVIDUAL` or `BUSINESS`).
Phone country code.
Phone number.
Customer's address.
Customer's city.
Customer's postcode.
VAT number (business customers only).
Key/value string pairs attached to the customer.
ISO 8601 creation timestamp.
```bash curl theme={null}
curl --request POST \
--url https://api.atoa.me/api/customers \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"fullName": "John Doe",
"email": "john.doe@example.com",
"type": "INDIVIDUAL",
"phoneCountryCode": "44",
"phoneNumber": "7911123456",
"address": "123 High Street",
"city": "London",
"postcode": "SW1A 1AA",
"metadata": {
"crmId": "CRM-8842",
"loyaltyTier": "gold"
}
}'
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/customers"
payload = {
"fullName": "John Doe",
"email": "john.doe@example.com",
"type": "INDIVIDUAL",
"phoneCountryCode": "44",
"phoneNumber": "7911123456",
"address": "123 High Street",
"city": "London",
"postcode": "SW1A 1AA",
"metadata": {
"crmId": "CRM-8842",
"loyaltyTier": "gold"
}
}
headers = {
"Authorization": "Bearer ",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
```
```javascript JavaScript theme={null}
const response = await fetch("https://api.atoa.me/api/customers", {
method: "POST",
headers: {
Authorization: "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
fullName: "John Doe",
email: "john.doe@example.com",
type: "INDIVIDUAL",
phoneCountryCode: "44",
phoneNumber: "7911123456",
address: "123 High Street",
city: "London",
postcode: "SW1A 1AA",
metadata: {
crmId: "CRM-8842",
loyaltyTier: "gold",
},
}),
});
const data = await response.json();
console.log(data);
```
```php PHP theme={null}
"https://api.atoa.me/api/customers",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
"fullName" => "John Doe",
"email" => "john.doe@example.com",
"type" => "INDIVIDUAL",
"phoneCountryCode" => "44",
"phoneNumber" => "7911123456",
"address" => "123 High Street",
"city" => "London",
"postcode" => "SW1A 1AA",
"metadata" => [
"crmId" => "CRM-8842",
"loyaltyTier" => "gold"
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer ",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
```
```go Go theme={null}
package main
import (
"fmt"
"strings"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.atoa.me/api/customers"
payload := strings.NewReader(`{
"fullName": "John Doe",
"email": "john.doe@example.com",
"type": "INDIVIDUAL",
"phoneCountryCode": "44",
"phoneNumber": "7911123456",
"address": "123 High Street",
"city": "London",
"postcode": "SW1A 1AA",
"metadata": {
"crmId": "CRM-8842",
"loyaltyTier": "gold"
}
}`)
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer ")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.post("https://api.atoa.me/api/customers")
.header("Authorization", "Bearer ")
.header("Content-Type", "application/json")
.body("{\"fullName\":\"John Doe\",\"email\":\"john.doe@example.com\",\"type\":\"INDIVIDUAL\",\"phoneCountryCode\":\"44\",\"phoneNumber\":\"7911123456\",\"address\":\"123 High Street\",\"city\":\"London\",\"postcode\":\"SW1A 1AA\",\"metadata\":{\"crmId\":\"CRM-8842\",\"loyaltyTier\":\"gold\"}}")
.asString();
```
```json 201 theme={null}
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"fullName": "John Doe",
"email": "john.doe@example.com",
"type": "INDIVIDUAL",
"phoneCountryCode": "44",
"phoneNumber": "7911123456",
"address": "123 High Street",
"city": "London",
"postcode": "SW1A 1AA",
"metadata": {
"crmId": "CRM-8842",
"loyaltyTier": "gold"
},
"createdAt": "2025-06-15T10:30:00.000Z"
}
```
```json 400 theme={null}
{
"name": "BAD_REQUEST",
"message": "Full name must be at least 2 characters.",
"status": 400,
"errors": []
}
```
# Delete Customer | Customers API
Source: https://docs.paywithatoa.co.uk/api-reference/customers/delete-customer
DELETE https://api.atoa.me/api/customers/{customerId}
Delete customer via the Atoa Customers API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Delete a customer by ID. This also removes all saved payment methods associated with the customer.
### Authorization
Bearer ``
**Path Parameters**
The customer's unique identifier (UUID).
```bash curl theme={null}
curl --request DELETE \
--url https://api.atoa.me/api/customers/{customerId} \
--header 'Authorization: Bearer '
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/customers/{customerId}"
headers = {"Authorization": "Bearer "}
response = requests.delete(url, headers=headers)
print(response.json())
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.atoa.me/api/customers/{customerId}",
{
method: "DELETE",
headers: { Authorization: "Bearer " },
}
);
const data = await response.json();
console.log(data);
```
```php PHP theme={null}
"https://api.atoa.me/api/customers/{customerId}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer "
],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
```
```go Go theme={null}
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.atoa.me/api/customers/{customerId}"
req, _ := http.NewRequest("DELETE", url, nil)
req.Header.Add("Authorization", "Bearer ")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.delete("https://api.atoa.me/api/customers/{customerId}")
.header("Authorization", "Bearer ")
.asString();
```
```json 200 theme={null}
{
"success": true,
"message": "Customer deleted successfully"
}
```
```json 404 theme={null}
{
"name": "NOT_FOUND",
"message": "Customer not found",
"status": 404,
"errors": []
}
```
# Get Customer | Customers API
Source: https://docs.paywithatoa.co.uk/api-reference/customers/get-customer
GET https://api.atoa.me/api/customers/{customerId}
Retrieve customer via the Atoa Customers API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Get a customer by ID.
### Authorization
Bearer ``
**Path Parameters**
The customer's unique identifier (UUID).
**Response**
Customer UUID.
Customer's full name.
Customer's email address.
`INDIVIDUAL` or `BUSINESS`.
Phone country code.
Phone number.
Customer's address.
Customer's city.
Customer's postcode.
Key/value string pairs attached to the customer.
ISO 8601 creation timestamp.
```bash curl theme={null}
curl --request GET \
--url https://api.atoa.me/api/customers/{customerId} \
--header 'Authorization: Bearer '
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/customers/{customerId}"
headers = {"Authorization": "Bearer "}
response = requests.get(url, headers=headers)
print(response.json())
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.atoa.me/api/customers/{customerId}",
{
headers: { Authorization: "Bearer " },
}
);
const data = await response.json();
console.log(data);
```
```php PHP theme={null}
"https://api.atoa.me/api/customers/{customerId}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer "
],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
```
```go Go theme={null}
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.atoa.me/api/customers/{customerId}"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer ")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.get("https://api.atoa.me/api/customers/{customerId}")
.header("Authorization", "Bearer ")
.asString();
```
```json 200 theme={null}
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"fullName": "John Doe",
"email": "john.doe@example.com",
"type": "INDIVIDUAL",
"phoneCountryCode": "44",
"phoneNumber": "7911123456",
"address": "123 High Street",
"city": "London",
"postcode": "SW1A 1AA",
"metadata": {
"crmId": "CRM-8842",
"loyaltyTier": "gold"
},
"createdAt": "2025-06-15T10:30:00.000Z"
}
```
```json 404 theme={null}
{
"name": "NOT_FOUND",
"message": "Customer not found",
"status": 404,
"errors": []
}
```
# List Customers | Customers API
Source: https://docs.paywithatoa.co.uk/api-reference/customers/list-customers
GET https://api.atoa.me/api/customers
List customers via the Atoa Customers API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Get all customers for the merchant with pagination.
### Authorization
Bearer ``
**Query Parameters**
Page number (0-indexed).
Number of items per page.
Search by customer name, email, or phone number.
Filter by metadata using `metadata[key]=value`, which returns only customers whose metadata contains that exact key/value pair. Repeat with distinct keys to combine conditions with AND, for example `?metadata[plan]=pro&metadata[region]=uk`. Keys may be any string, including digits only such as `metadata[5]=x`. Filtering the same key by more than one value returns a `400`.
**Response**
Array of customer objects.
Customer UUID.
Customer's full name.
Customer's email address.
`INDIVIDUAL` or `BUSINESS`.
Key/value string pairs attached to the customer.
ISO 8601 creation timestamp.
Total number of customers.
Current page number.
Items per page.
```bash curl theme={null}
curl --request GET \
--url 'https://api.atoa.me/api/customers?page=0&size=20' \
--header 'Authorization: Bearer '
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/customers"
params = {"page": 0, "size": 20}
headers = {"Authorization": "Bearer "}
response = requests.get(url, params=params, headers=headers)
print(response.json())
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.atoa.me/api/customers?page=0&size=20",
{
headers: { Authorization: "Bearer " },
}
);
const data = await response.json();
console.log(data);
```
```php PHP theme={null}
"https://api.atoa.me/api/customers?page=0&size=20",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer "
],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
```
```go Go theme={null}
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.atoa.me/api/customers?page=0&size=20"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer ")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.get("https://api.atoa.me/api/customers")
.header("Authorization", "Bearer ")
.queryString("page", 0)
.queryString("size", 20)
.asString();
```
```json 200 theme={null}
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"fullName": "John Doe",
"email": "john.doe@example.com",
"type": "INDIVIDUAL",
"phoneCountryCode": "44",
"phoneNumber": "7911123456",
"address": "123 High Street",
"city": "London",
"postcode": "SW1A 1AA",
"metadata": {
"crmId": "CRM-8842",
"loyaltyTier": "gold"
},
"createdAt": "2025-06-15T10:30:00.000Z"
}
],
"totalCount": 50,
"page": 0,
"size": 20
}
```
# Update Customer | Customers API
Source: https://docs.paywithatoa.co.uk/api-reference/customers/update-customer
PUT https://api.atoa.me/api/customers/{customerId}
Update customer via the Atoa Customers API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Update a customer by ID. All fields are optional β only include the fields you want to update.
### Authorization
Bearer ``
**Path Parameters**
The customer's unique identifier (UUID).
**Request Body Schema**
All fields are optional. Same validation rules as [Create Customer](./create-customer).
Customer's full name (2-30 characters).
Valid email address.
`INDIVIDUAL` or `BUSINESS`.
Phone country code (max 5 chars, digits only).
Phone number (max 15 chars, digits only).
Customer's address (max 100 characters).
Customer's city (max 30 characters).
Postcode (4-8 characters, alphanumeric).
VAT number (9 digits, optionally prefixed with `GB`).
Set of key-value pairs that you can attach to a customer. Supplied keys are merged onto the existing metadata rather than replacing it.
* Up to **50** key-value pairs.
* Each key must be **1-40** characters, must not contain `[` or `]`, and must not be blank or have leading or trailing whitespace.
* Each value must be a **string** of at most **500** characters.
* A key sent with an empty-string value (`""`) removes that key; other keys are added or updated.
Once set, metadata is returned in the **Get Customer**, **List Customers**, and **Update Customer** responses.
```bash curl theme={null}
curl --request PUT \
--url https://api.atoa.me/api/customers/{customerId} \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"fullName": "John Doe Updated",
"address": "456 New Street",
"city": "Manchester",
"metadata": {
"loyaltyTier": "platinum"
}
}'
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/customers/{customerId}"
payload = {
"fullName": "John Doe Updated",
"address": "456 New Street",
"city": "Manchester",
"metadata": {
"loyaltyTier": "platinum"
}
}
headers = {
"Authorization": "Bearer ",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.json())
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.atoa.me/api/customers/{customerId}",
{
method: "PUT",
headers: {
Authorization: "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
fullName: "John Doe Updated",
address: "456 New Street",
city: "Manchester",
metadata: {
loyaltyTier: "platinum",
},
}),
}
);
const data = await response.json();
console.log(data);
```
```php PHP theme={null}
"https://api.atoa.me/api/customers/{customerId}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
"fullName" => "John Doe Updated",
"address" => "456 New Street",
"city" => "Manchester",
"metadata" => [
"loyaltyTier" => "platinum"
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer ",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
```
```go Go theme={null}
package main
import (
"fmt"
"strings"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.atoa.me/api/customers/{customerId}"
payload := strings.NewReader(`{
"fullName": "John Doe Updated",
"address": "456 New Street",
"city": "Manchester",
"metadata": {
"loyaltyTier": "platinum"
}
}`)
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("Authorization", "Bearer ")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.put("https://api.atoa.me/api/customers/{customerId}")
.header("Authorization", "Bearer ")
.header("Content-Type", "application/json")
.body("{\"fullName\":\"John Doe Updated\",\"address\":\"456 New Street\",\"city\":\"Manchester\",\"metadata\":{\"loyaltyTier\":\"platinum\"}}")
.asString();
```
```json 200 theme={null}
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"fullName": "John Doe Updated",
"email": "john.doe@example.com",
"type": "INDIVIDUAL",
"phoneCountryCode": "44",
"phoneNumber": "7911123456",
"address": "456 New Street",
"city": "Manchester",
"postcode": "SW1A 1AA",
"metadata": {
"crmId": "CRM-8842",
"loyaltyTier": "platinum"
},
"createdAt": "2025-06-15T10:30:00.000Z"
}
```
```json 404 theme={null}
{
"name": "NOT_FOUND",
"message": "Customer not found",
"status": 404,
"errors": []
}
```
# Get List of Available Institutions or Banks
Source: https://docs.paywithatoa.co.uk/api-reference/institutions/institutions-controller
Get https://api.atoa.me/api/institutions
Retrieve the list of supported UK banks and institutions for Atoa Pay by Bank payments, with logos and identifiers.
Retrieve a comprehensive list of available institutions, accompanied by their respective images.
```json 200 theme={null}
[
{
"id": "lloyds",
"name": "Lloyds (Personal)",
"fullName": "Lloyds (Personal)",
"media": [
{
"type": "icon",
"source": "https://atoa-static-images.s3.eu-west-2.amazonaws.com/bank_logos/v3/lloyds_4x.webp"
},
{
"type": "logo",
"source": "https://atoa-static-images.s3.eu-west-2.amazonaws.com/bank_logos/v3/lloyds_4x.webp"
}
],
"features": [
"INITIATE_DOMESTIC_SINGLE_PAYMENT",
"INITIATE_DOMESTIC_VARIABLE_RECURRING_PAYMENT_SWEEPING",
"ACCOUNT_TRANSACTIONS",
"ACCOUNTS",
"ACCOUNT",
"READ_DOMESTIC_SINGLE_REFUND",
"CREATE_DOMESTIC_VARIABLE_RECURRING_PAYMENT_SWEEPING"
],
"enabled": true,
"orderBy": 999,
"popularBank": false,
"bankPhoneNumber": null,
"transactionAmountLimit": null,
"businessBank": null,
"bankCode": "LOYD"
},
{
"id": "bankofscotland",
"name": "Bank of Scotland (Personal)",
"fullName": "Bank of Scotland (Personal)",
"media": [
{
"type": "logo",
"source": "https://atoa-static-images.s3.eu-west-2.amazonaws.com/bank_logos/v3/bank_of_scotland_4x.webp"
},
{
"type": "icon",
"source": "https://atoa-static-images.s3.eu-west-2.amazonaws.com/bank_logos/v3/bank_of_scotland_4x.webp"
}
],
"features": [
"INITIATE_DOMESTIC_SINGLE_PAYMENT",
"INITIATE_DOMESTIC_VARIABLE_RECURRING_PAYMENT_SWEEPING",
"ACCOUNT_TRANSACTIONS",
"ACCOUNTS",
"ACCOUNT",
"READ_DOMESTIC_SINGLE_REFUND"
],
"enabled": true,
"orderBy": 999,
"popularBank": false,
"bankPhoneNumber": null,
"transactionAmountLimit": null,
"businessBank": null,
"bankCode": "BOFS"
},
{
"id": "rbs",
"name": "Royal Bank of Scotland",
"fullName": "Royal Bank of Scotland",
"media": [
{
"type": "logo",
"source": "https://atoa-static-images.s3.eu-west-2.amazonaws.com/bank_logos/v3/rbs_4x.webp"
},
{
"type": "icon",
"source": "https://atoa-static-images.s3.eu-west-2.amazonaws.com/bank_logos/v3/rbs_4x.webp"
}
],
"features": [
"INITIATE_DOMESTIC_SINGLE_PAYMENT",
"INITIATE_DOMESTIC_VARIABLE_RECURRING_PAYMENT_SWEEPING",
"ACCOUNT_TRANSACTIONS",
"ACCOUNTS",
"ACCOUNT"
],
"enabled": true,
"orderBy": 999,
"popularBank": false,
"bankPhoneNumber": null,
"transactionAmountLimit": null,
"businessBank": null,
"bankCode": "RBSS"
}
]
```
```json 500 theme={null}
{
"name": "INTERNAL_SERVER_ERROR",
"message": "Error connecting to institution, please contact support if persists or try again later",
"status": 500,
"errors": "[]"
}
```
**Response**
Unique Institution id
Name of the Institution
Full name of the Institution
Type of media - `icon` or `logo`
URL of the image
List of supported features for this institution (e.g.,
`INITIATE_DOMESTIC_SINGLE_PAYMENT`, `ACCOUNT_TRANSACTIONS`)
Whether the institution is currently enabled
Sort order for displaying the institution
Whether the institution is marked as a popular bank
Phone number of the bank (if available)
Transaction amount limit for the institution (if applicable)
Whether this is a business banking institution
Bank identifier code (e.g., SWIFT/BIC code)
# API Reference | Introduction
Source: https://docs.paywithatoa.co.uk/api-reference/introduction
Atoa REST API reference: authenticate with a Bearer token and call JSON endpoints for payments, refunds, customers, bank feed and webhooks.
The Atoa API is a RESTful, JSON-based API for taking payments and accessing account data. All requests use standard HTTP verbs and status codes and are authenticated with a Bearer token from your Atoa Business app; the base URL is `https://api.atoa.me`. This reference documents every resource, Customers, Payment Methods, Payments, Refunds, Institutions, Bank Feed, Payouts, Card on File and Webhooks, with request and response schemas and copy-paste code samples in cURL, Python, JavaScript, PHP, Go and Java. New to Atoa? Start with Getting started, then test in the Sandbox before switching to production.
## Authentication
| | |
| ------------------------- | ------ |
| Security Scheme Type | HTTP |
| HTTP Authorization Scheme | Bearer |
| Bearer format | JWT |
## Use our Postman Collection
We've created a comprehensive Postman collection containing all of our APIs for your convenience.
To authenticate your requests:
1. Navigate to the Authorization tab in the root folder
2. Replace api-access-token with your actual API token
[
](https://www.postman.com/atoapayments-7757260/atoa-payments-s-workspace/collection/52893499-b8187798-8121-4e36-99ee-87e6d7c4ae86?action=share\&source=copy-link\&creator=52893499)
If the button above doesn't work, you can also [access our collection directly on Postman](https://www.postman.com/red-shuttle-882685/atoa-payments/overview).
# Delete Payment Method
Source: https://docs.paywithatoa.co.uk/api-reference/payment-methods/delete-payment-method
DELETE https://api.atoa.me/api/customers/{customerId}/cards/{cardId}
Delete payment method via the Atoa Payment Methods API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Remove a saved payment method. The card can no longer be used for payments.
### Authorization
Bearer ``
**Path Parameters**
The customer UUID who owns the card.
The card identifier to delete.
```bash curl theme={null}
curl --request DELETE \
--url https://api.atoa.me/api/customers/{customerId}/cards/{cardId} \
--header 'Authorization: Bearer '
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/customers/{customerId}/cards/{cardId}"
headers = {"Authorization": "Bearer "}
response = requests.delete(url, headers=headers)
print(response.json())
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.atoa.me/api/customers/{customerId}/cards/{cardId}",
{
method: "DELETE",
headers: { Authorization: "Bearer " },
}
);
const data = await response.json();
console.log(data);
```
```php PHP theme={null}
"https://api.atoa.me/api/customers/{customerId}/cards/{cardId}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer "
],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
```
```go Go theme={null}
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.atoa.me/api/customers/{customerId}/cards/{cardId}"
req, _ := http.NewRequest("DELETE", url, nil)
req.Header.Add("Authorization", "Bearer ")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.delete("https://api.atoa.me/api/customers/{customerId}/cards/{cardId}")
.header("Authorization", "Bearer ")
.asString();
```
```json 200 theme={null}
{
"success": true,
"message": "Card deleted successfully"
}
```
```json 404 theme={null}
{
"name": "NOT_FOUND",
"message": "Card not found",
"status": 404,
"errors": []
}
```
# Get Payment Method
Source: https://docs.paywithatoa.co.uk/api-reference/payment-methods/get-payment-method
GET https://api.atoa.me/api/customers/{customerId}/cards/{cardId}
Retrieve payment method via the Atoa Payment Methods API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Retrieve details of a single saved payment method.
### Authorization
Bearer ``
**Path Parameters**
The customer UUID who owns the card.
The card identifier (from [List Payment Methods](./list-payment-methods) response).
**Response**
Card identifier.
Cardholder name.
Card type (e.g., `credit`, `debit`).
Last 4 digits of the card number.
Card category.
Card type (e.g., `DEBIT`, `CREDIT`, `PREPAID`).
Card network brand (e.g., `VISA`, `MASTERCARD`).
Card issuing country code.
Card expiry date in `MM/YYYY` format.
The key/value string pairs attached to the card when it was saved.
ISO 8601 timestamp of when the card was saved.
```bash curl theme={null}
curl --request GET \
--url https://api.atoa.me/api/customers/{customerId}/cards/{cardId} \
--header 'Authorization: Bearer '
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/customers/{customerId}/cards/{cardId}"
headers = {"Authorization": "Bearer "}
response = requests.get(url, headers=headers)
print(response.json())
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.atoa.me/api/customers/{customerId}/cards/{cardId}",
{
headers: { Authorization: "Bearer " },
}
);
const data = await response.json();
console.log(data);
```
```php PHP theme={null}
"https://api.atoa.me/api/customers/{customerId}/cards/{cardId}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer "
],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
```
```go Go theme={null}
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.atoa.me/api/customers/{customerId}/cards/{cardId}"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer ")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.get("https://api.atoa.me/api/customers/{customerId}/cards/{cardId}")
.header("Authorization", "Bearer ")
.asString();
```
```json 200 theme={null}
{
"id": "card_abc123def456",
"name": "John Doe",
"type": "credit",
"lastFourDigits": "4242",
"category": "consumer",
"cardType": "DEBIT",
"brand": "VISA",
"country": "GB",
"expiryDate": "12/2027",
"metadata": { "internalId": "42" },
"createdAt": "2025-06-15T10:30:00.000Z"
}
```
```json 404 theme={null}
{
"name": "NOT_FOUND",
"message": "Card not found",
"status": 404,
"errors": []
}
```
# List Payment Methods
Source: https://docs.paywithatoa.co.uk/api-reference/payment-methods/list-payment-methods
GET https://api.atoa.me/api/customers/{customerId}/cards
List payment methods via the Atoa Payment Methods API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
List all saved payment methods for a customer. Use the `id` field as `paymentMethodId` when charging a saved card.
### Authorization
Bearer ``
**Path Parameters**
The customer UUID whose cards to retrieve.
**Query Parameters**
Filter to cards whose metadata contains an exact key/value match. Use bracket syntax: `?metadata[internalId]=42`.
**Response**
Returns an array of saved card objects. Returns an empty array `[]` if no cards are saved.
Card identifier. **Use this as `paymentMethodId` when processing payments.**
Cardholder name.
Card type (e.g., `credit`, `debit`).
Last 4 digits of the card number.
Card category (e.g., `consumer`, `commercial`).
Card type (e.g., `DEBIT`, `CREDIT`, `PREPAID`).
Card network brand (e.g., `VISA`, `MASTERCARD`).
Card issuing country code (e.g., `GB`).
Card expiry date in `MM/YYYY` format.
The key/value string pairs attached to the card when it was saved.
ISO 8601 timestamp of when the card was saved.
```bash curl theme={null}
curl --request GET \
--url https://api.atoa.me/api/customers/{customerId}/cards \
--header 'Authorization: Bearer '
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/customers/{customerId}/cards"
headers = {"Authorization": "Bearer "}
response = requests.get(url, headers=headers)
print(response.json())
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.atoa.me/api/customers/{customerId}/cards",
{
headers: { Authorization: "Bearer " },
}
);
const data = await response.json();
console.log(data);
```
```php PHP theme={null}
"https://api.atoa.me/api/customers/{customerId}/cards",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer "
],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
```
```go Go theme={null}
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.atoa.me/api/customers/{customerId}/cards"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer ")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.get("https://api.atoa.me/api/customers/{customerId}/cards")
.header("Authorization", "Bearer ")
.asString();
```
```json 200 theme={null}
[
{
"id": "card_abc123def456",
"name": "John Doe",
"type": "credit",
"lastFourDigits": "4242",
"category": "consumer",
"cardType": "DEBIT",
"brand": "VISA",
"country": "GB",
"expiryDate": "12/2027",
"metadata": { "internalId": "42" },
"createdAt": "2025-06-15T10:30:00.000Z"
}
]
```
```json 404 theme={null}
{
"name": "NOT_FOUND",
"message": "Customer not found",
"status": 404,
"errors": []
}
```
# Update Payment Method
Source: https://docs.paywithatoa.co.uk/api-reference/payment-methods/update-payment-method
PUT https://api.atoa.me/api/customers/{customerId}/cards/{cardId}
Update payment method via the Atoa Payment Methods API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Update a saved payment method. Today this updates the card's metadata β supply only the metadata you want to change.
### Authorization
Bearer ``
**Path Parameters**
The customer UUID who owns the card.
The card identifier to update.
**Request Body Schema**
Set of key-value pairs to attach to the saved card. Supplied keys are merged into the card's existing metadata: a key is added or overwritten, and setting a key to an empty string (`""`) removes it. Keys you don't include are left unchanged.
* Up to **50** key-value pairs.
* Each key must be **1β40** characters and must not contain `[` or `]`.
* Each value must be a **string** of at most **500** characters.
**Response**
Returns the updated saved card object.
Card identifier.
Cardholder name.
Card type (e.g., `credit`, `debit`).
Last 4 digits of the card number.
Card category.
Card type (e.g., `DEBIT`, `CREDIT`, `PREPAID`).
Card network brand (e.g., `VISA`, `MASTERCARD`).
Card issuing country code.
Card expiry date in `MM/YYYY` format.
The key/value string pairs attached to the card after the update.
ISO 8601 timestamp of when the card was saved.
```bash curl theme={null}
curl --request PUT \
--url https://api.atoa.me/api/customers/{customerId}/cards/{cardId} \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"metadata": { "internalId": "42", "legacyRef": "" }
}'
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/customers/{customerId}/cards/{cardId}"
payload = {
"metadata": {"internalId": "42", "legacyRef": ""}
}
headers = {
"Authorization": "Bearer ",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.json())
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.atoa.me/api/customers/{customerId}/cards/{cardId}",
{
method: "PUT",
headers: {
Authorization: "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
metadata: { internalId: "42", legacyRef: "" },
}),
}
);
const data = await response.json();
console.log(data);
```
```php PHP theme={null}
"https://api.atoa.me/api/customers/{customerId}/cards/{cardId}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
"metadata" => ["internalId" => "42", "legacyRef" => ""]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer ",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
```
```go Go theme={null}
package main
import (
"fmt"
"strings"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.atoa.me/api/customers/{customerId}/cards/{cardId}"
payload := strings.NewReader(`{
"metadata": { "internalId": "42", "legacyRef": "" }
}`)
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("Authorization", "Bearer ")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.put("https://api.atoa.me/api/customers/{customerId}/cards/{cardId}")
.header("Authorization", "Bearer ")
.header("Content-Type", "application/json")
.body("{\"metadata\":{\"internalId\":\"42\",\"legacyRef\":\"\"}}")
.asString();
```
```json 200 theme={null}
{
"id": "card_abc123def456",
"name": "John Doe",
"type": "credit",
"lastFourDigits": "4242",
"category": "consumer",
"cardType": "DEBIT",
"brand": "VISA",
"country": "GB",
"expiryDate": "12/2027",
"metadata": { "internalId": "42" },
"createdAt": "2025-06-15T10:30:00.000Z"
}
```
```json 400 theme={null}
{
"name": "BAD_REQUEST",
"message": "metadata keys must be 40 characters or less.",
"status": 400,
"errors": []
}
```
```json 404 theme={null}
{
"name": "NOT_FOUND",
"message": "Card not found",
"status": 404,
"errors": []
}
```
# Cancel Payment | Payments API
Source: https://docs.paywithatoa.co.uk/api-reference/payment/cancel-payment
Post https://api.atoa.me/api/payment-request/cancel/{paymentRequestId}
Cancel payment via the Atoa Payments API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
The Cancel Payment API facilitates the quick cancellation of a payment request. Simply pass the paymentRequestId obtained during payment generation.
The payment can be cancelled if the status of the payment request is in PAYMENT\_NOT\_INITIATED or AWAITING\_AUTHORIZATION.
### Authorization
Bearer ``
**Request Body Schema**
The paymentRequestId of the initiated payment which youβll receive from
process-payment API
```json 200 theme={null}
{
"message": "Your payment has been cancelled successfully."
}
```
```json 400 theme={null}
{
"name": "BAD_REQUEST",
"message": "Your payment has already been processed and can't be cancelled. Please contact us if you need any help.",
"status": 400,
"errors": "[]"
}
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": "[]"
}
```
**Response**
Your payment has been cancelled successfully.
# Get Payment Status | Payments API
Source: https://docs.paywithatoa.co.uk/api-reference/payment/get-payment-status
Get https://api.atoa.me/api/payments/v1/payment-status/{id}
Retrieve payment status via the Atoa Payments API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
If the status is PENDING, you should Poll for success at regular intervals. It should take 2-3 minutes at maximum for payment to be completed (dependent on the bank used).For a more effective approach, consider implementing an [exponential back-off strategy](https://en.wikipedia.org/wiki/Exponential_backoff).
So this API can be used to check the status of an individual payment transaction through polling, in case your integration hasn't been setup for webhook usage.
NOTE: If the customer hasn't initiated any payment, calling this API will
result in a '200 OK' with message: Payment not initiated.
**Card Payments:** For card transactions, this endpoint returns additional `cardPaymentDetails` in each transaction detail. Within `cardPaymentDetails`, `metadata.isMitPayment: true` indicates a merchant-initiated [card-on-file](/api-reference/card-on-file/introduction) payment. The overall payment `status` may be `AUTHORIZED` (for MANUAL\_CAPTURE payments not yet captured) or `CANCELLED` (for cancelled authorizations), in addition to the standard statuses β these values only appear for card-on-file payments.
**Request Body Schema**
The PaymentRequestId is required for this step, which you'll receive it from
the process-payment API when generating a payment request.
pass env=sandbox in case of sandbox env.
**Response**
Unique identifier assigned to the user who initiated the payment
Unique identifier for this specific payment request
Unique identifier for the customer making the payment
Unique identifier for the merchant
Business name of the merchant
Unique Identifier for the customer who made the payment
URL that will be called after payment completion
URL of the merchant's store profile image
Cryptographic hash used to verify payment authenticity
Additional parameters passed in the redirect URL after payment
Current status of the payment: `COMPLETED`, `PENDING`, `FAILED`, `AUTHORIZED`, or `CANCELLED`. The `AUTHORIZED` and `CANCELLED` statuses only apply to [card-on-file](/api-reference/card-on-file/introduction) payments with `MANUAL_CAPTURE` or `CAPTURE_BEFORE_EXPIRY` capture type.
Total amount paid by the customer
Currency code for the transaction (e.g. GBP)
Amount of tax charged on the transaction
Service fees charged on the transaction
Tip amount added by the customer
Custom note provided during [Payment Request Creation](./process-payment).
Unique identifier for the merchant's store location
Physical address of the store
Display name of the store location
Unique reference ID provided by ATOA eg. ATOA1692417435050
Unique identifier for the customer
Name of the customer as registered with their bank
Identifier for the customer's banking institution
Internal tracking ID for the transaction
Current processing status of the payment
Timestamp of the last status update
Standardized ISO payment status code
Human-readable description of the ISO status code
Amount paid in this specific transaction
Currency code for this transaction
Name of the customer's bank
Masked bank account number of the customer
Timestamp when the transaction was initiated
Timestamp of the last transaction update
Total amount refunded for this transaction
Tax amount for this specific transaction
Service fees for this specific transaction
Tip amount for this specific transaction
Additional notes or comments on the transaction
Only present when metadata was supplied on the payment. See [Process Payment](/api-reference/payment/process-payment) for the field limits.
Detailed description of any errors that occurred
Details about the device used for payment
Indicates if this was part of a split bill payment
Unique identifier for this refund transaction
Amount refunded in this transaction
Current status of the refund
Timestamp when the refund was initiated
Timestamp of the last refund status update
Reason or notes provided for the refund
Indicates whether refund is FULL or PARTIAL
Currency code for the refund amount
Unique UUID assigned to this refund
Card-specific details. **Only present for card transactions.** The `metadata.isMitPayment` field indicates whether this was a merchant-initiated [card-on-file](/api-reference/card-on-file/introduction) payment.
Card brand (e.g., `VISA`, `MASTERCARD`)
Payment sub-type: `CARD`, `GOOGLE_PAY`, or `APPLE_PAY`
Masked card display text (e.g., "Visa \*\*\*\* 4242")
`AUTO_CAPTURE`, `MANUAL_CAPTURE`, or `CAPTURE_BEFORE_EXPIRY`
Card payment metadata. `isMitPayment: true` indicates this is a card-on-file (merchant-initiated) payment. Also includes authorization/capture timestamps.
The saved card/payment method ID used for this transaction
```json 200 theme={null}
{
"userUuid": "65abed89-7e7f-44e1-8855-0ca696f9a75b",
"paymentRequestId": "75abed89-7e7f-44e1-8855-0ca696f9a75b",
"customerId": "string",
"merchantId": "eed6202f-4dfd-42fc-9bc6-48323556b59d ",
"merchantName": "John",
"applicationUserId": "99abed89-7e7f-44e1-8855-0ca696f9a75b",
"callback": "string",
"avatar": "string",
"orderId": "e21d31cc3",
"signatureHash": "string",
"redirectUrlParams": {},
"redirectUrl": "https://paywithatoa.co.uk",
"status": "COMPLETED",
"paidAmount": "100",
"currency": "GBP",
"taxAmount": "2",
"serviceAmount": "16",
"tipAmount": "5",
"storeDetails": {
"id": "d77e02d5-4e93-46cf-a8be-50da650df562",
"address": "London",
"locationName": "Location 1"
},
"transactionDetails": [
{
"paymentIdempotencyId": "ATOA1692417435050",
"consumerId": "eed6202f-4dfd-42fc-9bc6-48323556b59d ",
"consumerName": "Doe",
"institutionId": "modelo-sandbox",
"tracingId": "e21d31cc354d44559d3c7b92ca07060a",
"statusDetails": {
"status": "COMPLETED",
"statusUpdateDate": "2023-08-19T03:31:49.073Z",
"isoStatus": {
"code": "ACSC",
"name": "AcceptedSettlementCompleted"
}
},
"paidAmount": "100",
"totalRefundAmount": "4",
"currency": "GBP",
"bankName": "Barclays",
"bankAccountNo": "756",
"createdAt": "2023-08-19T03:57:20.871Z",
"updatedAt": "2023-08-19T03:57:20.871Z",
"taxAmount": "2",
"serviceAmount": "16",
"tipAmount": "5",
"notes": "string",
"metadata": {
"bookingId": "BK-42427",
"orderReference": "ORD-1"
},
"errorDescription": "Insufficient funds",
"paymentDevice": {},
"splitBill": false,
"refundDetails": [
{
"id": "ATOA1745651316374",
"amount": 4,
"currency": "GBP",
"status": "COMPLETED",
"createdAt": "2025-04-26T01:38:36.460Z",
"updatedAt": "2025-04-26T01:38:40.337Z",
"refundNotes": "notes",
"refundType": "PARTIAL",
"refundId": "123e4567-e89b-12d3-a456-426614174000"
}
]
}
]
}
```
```json 200 (Card Payment) theme={null}
{
"paymentRequestId": "9baa68d8-362a-4127-994d-2ea622ef35ee",
"customerId": "550e8400-e29b-41d4-a716-446655440000",
"merchantId": "eed6202f-4dfd-42fc-9bc6-48323556b59d",
"merchantName": "My Shop",
"applicationUserId": "99abed89-7e7f-44e1-8855-0ca696f9a75b",
"callback": "string",
"avatar": "string",
"orderId": "ORDER-001",
"signatureHash": "string",
"redirectUrlParams": {},
"status": "AUTHORIZED",
"paidAmount": 10.50,
"currency": "GBP",
"taxAmount": 0,
"serviceAmount": 0,
"tipAmount": 0,
"storeDetails": {
"id": "d77e02d5-4e93-46cf-a8be-50da650df562",
"address": "London",
"locationName": "London Store"
},
"transactionDetails": [
{
"paymentIdempotencyId": "ATOA1692417435050",
"consumerId": "consumer_123",
"consumerName": "John Doe",
"tracingId": "e21d31cc354d44559d3c7b92ca07060a",
"statusDetails": {
"status": "AUTHORIZED",
"statusUpdateDate": "2025-06-15T10:30:05.000Z",
"isoStatus": {
"code": "PDNG",
"name": "Pending"
}
},
"paidAmount": 10.50,
"totalRefundAmount": 0,
"currency": "GBP",
"createdAt": "2025-06-15T10:30:00.000Z",
"updatedAt": "2025-06-15T10:30:05.000Z",
"notes": "Payment for order #001",
"metadata": {
"bookingId": "BK-42427",
"orderReference": "ORD-1"
},
"splitBill": false,
"refundDetails": [],
"cardPaymentDetails": {
"cardType": "VISA",
"transactionSubType": "CARD",
"displayText": "Visa **** 4242",
"captureType": "MANUAL_CAPTURE",
"metadata": {
"authorized": true,
"authorizedAt": "2025-06-15T10:30:05.000Z",
"captured": false,
"isMitPayment": true
},
"paymentMethodId": "card_abc123def456"
}
}
]
}
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": "[]"
}
```
# Get stores | Payments API
Source: https://docs.paywithatoa.co.uk/api-reference/payment/get-stores
Get https://api.atoa.me/api/payments/stores
Retrieve stores via the Atoa Payments API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
If your business has more than one store location and you want to manage staff and payments separately for each store, make sure to pass the correct store ID in the payment request.
When you sign up with Atoa, a **default (primary) store** is automatically
created for your business. You can create or disable additional store
locations from the Atoa Dashboard or Mobile app
If the `storeId` is **missing**, **incorrect**, or refers to a **disabled** store the
payment request will be routed to your default (primary) store.
### Authorization
Bearer ``
**Response**
Current page number
Number of objects per page.
Array of objects
A unique Store id created for every store.
Location Name.
address Line 1.
address Line 2.
Address postal code.
Store is primary or not.
Store is enabled or not.
Merchant Store images that are visible to customers during the checkout process.
Total number of stores
```json 200 theme={null}
{
"page": 0,
"size": 20,
"data": [
{
"id": "549000a4-eee4-4b7d-bc88-c529a92544cd",
"locationName": "cambridge",
"addressLine1": "UNIT 3 BOLNEY GRANGE INDUSTRIAL PK ",
"addressLine2": "49 THE MARTLETS",
"addressPostalCode": "PO16 7GZ",
"cityOrTown": "England",
"primary": "true",
"enabled": "true",
"images": "[image-url]"
}
],
"totalCount": 1
}
```
```json 400 theme={null}
{
"name": "BAD_REQUEST",
"message": "merchant not found",
"status": 400,
"errors": "[]"
}
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": "[]"
}
```
```json 404 theme={null}
{
"name": "RESOURCE NOT FOUND",
"message": "Resource not found",
"status": 404,
"errors": "[]"
}
```
# Get Transactions | Payments API
Source: https://docs.paywithatoa.co.uk/api-reference/payment/get-transactions
POST https://api.atoa.me/api/payments/transactions
Retrieve transactions via the Atoa Payments API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Returns a paginated list of transactions for the authenticated merchant. Use filters to narrow down results by date, status, payment method, customer, or store.
### Authorization
Bearer ``
**Query Parameters**
Page number (0-indexed).
Number of items per page.
**Request Body Schema**
All fields are optional. Omit the field to skip filtering.
Array filters must have at least 1 item when provided. Passing an empty array `[]` will return a validation error.
Free-text search by consumer name, order ID, payment request ID, or amount.
Start date in `YYYY-MM-DD` format. Example: `2025-01-01`.
End date in `YYYY-MM-DD` format. Example: `2025-12-31`.
Filter by payment status.
* `COMPLETED` β Payment successfully captured
* `FAILED` β Payment failed
* `PENDING` β Payment is being processed
* `REFUNDED` β Payment fully refunded
* `PARTIALLY_REFUNDED` β Payment partially refunded
* `AUTHORIZED` β Payment authorized, not yet captured (MANUAL\_CAPTURE)
* `CANCELLED` β Payment was cancelled
Filter by Atoa customer UUIDs (from [Create Customer](/api-reference/customers/create-customer)). Each must be a valid UUID.
Filter by payment method.
* `PAY_BY_BANK` β Open banking payments
* `CARD` β Card payments
Filter by store UUIDs. Each must be a valid UUID.
**Response**
Unique transaction identifier (e.g., `ATOA1692417435050`).
Payment request UUID.
Customer UUID.
Merchant order reference.
Customer name.
Amount paid.
Currency code (e.g., `GBP`).
Transaction status (COMPLETED, FAILED, PENDING, REFUNDED, PARTIALLY\_REFUNDED, AUTHORIZED, CANCELLED).
`CARD` for card payments, `OPEN_BANKING` for Pay by Bank.
Payment notes.
Only present when metadata was supplied on the payment. See [Process Payment](/api-reference/payment/process-payment) for the field limits.
Store UUID.
Store address.
Store name.
Error description for failed payments.
Card-specific details. Only present for card transactions (`transactionType: "CARD"`). The `metadata.isMitPayment` field indicates whether this was a merchant-initiated [card-on-file](/api-reference/card-on-file/introduction) payment.
Card brand (e.g., `VISA`, `MASTERCARD`).
`CARD`, `GOOGLE_PAY`, or `APPLE_PAY`.
`AUTO_CAPTURE`, `MANUAL_CAPTURE`, or `CAPTURE_BEFORE_EXPIRY`.
Card payment metadata. `isMitPayment: true` indicates a merchant-initiated [card-on-file](/api-reference/card-on-file/introduction) payment.
The card/payment method ID used.
ISO 8601 creation timestamp.
ISO 8601 last update timestamp.
Total number of transactions matching the filters.
Current page number.
Items per page.
```bash curl theme={null}
curl --request POST \
--url 'https://api.atoa.me/api/payments/transactions?page=0&size=20' \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"fromDate": "2025-01-01",
"toDate": "2025-12-31",
"status": ["COMPLETED", "AUTHORIZED"],
"paymentMethod": ["CARD"]
}'
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/payments/transactions"
params = {"page": 0, "size": 20}
payload = {
"fromDate": "2025-01-01",
"toDate": "2025-12-31",
"status": ["COMPLETED", "AUTHORIZED"],
"paymentMethod": ["CARD"]
}
headers = {
"Authorization": "Bearer ",
"Content-Type": "application/json"
}
response = requests.post(url, params=params, json=payload, headers=headers)
print(response.json())
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.atoa.me/api/payments/transactions?page=0&size=20",
{
method: "POST",
headers: {
Authorization: "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
fromDate: "2025-01-01",
toDate: "2025-12-31",
status: ["COMPLETED", "AUTHORIZED"],
paymentMethod: ["CARD"],
}),
}
);
const data = await response.json();
console.log(data);
```
```php PHP theme={null}
"https://api.atoa.me/api/payments/transactions?page=0&size=20",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
"fromDate" => "2025-01-01",
"toDate" => "2025-12-31",
"status" => ["COMPLETED", "AUTHORIZED"],
"paymentMethod" => ["CARD"]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer ",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
```
```go Go theme={null}
package main
import (
"fmt"
"strings"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.atoa.me/api/payments/transactions?page=0&size=20"
payload := strings.NewReader(`{
"fromDate": "2025-01-01",
"toDate": "2025-12-31",
"status": ["COMPLETED", "AUTHORIZED"],
"paymentMethod": ["CARD"]
}`)
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer ")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.post("https://api.atoa.me/api/payments/transactions?page=0&size=20")
.header("Authorization", "Bearer ")
.header("Content-Type", "application/json")
.body("{\"fromDate\":\"2025-01-01\",\"toDate\":\"2025-12-31\",\"status\":[\"COMPLETED\",\"AUTHORIZED\"],\"paymentMethod\":[\"CARD\"]}")
.asString();
```
```json 200 theme={null}
{
"data": [
{
"paymentIdempotencyId": "ATOA1692417435050",
"paymentRequestId": "9baa68d8-362a-4127-994d-2ea622ef35ee",
"customerId": "550e8400-e29b-41d4-a716-446655440000",
"consumerId": "consumer_123",
"merchantName": "My Shop",
"merchantId": "eed6202f-4dfd-42fc-9bc6-48323556b59d",
"orderId": "ORDER-001",
"consumerName": "John Doe",
"paidAmount": 10.50,
"currency": "GBP",
"status": "COMPLETED",
"transactionType": "CARD",
"notes": "Payment for order #001",
"metadata": {
"bookingId": "BK-42427",
"orderReference": "ORD-1"
},
"storeDetails": {
"id": "d77e02d5-4e93-46cf-a8be-50da650df562",
"address": "London",
"locationName": "London Store"
},
"cardPaymentDetails": {
"cardType": "VISA",
"transactionSubType": "CARD",
"captureType": "AUTO_CAPTURE",
"metadata": {
"isMitPayment": true
},
"paymentMethodId": "card_abc123def456"
},
"createdAt": "2025-06-15T10:30:00.000Z",
"updatedAt": "2025-06-15T10:31:00.000Z"
}
],
"totalCount": 100,
"page": 0,
"size": 20
}
```
```json 400 theme={null}
{
"name": "BAD_REQUEST",
"message": "From date must be in YYYY-MM-DD format.",
"status": 400,
"errors": []
}
```
# Process Payment: Create a Payment Request
Source: https://docs.paywithatoa.co.uk/api-reference/payment/process-payment
Post https://api.atoa.me/api/payments/process-payment
Create an Atoa payment request and get back a payment link, QR code and paymentRequestId to collect a Pay by Bank or card payment.
To initiate a new payment request that can be completed by the customer, you will need to obtain your API key from the Atoa Business App. This endpoint should be called on the server-side to prevent exposing your API key.
### Authorization
Bearer ``
**Request Body Schema**
Unique Customer Id.
An Identifier you can use for transaction reference and signature verification.
The total cart amount for the payment.
If you want to create a payment under a particular store, else the transaction
would be created in the default store. Refer [Get-Stores API](./getstores).
Controls the set of payment methods shown to the consumer during checkout. If not specified, the checkout defaults to offering all payment methods enabled on the merchant account.
* `PAY_BY_BANK` β When provided, only Pay by Bank will be shown to the customer.
* `CARD` β When provided, only card payment will be shown to the customer. If card is not enabled in your account please contact support.
* If both `PAY_BY_BANK` and `CARD` are passed, both options are displayed, allowing the customer to choose their preferred payment method during checkout.
Save the customer's card during checkout for future payments. When enabled:
* Card is saved after successful 3DS authentication
* **Requires `atoaCustomerId`** (from [Create Customer](/api-reference/customers/create-customer))
* Only supported for card payments (`paymentMethod` must include `CARD`)
* Cannot be used with `splitBill`
* Card payments must be enabled on your account
See [Card on File guide](/api-reference/card-on-file/introduction) for the full integration flow.
Customer ID from [Create Customer](/api-reference/customers/create-customer). Links this payment to a customer.
**Benefits:**
* **Save Cards**: Required when `savePaymentMethod=true`
* **Faster Checkout**: Returning customers see their saved cards automatically
* **Tracking**: Links all payments to the customer for reporting
Your own key-value pairs to attach to the card saved during this payment. Only applies when `savePaymentMethod` is `true`.
* Up to **50** key-value pairs.
* Each key must be **1β40** characters and must not contain `[` or `]`.
* Each value must be a **string** of at most **500** characters.
Returned against the saved card in [List Payment Methods](/api-reference/payment-methods/list-payment-methods).
Auto-redirection is disabled by default (false). Enabling it (true) automatically redirects users to Atoa's payment webpage for transaction completion.
Consumer phone country code
Consumer phone number
Consumer email
Consumer first name
Consumer last name
The completion of the payment process, the specified data is appended to the redirected URL. For example, if your redirected URL is `https://paywithatoa.co.uk/` and your callback data is `{couponCode:245561,refId:2342},` the final redirected URL would be `https://paywithatoa.co.uk/?couponCode=245561&refId=2342`.
**Expressed in milliseconds:** This specifies the time after which an individual payment request will expire. If no expiresIn time is provided when creating a payment request, it will default to expire exactly 3 minutes later.
Note:- All URLs received in the API response will never expire. It's the individual payment request that will expire if the user does not complete the payment within the specified time. Refer to the webhook guide for more details about the [expiry payload](../../introduction#payment-expiration-payload).
Adds a custom note to the payment for tracking and reconciliation. The note is always visible in the Atoa dashboard and can be used for internal reference.
Note:- The note must be 30 characters or less.
Set of key-value pairs that you can attach to an object. This can be useful for storing additional information about the object in a structured format.
* Up to **50** key-value pairs.
* Each key must be **1β40** characters and must not contain `[` or `]`.
* Each value must be a **string** of at most **500** characters.
Once set, metadata is returned in the [Get Transactions](/api-reference/payment/get-transactions), [Get Payout Transactions](/api-reference/payouts/get-payout-transactions), and [Get Payment Status](/api-reference/payment/get-payment-status) responses, and is included in the [Payment Status Webhook](/api-reference/webhook/process-payment-webhook-response).
Metadata is frozen when the payment is created and cannot be changed afterwards. An empty object (`{}`) is treated as no metadata.
Deciding whether to include tip collection on payments. The option to enter tips will only be displayed if tips are enabled in the Atoa Business app as well. In the event that tips are enabled in the app but 'enableTips' is passed as false, it will override and prevent the tips input from being shown to the user.
To enable consumers to retry their payment if the transaction fails or stalls, set this parameter to true. If not, set it to false. The default value is true.
You have the option to include a redirect URL. Once the payment is successfully completed, the customer will be automatically redirected to the specified URL.
By default, split bill is set to false. The 'splitBill' parameter empowers your customers with the ability to seamlessly divide the total bill among their friends during the payment process. This feature proves particularly valuable in scenarios like dining at restaurants, facilitating a hassle-free equal distribution of expenses within a group of friends.
To elevate your customer's payment experience through ATOA QR codes on both the Till display and customer receipts, utilize the following parameter.
Displaying QR Codes on Kiosk or Till:
Generate an ATOA QR template image for external displays by including the 'template' parameter with values 'EXTERNAL\_DISPLAY' (SVG format) or 'EXTERNAL\_DISPLAY\_PNG' (PNG format). This feature enables customers to easily scan the QR code from the kiosk or till, ensuring a swift and seamless payment process.
Printing QR Codes on Customer Receipts:
Effortlessly integrate an ATOA QR image into your customer receipts by using the 'template' parameter with values 'RECEIPT' (SVG format) or 'RECEIPT\_PNG' (PNG format). Print this on the customer receipt from the terminal printer, allowing customers to scan the QR code and complete the payment process.
If a redirection URL is initially set during access token creation but a
different URL is provided in the process-payment API, the URL from the
process-payment API will be prioritized.
**Response**
URL to retrieve the QR code for payment initiation. Suitable for scanning with
any camera app.
URL for the QR code in PNG format, offering a background-free image for
versatile use.
URL to fetch display or receipt images based on the specified template. Ideal
for QR displays on kiosks or printing on receipts.
Direct link for payment, encoded in the QR code. It bridges the QR scan to the
payment gateway.
A unique identifier for the payment request, crucial for tracking the payment
from start to finish.
```bash cURL theme={null}
curl --request POST \
--url https://api.atoa.me/api/payments/process-payment \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"customerId": "549000a4-eee4-4b7d-bc88-c529a92544cd",
"orderId": "6eb7e0f8-972a-4bf6-99d2-864bdccd5a34",
"amount": 123,
"storeId": "43c7b991-0803-4177-9589-d165d9a779f6",
"redirectUrl": "https://paywithatoa.co.uk"
}'
```
```bash Save Card During Checkout (cURL) theme={null}
# Requires atoaCustomerId from Create Customer API
curl --request POST \
--url https://api.atoa.me/api/payments/process-payment \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"customerId": "549000a4-eee4-4b7d-bc88-c529a92544cd",
"orderId": "6eb7e0f8-972a-4bf6-99d2-864bdccd5a34",
"amount": 123,
"storeId": "43c7b991-0803-4177-9589-d165d9a779f6",
"paymentMethod": ["CARD"],
"atoaCustomerId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"savePaymentMethod": true,
"paymentMethodMetadata": { "internalId": "42" },
"redirectUrl": "https://paywithatoa.co.uk"
}'
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/payments/process-payment"
payload = {
"customerId": "549000a4-eee4-4b7d-bc88-c529a92544cd",
"orderId": "6eb7e0f8-972a-4bf6-99d2-864bdccd5a34",
"amount": 123,
"storeId": "43c7b991-0803-4177-9589-d165d9a779f6",
"redirectUrl": "https://paywithatoa.co.uk"
}
headers = {
"Authorization": "Bearer ",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)
```
```python Save Card During Checkout (Python) theme={null}
# Requires atoaCustomerId from Create Customer API
import requests
url = "https://api.atoa.me/api/payments/process-payment"
payload = {
"customerId": "549000a4-eee4-4b7d-bc88-c529a92544cd",
"orderId": "6eb7e0f8-972a-4bf6-99d2-864bdccd5a34",
"amount": 123,
"storeId": "43c7b991-0803-4177-9589-d165d9a779f6",
"paymentMethod": ["CARD"],
"atoaCustomerId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"savePaymentMethod": True,
"paymentMethodMetadata": {"internalId": "42"},
"redirectUrl": "https://paywithatoa.co.uk"
}
headers = {
"Authorization": "Bearer ",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)
```
```javascript JavaScript theme={null}
const options = {
method: "POST",
headers: {
Authorization: "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
customerId: "549000a4-eee4-4b7d-bc88-c529a92544cd",
orderId: "6eb7e0f8-972a-4bf6-99d2-864bdccd5a34",
amount: 123,
storeId: "43c7b991-0803-4177-9589-d165d9a779f6",
redirectUrl: "https://paywithatoa.co.uk"
})
};
fetch("https://api.atoa.me/api/payments/process-payment", options)
.then((response) => response.json())
.then((response) => console.log(response))
.catch((err) => console.error(err));
```
```javascript Save Card During Checkout (JavaScript) theme={null}
// Requires atoaCustomerId from Create Customer API
const options = {
method: "POST",
headers: {
Authorization: "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
customerId: "549000a4-eee4-4b7d-bc88-c529a92544cd",
orderId: "6eb7e0f8-972a-4bf6-99d2-864bdccd5a34",
amount: 123,
storeId: "43c7b991-0803-4177-9589-d165d9a779f6",
paymentMethod: ["CARD"],
atoaCustomerId: "a1b2c3d4-5678-90ab-cdef-1234567890ab",
savePaymentMethod: true,
paymentMethodMetadata: { internalId: "42" },
redirectUrl: "https://paywithatoa.co.uk"
})
};
fetch("https://api.atoa.me/api/payments/process-payment", options)
.then((response) => response.json())
.then((response) => console.log(response))
.catch((err) => console.error(err));
```
```php PHP theme={null}
"https://api.atoa.me/api/payments/process-payment",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
"customerId" => "549000a4-eee4-4b7d-bc88-c529a92544cd",
"orderId" => "6eb7e0f8-972a-4bf6-99d2-864bdccd5a34",
"amount" => 123,
"storeId" => "43c7b991-0803-4177-9589-d165d9a779f6",
"redirectUrl" => "https://paywithatoa.co.uk"
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer ",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
```
```php Save Card During Checkout (PHP) theme={null}
"https://api.atoa.me/api/payments/process-payment",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
"customerId" => "549000a4-eee4-4b7d-bc88-c529a92544cd",
"orderId" => "6eb7e0f8-972a-4bf6-99d2-864bdccd5a34",
"amount" => 123,
"storeId" => "43c7b991-0803-4177-9589-d165d9a779f6",
"paymentMethod" => ["CARD"],
"atoaCustomerId" => "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"savePaymentMethod" => true,
"paymentMethodMetadata" => ["internalId" => "42"],
"redirectUrl" => "https://paywithatoa.co.uk"
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer ",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
```
```go Go theme={null}
package main
import (
"fmt"
"strings"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.atoa.me/api/payments/process-payment"
payload := strings.NewReader(`{
"customerId": "549000a4-eee4-4b7d-bc88-c529a92544cd",
"orderId": "6eb7e0f8-972a-4bf6-99d2-864bdccd5a34",
"amount": 123,
"storeId": "43c7b991-0803-4177-9589-d165d9a779f6",
"redirectUrl": "https://paywithatoa.co.uk"
}`)
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer ")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```go Save Card During Checkout (Go) theme={null}
// Requires atoaCustomerId from Create Customer API
package main
import (
"fmt"
"strings"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.atoa.me/api/payments/process-payment"
payload := strings.NewReader(`{
"customerId": "549000a4-eee4-4b7d-bc88-c529a92544cd",
"orderId": "6eb7e0f8-972a-4bf6-99d2-864bdccd5a34",
"amount": 123,
"storeId": "43c7b991-0803-4177-9589-d165d9a779f6",
"paymentMethod": ["CARD"],
"atoaCustomerId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"savePaymentMethod": true,
"paymentMethodMetadata": { "internalId": "42" },
"redirectUrl": "https://paywithatoa.co.uk"
}`)
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer ")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.post("https://api.atoa.me/api/payments/process-payment")
.header("Authorization", "Bearer ")
.header("Content-Type", "application/json")
.body("{\n \"customerId\": \"549000a4-eee4-4b7d-bc88-c529a92544cd\",\n \"orderId\": \"6eb7e0f8-972a-4bf6-99d2-864bdccd5a34\",\n \"amount\": 123,\n \"storeId\": \"43c7b991-0803-4177-9589-d165d9a779f6\",\n \"redirectUrl\": \"https://paywithatoa.co.uk\"\n}")
.asString();
```
```java Save Card During Checkout (Java) theme={null}
// Requires atoaCustomerId from Create Customer API
HttpResponse response = Unirest.post("https://api.atoa.me/api/payments/process-payment")
.header("Authorization", "Bearer ")
.header("Content-Type", "application/json")
.body("{\n \"customerId\": \"549000a4-eee4-4b7d-bc88-c529a92544cd\",\n \"orderId\": \"6eb7e0f8-972a-4bf6-99d2-864bdccd5a34\",\n \"amount\": 123,\n \"storeId\": \"43c7b991-0803-4177-9589-d165d9a779f6\",\n \"paymentMethod\": [\"CARD\"],\n \"atoaCustomerId\": \"a1b2c3d4-5678-90ab-cdef-1234567890ab\",\n \"savePaymentMethod\": true,\n \"paymentMethodMetadata\": {\"internalId\": \"42\"},\n \"redirectUrl\": \"https://paywithatoa.co.uk\"\n}")
.asString();
```
```json 200 theme={null}
{
"qrCodeUrl": "https://atoa-merchant-dev.s3.eu-west-2.amazonaws.com/payment-qr-codes/eed6202f-4dfd-42fc-9bc6-48323556b59d/1692703721570.svg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIASV2VH7U5MIC62E5K%2F20230822%2Feu-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230822T112842Z&X-Amz-Expires=3600&X-Amz-Signature=a17f22e0291ba9f3706fd686b41a8eda0aa5335e1f5bcac90aa05aa2425a4a7f&X-Amz-SignedHeaders=host",
"qrCodeUrlPng": "https://atoa-merchant-dev.s3.eu-west-2.amazonaws.com/payment-qr-codes/eed6202f-4dfd-42fc-9bc6-48323556b59d/1692703721570.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIASV2VH7U5MIC62E5K%2F20230822%2Feu-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230822T112842Z&X-Amz-Expires=3600&X-Amz-Signature=a17f22e0291ba9f3706fd686b41a8eda0aa5335e1f5bcac90aa05aa2425a4a7f&X-Amz-SignedHeaders=host",
"templateUrl": "https://atoa-merchant-dev.s3.eu-west-2.amazonaws.com/payment-qr-codes/eed6202f-4dfd-42fc-9bc6-48323556b59d/1692703721570.svg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIASV2VH7U5MIC62E5K%2F20230822%2Feu-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230822T112842Z&X-Amz-Expires=3600&X-Amz-Signature=a17f22e0291ba9f3706fd686b41a8eda0aa5335e1f5bcac90aa05aa2425a4a7f&X-Amz-SignedHeaders=host",
"paymentUrl": "https://atoa.me/pay/?paymentRequestId=9baa68d8-362a-4127-994d-2ea622ef35ee&callbackParams=e30=&source=EXTERNAL_MERCHANT",
"paymentRequestId": "9baa68d8-362a-4127-994d-2ea622ef35ee"
}
```
```json 400 - Missing Required Field theme={null}
{
"name": "BAD_REQUEST",
"message": "customerId should not be undefined",
"status": 400,
"errors": "[]"
}
```
```json 400 - savePaymentMethod Without atoaCustomerId theme={null}
{
"name": "BAD_REQUEST",
"message": "Saving payment method requires atoaCustomerId.",
"status": 400,
"errors": "[]"
}
```
```json 400 - savePaymentMethod for Pay by Bank Only theme={null}
{
"name": "BAD_REQUEST",
"message": "Saving payment method is only supported for card payments",
"status": 400,
"errors": "[]"
}
```
```json 400 - Invalid atoaCustomerId theme={null}
{
"name": "BAD_REQUEST",
"message": "Invalid atoaCustomerId. Customer not found or does not belong to this business.",
"status": 400,
"errors": "[]"
}
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized"
"status": 400,
"errors": "[]"
}
```
```json 500 theme={null}
{
"name": "INTERNAL_SERVER_ERROR",
"message": "Cannot read property 'phoneCountryCode' of undefined",
"status": 500,
"errors": "[]"
}
```
# Get Payout Transactions
Source: https://docs.paywithatoa.co.uk/api-reference/payouts/get-payout-transactions
GET https://api.atoa.me/api/payouts/{payoutId}/transactions
Retrieve payout transactions via the Atoa Payouts API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
This API retrieves a paginated list of transactions associated with a specific payout ID.
### Authorization
Bearer ``
### Path Parameters
Unique identifier of the payout. This can be obtained from the [Payouts
API](/api-reference/payouts/get-payouts).
### Query Parameters
Maximum number of records to return per page. Default: 10
Page number for pagination, starting from 0. Default: 0
### Response
List of transaction records
Unique identifier for the payment transaction
Unique identifier for the payment request. Only present if the payment was processed through the [Process Payment API](/api-reference/payment/process-payment).
ISO 8601 timestamp of when the transaction was created
Transaction amount with decimal precision
Currency code (e.g., GBP)
Unique identifier for the refund transaction (null if not a refund). Only present if the transaction was processed through the [Refund API](/api-reference/refund/initiate-refund).
Reference to the original payment being refunded. Only present if the transaction is a refund (when isRefund=true).
Reference identifier for the associated payout
ISO 8601 timestamp of when the transaction was finalized
Indicates whether the transaction is a refund (true/false)
Optional additional parameters provided in callbacks. Only present if the payment was processed through the [Process Payment API](/api-reference/payment/process-payment).
Optional notes associated with the transaction. Only present if the payment was processed through the [Process Payment API](/api-reference/payment/process-payment).
Only present when metadata was supplied on the payment. See [Process Payment](/api-reference/Payment/process-payment) for the field limits.
Current page number
Number of items per page
Total number of records matching the query
```json 200 theme={null}
{
"data": [
{
"paymentIdempotencyId": "ATOA1745577029069",
"paymentRequestId": "04869b92-7462-4d89-adbb-c169e735dbaa", // Only present if the payment was processed through the Process-Payment.
"createdAt": "2025-04-25T10:30:36.655Z",
"amount": "123.00",
"currency": "GBP",
"refundId": null,
"refundPaymentReference": null,
"payoutReferenceId": "APY4zt9jjqozv7",
"transactionFinalizedAt": "2025-04-27T06:30:10.925Z",
"isRefund": false,
"callbackParams": {
"test": "67"
}, // Only present if the payment was processed through the Process-Payment.
"notes": "Customer payment for order #12345", // Only present if the payment was processed through the Process-Payment.
"metadata": { // Only present if metadata was supplied when creating the payment.
"bookingId": "BK-42427",
"orderReference": "ORD-1"
}
},
{
"paymentIdempotencyId": "ATOA1745734720994",
"createdAt": "2025-04-27T06:18:48.086Z",
"amount": "162.06",
"currency": "GBP",
"refundId": null,
"refundPaymentReference": null,
"payoutReferenceId": "APY4zt9jjqozv7",
"transactionFinalizedAt": "2025-04-27T06:30:10.925Z",
"isRefund": false
},
{
"paymentIdempotencyId": "ATOA1745577449258",
"createdAt": "2025-04-25T10:37:38.988Z",
"amount": "1000.00",
"currency": "GBP",
"refundId": null,
"refundPaymentReference": null,
"payoutReferenceId": "APY4zt9jjqozv7",
"transactionFinalizedAt": "2025-04-27T06:30:10.925Z",
"isRefund": false
},
{
"paymentIdempotencyId": "ATOA1745557721665",
"createdAt": "2025-04-25T10:35:08.683Z",
"amount": "45.76",
"currency": "GBP",
"refundId": "5e82db42-bc4a-4098-802d-a7be0c5d50ad",
"refundPaymentReference": "ATOA1745557721665",
"payoutReferenceId": "APY4zt9jjqozv7",
"transactionFinalizedAt": "2025-04-27T06:30:10.925Z",
"isRefund": true
}
],
"page": 0,
"itemPerPage": 4,
"totalCount": 4
}
```
```json 400 theme={null}
{
"name": "AJV_VALIDATION_ERROR",
"message": "Bad request on parameter \"request.path.id\".\nValue must match format \"uuid\". Given value: \"{payoutId}\"",
"status": 400,
"errors": [
{
"instancePath": "",
"schemaPath": "#/format",
"keyword": "format",
"params": {
"format": "uuid"
},
"message": "must match format \"uuid\"",
"data": "{payoutId}",
"dataPath": ""
}
]
}
```
```json 401 theme={null}
{
"name": "INVALID_CREDENTIAL",
"message": "Invalid token",
"status": 401,
"errors": []
}
```
```json 404 theme={null}
{
"name": "TRANSACTION_NOT_FOUND",
"message": "No transactions found for this payout",
"status": 404,
"errors": []
}
```
```json 500 theme={null}
{
"name": "INTERNAL_SERVER_ERROR",
"message": "Internal Server Error",
"status": 500,
"errors": []
}
```
# Get Payouts | Payouts API
Source: https://docs.paywithatoa.co.uk/api-reference/payouts/get-payouts
GET https://api.atoa.me/api/payouts
Retrieve payouts via the Atoa Payouts API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
This API retrieves a paginated list of payouts based on specified filters.
Payouts are only available for Pay by Bank (Open Banking) payments. This feature is not enabled by default and is only accessible to merchants who have been granted payout access. Contact support if not enabled.
### Authorization
Bearer ``
### Query Parameters
Maximum number of records to return per page. Default: 20
Page number for pagination, starting from 0. Default: 0
Start date for filtering (YYYY-MM-DD format)
End date for filtering (YYYY-MM-DD format)
Sort order for results. Values: ASC or DESC. Default: DESC
Filter by payout status. Possible values: PENDING, PROCESSING, SETTLED, FAILED
Search term to filter results (e.g., amount, reference)
### Response
List of payout records
Unique identifier for the payout
Payout amount with decimal precision
Currency code (e.g., GBP)
ISO 8601 timestamp indicating when the settlement period ended
ISO 8601 timestamp of when the payout was created
ISO 8601 timestamp of when the payout was last updated
Current status of the payout (e.g., SETTLED, FAILED, PENDING, PROCESSING)
Reference identifier for the payout
Current page number
Number of items per page
Total number of records matching the query
```json 200 theme={null}
{
"data": [
{
"id": "cd8cf3db-f163-4267-93b9-922e40bef23a",
"amount": "1118.71",
"currency": "GBP",
"settlementPeriodEnd": "2025-04-27T06:20:09.000Z",
"createdAt": "2025-04-27T06:20:15.473Z",
"updatedAt": "2025-04-27T06:25:51.799Z",
"status": "SETTLED",
"reference": "APY4zt9jjqozv7"
},
{
"id": "ded62780-62cf-425d-b301-9a7b28473176",
"amount": "1076.65",
"currency": "GBP",
"settlementPeriodEnd": "2025-04-25T10:40:06.000Z",
"createdAt": "2025-04-25T10:40:11.399Z",
"updatedAt": "2025-04-25T10:40:11.399Z",
"status": "FAILED",
"reference": "APY3nm161qvgy7"
},
{
"id": "f86aa640-49dc-4699-a5fc-a486cd52d231",
"amount": "55.76",
"currency": "GBP",
"settlementPeriodEnd": "2025-04-25T05:10:07.000Z",
"createdAt": "2025-04-25T05:10:10.644Z",
"updatedAt": "2025-04-25T05:12:16.370Z",
"status": "SETTLED",
"reference": "APY7bp6hjfd9vw"
},
{
"id": "dbfec6d0-d433-4f48-8dd7-2993859b737c",
"amount": "40.56",
"currency": "GBP",
"settlementPeriodEnd": "2025-04-16T09:20:10.000Z",
"createdAt": "2025-04-16T09:20:14.299Z",
"updatedAt": "2025-04-17T05:39:31.006Z",
"status": "SETTLED",
"reference": "APY5d2r5j8uee2"
},
{
"id": "1e7b248d-50b0-4fb0-b345-c22d59b0fbaf",
"amount": "32.79",
"currency": "GBP",
"settlementPeriodEnd": "2025-04-16T09:00:53.000Z",
"createdAt": "2025-04-16T09:00:58.975Z",
"updatedAt": "2025-04-16T09:00:58.975Z",
"status": "FAILED",
"reference": "APY676utmqf46u"
},
{
"id": "7e23411a-d520-4db1-bf01-90ea1bcf4e57",
"amount": "0.67",
"currency": "GBP",
"settlementPeriodEnd": "2025-04-15T09:50:08.000Z",
"createdAt": "2025-04-15T09:50:15.197Z",
"updatedAt": "2025-04-15T09:50:15.197Z",
"status": "FAILED",
"reference": "APY67yluk1abwt"
},
{
"id": "42c2b1d8-329f-4f94-89ab-97570c34e745",
"amount": "1.17",
"currency": "GBP",
"settlementPeriodEnd": "2025-04-15T09:30:48.000Z",
"createdAt": "2025-04-15T09:30:52.026Z",
"updatedAt": "2025-04-15T09:30:52.026Z",
"status": "FAILED",
"reference": "APY0jilrbatxpc"
},
{
"id": "b702eb39-eb17-463f-8afe-22cc7959e478",
"amount": "74.85",
"currency": "GBP",
"settlementPeriodEnd": "2025-04-11T09:30:09.000Z",
"createdAt": "2025-04-11T09:30:13.854Z",
"updatedAt": "2025-04-15T09:22:44.030Z",
"status": "SETTLED",
"reference": "APY0vhdx08ut4e"
},
{
"id": "50537985-c830-498d-92a7-86b95288c16f",
"amount": "1379.91",
"currency": "GBP",
"settlementPeriodEnd": "2025-04-11T06:40:08.000Z",
"createdAt": "2025-04-11T06:40:14.213Z",
"updatedAt": "2025-04-11T06:41:04.118Z",
"status": "SETTLED",
"reference": "APY96sqosuhpp8"
},
{
"id": "a133c891-e2c9-4cae-90e9-a29fbbf60037",
"amount": "1405.97",
"currency": "GBP",
"settlementPeriodEnd": "2025-04-11T06:20:50.000Z",
"createdAt": "2025-04-11T06:20:54.090Z",
"updatedAt": "2025-04-11T06:20:54.090Z",
"status": "FAILED",
"reference": "APY3k0wacpb0cm"
},
{
"id": "c8a67fba-21ed-4357-bf3c-2b27ea45e79e",
"amount": "1362.06",
"currency": "GBP",
"settlementPeriodEnd": "2025-04-11T06:10:39.000Z",
"createdAt": "2025-04-11T06:10:43.167Z",
"updatedAt": "2025-04-11T06:10:43.167Z",
"status": "FAILED",
"reference": "APY56uydvhiwwp"
},
{
"id": "66c51182-8d06-41b9-a162-3494f76d8cf7",
"amount": "1261.17",
"currency": "GBP",
"settlementPeriodEnd": "2025-04-11T06:00:55.000Z",
"createdAt": "2025-04-11T06:00:59.047Z",
"updatedAt": "2025-04-11T06:00:59.047Z",
"status": "FAILED",
"reference": "APY0d0q5ma8g77"
},
{
"id": "56bef81d-7b25-46e6-a9f3-2033834090bf",
"amount": "1237.02",
"currency": "GBP",
"settlementPeriodEnd": "2025-04-10T09:50:50.000Z",
"createdAt": "2025-04-10T09:50:54.253Z",
"updatedAt": "2025-04-10T09:50:54.253Z",
"status": "FAILED",
"reference": "APY71cg3oiwl42"
},
{
"id": "1f8c9e3a-545a-4312-9947-54b7928ec6cd",
"amount": "1206.08",
"currency": "GBP",
"settlementPeriodEnd": "2025-04-10T09:40:07.000Z",
"createdAt": "2025-04-10T09:40:13.759Z",
"updatedAt": "2025-04-10T09:40:13.759Z",
"status": "FAILED",
"reference": "APY8z2qh70x30j"
},
{
"id": "8854481f-6b64-483a-acc2-b5ecf7d6f145",
"amount": "808.39",
"currency": "GBP",
"settlementPeriodEnd": "2025-04-10T09:30:09.000Z",
"createdAt": "2025-04-10T09:30:16.380Z",
"updatedAt": "2025-04-10T09:30:16.380Z",
"status": "FAILED",
"reference": "APY2cn4f0324op"
},
{
"id": "cf1e597d-e263-4013-9e92-30dfdef30175",
"amount": "788.68",
"currency": "GBP",
"settlementPeriodEnd": "2025-04-10T09:20:07.000Z",
"createdAt": "2025-04-10T09:20:13.494Z",
"updatedAt": "2025-04-10T09:20:13.494Z",
"status": "FAILED",
"reference": "APY7p6jnc3bbug"
},
{
"id": "effd9462-875a-47fa-bb07-991e1db35eb2",
"amount": "782.55",
"currency": "GBP",
"settlementPeriodEnd": "2025-04-09T07:20:53.000Z",
"createdAt": "2025-04-09T07:20:57.898Z",
"updatedAt": "2025-04-09T07:20:57.898Z",
"status": "FAILED",
"reference": "APY5x8156x60pa"
},
{
"id": "8b0016a4-c633-4c49-b2bd-af9190807cb0",
"amount": "751.61",
"currency": "GBP",
"settlementPeriodEnd": "2025-04-09T06:30:09.000Z",
"createdAt": "2025-04-09T06:30:14.903Z",
"updatedAt": "2025-04-09T06:30:14.903Z",
"status": "FAILED",
"reference": "APY3dabhgqrlk2"
},
{
"id": "3376c8d4-0523-490a-bb2f-8da2ad260790",
"amount": "717.22",
"currency": "GBP",
"settlementPeriodEnd": "2025-04-08T12:30:10.000Z",
"createdAt": "2025-04-08T12:30:14.633Z",
"updatedAt": "2025-04-08T12:30:14.633Z",
"status": "FAILED",
"reference": "APY14gu3idi27q"
},
{
"id": "a9f87a7e-03bd-422c-b40f-fa6dace29abc",
"amount": "686.28",
"currency": "GBP",
"settlementPeriodEnd": "2025-04-08T11:20:46.000Z",
"createdAt": "2025-04-08T11:20:49.674Z",
"updatedAt": "2025-04-08T11:20:49.674Z",
"status": "FAILED",
"reference": "APY0ierz1cvw73"
}
],
"page": 0,
"itemPerPage": 20,
"totalCount": 94
}
```
```json 400 theme={null}
{
"name": "AJV_VALIDATION_ERROR",
"message": "Bad request on parameter \"request.query\".\nPayoutsQueryModel.page must be >= 0. Given value: -1",
"status": 400,
"errors": [
{
"instancePath": "/page",
"schemaPath": "#/properties/page/minimum",
"keyword": "minimum",
"params": {
"comparison": ">=",
"limit": 0
},
"message": "must be >= 0",
"data": -1,
"modelName": "PayoutsQueryModel",
"dataPath": ".page"
}
]
}
```
```json 401 theme={null}
{
"name": "INVALID_CREDENTIAL",
"message": "Invalid token",
"status": 401,
"errors": []
}
```
```json 500 theme={null}
{
"name": "INTERNAL_SERVER_ERROR",
"message": "Internal Server Error",
"status": 500,
"errors": []
}
```
# Cancel Refund | Refunds API
Source: https://docs.paywithatoa.co.uk/api-reference/refund/cancel-refund
DELETE https://api.atoa.me/api/refund/{refundId}/cancel
Cancel refund via the Atoa Refunds API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
This API allows you to cancel a refund that is still in a pending state and has not yet been processed. Once canceled, the refund will not be disbursed to the customerβs bank account.
You can only cancel refunds that are in a pending state and have not been
processed yet.
Sandbox environment refund cancellations cannot be simulated.
### Authorization
Bearer ``
### Request Body Schema
The unique identifier of the refund you want to cancel. This is the refundId
that was returned when you initiated the refund from
Initiate Refund.
### Response
The HTTP status code indicating the result of the operation. A value of 200
indicates success.
A human-readable message describing the result of the operation.
The unique identifier of the refund that was cancelled.
```json 200 theme={null}
{
"status": 200,
"message": "Refund cancelled successfully",
"refundId": "4fcddd66-4425-4bf7-88cc-96463e279eb1"
}
```
```json 400 theme={null}
{
"name": "BadRequestException",
"message": "Refund not found"
}
```
# Get Refund Payments | Refunds API
Source: https://docs.paywithatoa.co.uk/api-reference/refund/get-refund-payments
GET https://api.atoa.me/api/refund/{paymentRequestId}
Retrieve refund payments via the Atoa Refunds API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
This API is used to get the refund details for a given payment request id. It returns information about the refund transaction including the amount, status, and related payment details.
### Authorization
Bearer ``
Using a sandbox token with a production payment request ID, or a production
token with a sandbox payment request ID, will result in an error.
**Request Body Schema**
The payment request id is required for this step, which you'll receive it from
the process-payment API when generating a payment request.
**Response**
The API returns a refund payment object with the following fields:
A unique UUID for the refund transaction.
The ID of the original payment request. Refer to
[process-payment](../payment/process-payment)
The unique identifier for the payment transaction.
The current status of the refund payment
(e.g.,
INITIATED,PENDING,COMPLETED,FAILED) - INITIATED : The refund request has been
raised - PENDING : The refund is being ready to be processed - COMPLETED : The
refund payment has been completed - FAILED : The refund payment has failed -
CANCELLED : The refund payment has been cancelled
The original amount that was paid.
The amount that was refunded.
The three-letter currency code (e.g., GBP).
The notes associated with the refund.
The ID of the order associated with this refund.
The type of refund (e.g., PARTIAL, FULL).
The timestamp when the refund was created.
The timestamp when the refund was last updated.
Only present when metadata was supplied on the refund. See [Initiate Refund](/api-reference/refund/initiate-refund) for the field limits.
```json 200 theme={null}
[
{
"refundId": "a7c82e45-9f31-42b6-8d15-7e63f1b92c4d",
"paymentRequestId": "5e8b2c73-1f64-4a97-9c38-d2f6ae15b8c2",
"paymentIdempotencyId": "ATOA1234567890123",
"status": "INITIATED",
"paidAmount": "250.75",
"refundAmount": "25.50",
"currency": "GBP",
"refundNotes": "Customer requested refund",
"orderId": "987654321",
"refundType": "PARTIAL",
"createdAt": "2023-11-15T14:32:45.789Z",
"updatedAt": "2023-11-15T14:32:45.789Z",
"metadata": {
"refundReason": "damaged-item",
"ticketId": "SUP-9981"
}
}
]
```
```json 400 theme={null}
{
"name": "BAD_REQUEST",
"status": 400,
"message": "No payment request found",
"errors": []
}
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": "[]"
}
```
```json 404 theme={null}
{
"name": "NOT_FOUND",
"message": "No refunds found for this payment request",
"status": 404,
"errors": []
}
```
# Initiate Refund | Refunds API
Source: https://docs.paywithatoa.co.uk/api-reference/refund/initiate-refund
POST https://api.atoa.me/api/refund
Initiate refund via the Atoa Refunds API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Instant refunds are only available for selected merchants with same day payout
functionality. Please reach out to support if you want to enable refunds in
your account.
This API creates a refund for a specific successful payment. The refunded amount is credited to the customer's source bank account via a bank transfer within 30 minutes, provided that you have sufficient balance available in your account.
Refund can be either full or partial.
### Authorization
Bearer ``
### Request Body Schema
The payment request ID of the transaction that needs to be refunded. Only
completed payments can be refunded.
Note: If you need to know how to generate a payment request ID, refer to
Process payment
.
Amount to be refunded (should not exceed the original transaction amount).
Decimal values are allowed. Minimum value is 1.
The currency of the refund amount. If not specified, the currency of the
original transaction will be used. Example: "GBP".
Refund reason for your internal reference.
Note:- The note must be 200 characters or less.
To simulate a `FAILED` refund in sandbox environment you can pass a note with `FAILURE TEST`
Set of key-value pairs that you can attach to an object. This can be useful for storing additional information about the object in a structured format.
* Up to **50** key-value pairs.
* Each key must be **1β40** characters and must not contain `[` or `]`.
* Each value must be a **string** of at most **500** characters.
Once set, metadata is returned in the [Get Refunds](/api-reference/refund/get-refund-payments) response, and is included in the [Refund Status Webhook](/api-reference/webhook/refund-status-webhook-response).
The refund's metadata is independent of the payment's β it is not inherited. Metadata is frozen when the refund is created and cannot be changed afterwards. An empty object (`{}`) is treated as no metadata.
### Response
The unique identifier for the refund transaction. You can use this ID to
cancel the refund later via the
Cancel Refund API.
The ID of the original payment request that is being refunded.
The idempotency ID of the original payment, used to ensure the payment was
processed only once.
The current status of the refund request (e.g., "INITIATED", "COMPLETED",
"FAILED").
The original amount that was paid in the transaction being refunded.
The amount being refunded to the customer.
The currency code for the refund transaction (e.g., "GBP").
Notes or reason provided for the refund.
The order ID associated with the original transaction.
The timestamp when this refund request was last updated.
The timestamp when this refund request was created in the system.
```json 200 theme={null}
{
"refundId": "a7c82e45-9f31-42b6-8d15-7e63f1b92c4d",
"paymentRequestId": "5e8b2c73-1f64-4a97-9c38-d2f6ae15b8c2",
"paymentIdempotencyId": "ATOA1234567890123",
"status": "INITIATED",
"paidAmount": "250.75",
"refundAmount": "25.50",
"currency": "GBP",
"refundNotes": "Customer requested refund",
"orderId": "987654321",
"createdAt": "2023-11-15T14:32:45.789Z",
"updatedAt": "2023-11-15T14:32:45.789Z"
}
```
```json 400 theme={null}
{
"name": "BadRequestException",
"message": "Refund amount exceeded"
}
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": []
}
```
```json 404 theme={null}
{
"name": "NOT_FOUND",
"message": "The payment request ID provided by you does not exist. Please enter a valid one and try again.",
"status": 404,
"errors": []
}
```
```json 405 theme={null}
{
"name": "METHOD_NOT_ALLOWED",
"message": "Refunds are not available for this account. Please contact support.",
"status": 405,
"errors": []
}
```
# List Terminals (PAX) for Remote Payments
Source: https://docs.paywithatoa.co.uk/api-reference/remote-payments/list-terminals
POST https://api.atoa.me/api/terminal/list
List the PAX card terminals registered to your Atoa account via the API, ready to accept remote card and Pay by Bank payments.
Returns the POS terminals registered against the merchant, resolved from the SDK API key you authenticate with.
Live online status (`online` and `lastActive`) is resolved via a real-time
presence lookup against the terminals' push-notification devices. This runs
**by default** β `availability` defaults to `true`. Pass `availability: false`
in the body to skip the lookup (the request is then faster); `online` is
returned as `false` and `lastActive` as `null`.
### Authorization
Bearer ``
**Body**
Filter terminals by one or more stores. Each value must be a valid store UUID.
**If omitted, terminals across all of the merchant's stores are returned.**
When `true` (the default), performs a live presence lookup and populates the
`online` and `lastActive` fields for each terminal. Pass `false` to skip the
lookup; those fields are then not resolved (`online` is `false`, `lastActive`
is `null`).
**Response**
Returns an array of POS terminal objects.
Primary key of the POS terminal. Use this value as the `id` when
initiating a remote payment.
Human-readable name of the POS terminal. Can be `null` when no name is set.
Hardware serial number of the POS terminal device.
ID of the store the terminal belongs to.
Whether the device is currently online according to its push-notification
presence. Only meaningful when the request was made with `availability: true`
(the default); otherwise always `false`.
Unix timestamp (in seconds) of the device's last presence activity. `null`
when unknown or when `availability: false` was sent.
`true` when no push-notification device token is registered against the
terminal. Such a terminal cannot receive remote payments and will always
report `online: false`.
```bash cURL theme={null}
curl --request POST \
--url 'https://api.atoa.me/api/terminal/list' \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"storeIds": [
"43c7b991-0803-4177-9589-d165d9a779f6",
"9f8e7d6c-5b4a-3210-fedc-ba9876543210"
],
"availability": true
}'
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/terminal/list"
payload = {
"storeIds": [
"43c7b991-0803-4177-9589-d165d9a779f6",
"9f8e7d6c-5b4a-3210-fedc-ba9876543210",
],
"availability": True,
}
headers = {
"Authorization": "Bearer ",
"Content-Type": "application/json",
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
```
```javascript JavaScript theme={null}
const response = await fetch("https://api.atoa.me/api/terminal/list", {
method: "POST",
headers: {
Authorization: "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
storeIds: [
"43c7b991-0803-4177-9589-d165d9a779f6",
"9f8e7d6c-5b4a-3210-fedc-ba9876543210",
],
availability: true,
}),
});
const data = await response.json();
console.log(data);
```
```php PHP theme={null}
"https://api.atoa.me/api/terminal/list",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
"storeIds" => [
"43c7b991-0803-4177-9589-d165d9a779f6",
"9f8e7d6c-5b4a-3210-fedc-ba9876543210",
],
"availability" => true,
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer ",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
```
```go Go theme={null}
package main
import (
"bytes"
"fmt"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.atoa.me/api/terminal/list"
payload := []byte(`{"storeIds":["43c7b991-0803-4177-9589-d165d9a779f6","9f8e7d6c-5b4a-3210-fedc-ba9876543210"],"availability":true}`)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload))
req.Header.Add("Authorization", "Bearer ")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.post("https://api.atoa.me/api/terminal/list")
.header("Authorization", "Bearer ")
.header("Content-Type", "application/json")
.body("{\"storeIds\":[\"43c7b991-0803-4177-9589-d165d9a779f6\",\"9f8e7d6c-5b4a-3210-fedc-ba9876543210\"],\"availability\":true}")
.asString();
```
```json 200 theme={null}
[
{
"id": "1",
"name": "Front Till",
"serialNumber": "100200300",
"storeId": "43c7b991-0803-4177-9589-d165d9a779f6",
"online": true,
"lastActive": 1720512000,
"tokenMissing": false
}
]
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": "[]"
}
```
```json 500 theme={null}
{
"name": "INTERNAL_SERVER_ERROR",
"message": "Internal Server Error",
"status": 500,
"errors": "[]"
}
```
# Process Remote Payment
Source: https://docs.paywithatoa.co.uk/api-reference/remote-payments/process-remote-payment
POST https://api.atoa.me/api/terminal/process-remote-payment
Trigger a payment on a merchant's own PAX terminal remotely through the Atoa API, and track it to completion using webhooks.
Creates a remote payment request and notifies a POS terminal so it can present the payment to the customer. The merchant is resolved from the SDK API key you authenticate with.
Pick the target terminal with [List Terminals](/api-reference/remote-payments/list-terminals) and pass its `id`. On success the endpoint returns a `paymentRequestId` you can use to track the payment.
The whole payment is assembled server-side. Fields such as the customerid,
expiry, tips, retry and split behaviour, store and terminal binding, and
environment are all set by Atoa. Only the fields documented below are read
from your request; any other field in the body is ignored.
### Authorization
Bearer ``
**Body Parameters**
The `id` of the POS terminal that should receive the payment, as returned by
[List Terminals](/api-reference/remote-payments/list-terminals). The terminal
must belong to the authenticated merchant, have a registered
push-notification device (`tokenMissing: false`), and have remote payments
enabled.
The amount to charge. Must have at most 2 decimal places.
Controls the set of payment methods shown to the customer on the terminal. If not specified, the terminal defaults to offering all payment methods enabled on the merchant account.
* `PAY_BY_BANK` β When provided, only Pay by Bank will be shown to the customer.
* `CARD` β When provided, only card payment will be shown to the customer. If card is not enabled in your account please contact support.
* If omitted or set to any other value, both options are displayed, allowing the customer to choose their preferred payment method.
Your own reference for this payment, returned in the [Get Payment Status](/api-reference/payment/get-payment-status)
response and the [POS Payment Status Webhook](/api-reference/webhook/pos-payment-status-webhook-response).
Optional β Atoa generates a reference if you omit it.
Optional note shown to the customer. Trimmed of surrounding whitespace and must
not exceed 30 characters.
Optional customer details, saved as a consumer record and linked to this
payment. When both `phoneCountryCode` and `phoneNumber` are supplied the phone
number is validated.
Customer phone country code, e.g. `+44`.
Customer phone number.
Customer email address.
Customer first name.
Customer last name.
Set of key-value pairs that you can attach to an object. This can be useful for storing additional information about the object in a structured format.
* Up to **50** key-value pairs.
* Each key must be **1β40** characters and must not contain `[` or `]`.
* Each value must be a **string** of at most **500** characters.
Once set, metadata is returned in the [Get Transactions](/api-reference/payment/get-transactions), [Get Payout Transactions](/api-reference/payouts/get-payout-transactions), and [Get Payment Status](/api-reference/payment/get-payment-status) responses, and is included in the [POS Payment Status Webhook](/api-reference/webhook/pos-payment-status-webhook-response).
Metadata is frozen when the payment is created and cannot be changed afterwards. An empty object (`{}`) is treated as no metadata.
**Response**
The identifier (UUID) of the created payment request. Use it to track the
payment via the payment-status endpoint or the `POS_PAYMENT_STATUS` webhook.
### What happens next
The target terminal prompts the customer to complete the payment. To track the
outcome, poll [Get Payment Status](/api-reference/payment/get-payment-status) with
the returned `paymentRequestId`, and/or subscribe to the
[`POS_PAYMENT_STATUS` webhook](/api-reference/webhook/pos-payment-status-webhook-response).
Receiving webhooks requires a `whsec_` signing secret configured for your
endpoint.
If you're experiencing issues, please check the [Troubleshooting](/remote-payments#troubleshooting)
section.
**Errors**
| Status | When |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | Missing `id`, remote payments not enabled for the merchant or the terminal, terminal not configured for push notifications, `notes` over the 30-character limit, or `orderId` empty, over 50 characters, or containing unsupported characters. |
| `401` | Missing or invalid API key. |
| `404` | Terminal not found, or not owned by the authenticated merchant. |
| `500` | Unexpected internal error. |
```bash cURL theme={null}
curl --request POST \
--url 'https://api.atoa.me/api/terminal/process-remote-payment' \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"id": "1",
"amount": 42.50,
"paymentMethod": "PAY_BY_BANK",
"orderId": "ORD-1",
"notes": "Table 12",
"consumerDetails": {
"phoneCountryCode": "+44",
"phoneNumber": "7700900123",
"email": "customer@example.com",
"firstName": "Alex",
"lastName": "Doe"
},
"metadata": {
"bookingId": "BK-42427",
"orderReference": "ORD-1"
}
}'
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/terminal/process-remote-payment"
headers = {
"Authorization": "Bearer ",
"Content-Type": "application/json",
}
payload = {
"id": "1",
"amount": 42.50,
"paymentMethod": "PAY_BY_BANK",
"orderId": "ORD-1",
"notes": "Table 12",
"consumerDetails": {
"phoneCountryCode": "+44",
"phoneNumber": "7700900123",
"email": "customer@example.com",
"firstName": "Alex",
"lastName": "Doe",
},
"metadata": {
"bookingId": "BK-42427",
"orderReference": "ORD-1",
},
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.atoa.me/api/terminal/process-remote-payment",
{
method: "POST",
headers: {
Authorization: "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
id: "1",
amount: 42.5,
paymentMethod: "PAY_BY_BANK",
orderId: "ORD-1",
notes: "Table 12",
consumerDetails: {
phoneCountryCode: "+44",
phoneNumber: "7700900123",
email: "customer@example.com",
firstName: "Alex",
lastName: "Doe",
},
metadata: {
bookingId: "BK-42427",
orderReference: "ORD-1",
},
}),
}
);
const data = await response.json();
console.log(data);
```
```php PHP theme={null}
"https://api.atoa.me/api/terminal/process-remote-payment",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
"id" => "1",
"amount" => 42.50,
"paymentMethod" => "PAY_BY_BANK",
"orderId" => "ORD-1",
"notes" => "Table 12",
"consumerDetails" => [
"phoneCountryCode" => "+44",
"phoneNumber" => "7700900123",
"email" => "customer@example.com",
"firstName" => "Alex",
"lastName" => "Doe"
],
"metadata" => [
"bookingId" => "BK-42427",
"orderReference" => "ORD-1"
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer ",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
```
```go Go theme={null}
package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
)
func main() {
url := "https://api.atoa.me/api/terminal/process-remote-payment"
payload := []byte(`{
"id": "1",
"amount": 42.50,
"paymentMethod": "PAY_BY_BANK",
"orderId": "ORD-1",
"notes": "Table 12",
"metadata": {
"bookingId": "BK-42427",
"orderReference": "ORD-1"
}
}`)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload))
req.Header.Add("Authorization", "Bearer ")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.post("https://api.atoa.me/api/terminal/process-remote-payment")
.header("Authorization", "Bearer ")
.header("Content-Type", "application/json")
.body("{\"id\":\"1\",\"amount\":42.50,\"paymentMethod\":\"PAY_BY_BANK\",\"orderId\":\"ORD-1\",\"notes\":\"Table 12\",\"metadata\":{\"bookingId\":\"BK-42427\",\"orderReference\":\"ORD-1\"}}")
.asString();
```
```json 200 theme={null}
{
"paymentRequestId": "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
}
```
```json 400 theme={null}
{
"name": "BAD_REQUEST",
"message": "Remote payments are not enabled for this merchant.",
"status": 400,
"errors": "[]"
}
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": "[]"
}
```
```json 404 theme={null}
{
"name": "NOT_FOUND",
"message": "Terminal not found",
"status": 404,
"errors": "[]"
}
```
```json 500 theme={null}
{
"name": "INTERNAL_SERVER_ERROR",
"message": "Internal Server Error",
"status": 500,
"errors": "[]"
}
```
# Create webhook event
Source: https://docs.paywithatoa.co.uk/api-reference/webhook/create-webhook-event
Post https://api.atoa.me/api/webhook/merchant
Create webhook event via the Atoa Webhooks API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
**v1 Legacy API** β This endpoint registers a single event type per call. Authentication configuration is not supported in v1 β use the [v2 Create Endpoint](/api-reference/webhook/v2/create-endpoint) to configure authentication, subscribe to multiple events per endpoint, and manage endpoints with full CRUD.
Register a webhook endpoint for a specific event type. Each event type requires a separate subscription. See the [Webhook introduction](/api-reference/webhook/introduction) for event types and signature verification.
### Authorization
Bearer ``
**Request Body Schema**
The endpoint URL that will receive webhook notifications. Must return HTTP 200 on success β failed deliveries are retried with exponential back-off.
The event type to subscribe to: `PAYMENTS_STATUS`, `EXPIRED_STATUS`, `REFUND_STATUS`, `POS_PAYMENT_STATUS`, or `PAYMENT_METHOD_STATUS`.
**Response**
Unique identifier for this webhook subscription. Use this value to delete the subscription via `DELETE /api/webhook/{webhookId}/merchant`.
The registered webhook URL.
The subscribed event type.
```bash Curl theme={null}
curl --request POST \
--url https://api.atoa.me/api/webhook/merchant \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"url": "https://api/endpoint",
"event": "PAYMENTS_STATUS"
}'
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/webhook/merchant"
payload = {
"url": "https://api/endpoint",
"event": "PAYMENTS_STATUS"
}
headers = {
"Authorization": "Bearer ",
"Content-Type": "application/json"
}
response = requests.request("POST", url, json=payload, headers=headers)
print(response.text)
```
```javaScript javaScript theme={null}
const options = {
method: 'POST',
headers: {Authorization: 'Bearer ', 'Content-Type': 'application/json'},
body: '{"url":"https://api/endpoint","event":"PAYMENTS_STATUS"}'
};
fetch('https://api.atoa.me/api/webhook/merchant', options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
```php PHP theme={null}
"https://api.atoa.me/api/webhook/merchant",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "{\n \"url\": \"https://api/endpoint\",\n \"event\": \"PAYMENTS_STATUS\"\n }",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer ",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
```
```Go Go theme={null}
package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.atoa.me/api/webhook/merchant"
payload := strings.NewReader("{\n \"url\": \"https://api/endpoint\",\n \"event\": \"PAYMENTS_STATUS\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer ")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(res)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.post("https://api.atoa.me/api/webhook/merchant")
.header("Authorization", "Bearer ")
.header("Content-Type", "application/json")
.body("{\n \"url\": \"https://api/endpoint\",\n \"event\": \"PAYMENTS_STATUS\"\n}")
.asString();
```
```json 200 theme={null}
{
"webhookId": "4a124f25-fe8d-47a0-92c8-16d94cbfe24c",
"url": "https://example.com",
"event": "PAYMENTS_STATUS"
}
```
```json 400 (Missing URL) theme={null}
{
"name": "AJV_VALIDATION_ERROR",
"message": "Bad request on parameter request.body.WebhookDataModel must have required property 'url'. Given value: undefined",
"status": 400,
"errors": [
{
"instancePath": "",
"schemaPath": "required",
"keyword": "required",
"params": {
"missingProperty": "url"
},
"message": "must have required property 'url' ",
"modelName": "WebhookDataModel",
"dataPath": ""
}
]
}
```
```json 400 (POS without signing secret) theme={null}
{
"name": "BAD_REQUEST",
"message": "POS Payment Status requires V2 signing. Please generate a signing secret from the dashboard to proceed.",
"status": 400
}
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": "[]"
}
```
```json 404 theme={null}
{
"name": "NOT_FOUND",
"message": "Webhook not found",
"status": 404,
"errors": "[]"
}
```
# Delete webhook event
Source: https://docs.paywithatoa.co.uk/api-reference/webhook/delete-webhook-event
Delete https://api.atoa.me/api/webhook/{webhookId}/merchant
Delete webhook event via the Atoa Webhooks API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
**v1 Legacy API** β Deletes a single event subscription by its `webhookId`. For new integrations, use [Delete Endpoint (v2)](/api-reference/webhook/v2/delete-endpoint) which deletes the entire endpoint and all its event subscriptions at once.
Deletes a webhook subscription from your business account.
### Authorization
Bearer ``
The unique identifier of the webhook which needs to be deleted
```json 200 theme={null}
{
"message": "Webhook deleted successfully"
}
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": "[]"
}
```
```json 404 theme={null}
{
"name": "NOT_FOUND",
"message": "Webhook not found",
"status": 404,
"errors": "[]"
}
```
**Response**
Webhook deleted successfully
# Expired Status Webhook Payload
Source: https://docs.paywithatoa.co.uk/api-reference/webhook/expired-status-webhook-response
Reference for the Atoa 'expired Status Webhook Response' webhook payload, the fields Atoa sends and an example JSON body to handle.
Delivered when a payment request expires before the customer pays. Expiry time is set via the `expiresIn` parameter when [creating the payment request](/api-reference/payment/process-payment#payment-expiration).
**V2 Webhook Signing:** If you have enabled [V2 webhook signing](/api-reference/webhook/v2-signing), the `signatureHash` and `signature` fields are removed from the payload and an `eventType` field is added. The webhook signature is delivered in the `X-Atoa-Signature` HTTP header instead.
```json Expired theme={null}
{
"merchantId": "41a77f74-8b67-4f04-be50-c8dd5ad8836f",
"customerId": "Abcam",
"status": "EXPIRED",
"paidAmount": 1005,
"currency": "GBP",
"storeDetails": {
"id": "ee39ecfa-e336-461c-a957-1adc76ac087c",
"address": "Welwyn Garden City, Hertfordshire",
"locationName": "DEFAULT"
},
"orderId": "003decb3-c35",
"paymentRequestId": "6d1803f8-038a-48f3-8cd4-b44d37a3ab91",
"signatureHash": "1ea6ff07ca90ec4ca34dfd3b420dcdfe5db2d73e3a5de05e5379e8aa325a5e2e",
"redirectUrl": "https://paywithatoa.co.uk",
"redirectUrlParams": "{}",
"metadata": {
"bookingId": "BK-42427",
"orderReference": "ORD-1"
}
}
```
Only present when metadata was supplied on the payment. See [Process Payment](/api-reference/payment/process-payment) for the field limits.
# Get all merchant webhooks
Source: https://docs.paywithatoa.co.uk/api-reference/webhook/get-all-merchant-webhooks
Get https://api.atoa.me/api/webhook/merchant
Retrieve all merchant webhooks via the Atoa Webhooks API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
**v1 Legacy API** β Returns a flat list with one entry per event subscription. For new integrations, use [List Endpoints (v2)](/api-reference/webhook/v2/list-endpoints) which returns paginated endpoint objects with all subscribed events grouped per endpoint.
Returns all active webhook subscriptions for your business account.
### Authorization
Bearer ``
**Response**
Array Of Objects
Unique identifier for the webhook.
The registered webhook URL.
The subscribed event type.
```json 200 theme={null}
[
{
"webhookId": "4a124f25-fe8d-47a0-92c8-16d94cbfe24c",
"url": "https://api/endpoint",
"event": "PAYMENTS_STATUS"
},
{
"webhookId": "8b3f6a12-cd45-4e89-ab12-7d9e3f1c5a08",
"url": "https://api/endpoint",
"event": "POS_PAYMENT_STATUS"
}
]
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": "[]"
}
```
# Introduction | Webhooks API
Source: https://docs.paywithatoa.co.uk/api-reference/webhook/introduction
Atoa webhooks: subscribe to payment, refund and payout events so your system is notified in real time. Setup, event types and payloads.
Webhooks allow you to receive real-time notifications when events occur on Atoa, delivered directly to an HTTP endpoint of your choice. They eliminate the need to poll the Atoa API for status updates, saving time and resources.
## Event types
Subscribe to the event types relevant to your integration:
| Event type | Description | Payload reference |
| ----------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
| `PAYMENTS_STATUS` | Pay by Bank and Card on File payment completed, pending, or failed | [Payload](/api-reference/webhook/process-payment-webhook-response) |
| `EXPIRED_STATUS` | Payment request expired before the customer completed payment | [Payload](/api-reference/webhook/expired-status-webhook-response) |
| `REFUND_STATUS` | Refund completed, cancelled, or failed | [Payload](/api-reference/webhook/refund-status-webhook-response) |
| `POS_PAYMENT_STATUS` | POS terminal payments (Pay by Bank and card) and POS-initiated refunds (voids) | [Payload](/api-reference/webhook/pos-payment-status-webhook-response) |
| `PAYMENT_METHOD_STATUS` | Customer saved a payment method for future payments, or removed a saved one | [Payload](/api-reference/webhook/payment-method-status-webhook-response) |
Payments expire after the [expiresIn](/api-reference/payment/process-payment#payment-expiration) duration. See the [refunds](/api-reference/refund/initiate-refund) API for refund event details.
## Multiple endpoints per event type
You can register up to **3 endpoints per event type**. When an event fires, Atoa delivers the webhook to all registered endpoints for that event simultaneously β each endpoint receives its own independent delivery with its own retry tracking.
This lets different systems subscribe to the same event independently without coupling them together. For example:
| Endpoint | Subscribed events |
| ------------------------------------------- | ----------------------------------- |
| `https://orders.your-server.com/webhook` | `PAYMENTS_STATUS`, `REFUND_STATUS` |
| `https://analytics.your-server.com/webhook` | `PAYMENTS_STATUS` |
| `https://crm.your-server.com/webhook` | `PAYMENTS_STATUS`, `EXPIRED_STATUS` |
All three endpoints above receive `PAYMENTS_STATUS` webhooks independently. A failure at one endpoint does not affect delivery to the others.
The limit is **3 endpoints per event type**. Attempting to register a fourth endpoint for an event type that already has 3 returns a `400` error. Use [List endpoints](/api-reference/webhook/v2/list-endpoints) to review your current registrations. Multiple endpoints per event type is a **v2-only** feature β v1 supports one endpoint per event type.
## API versions
Atoa provides two webhook API versions. **Use v2 for all new integrations.**
| | v1 (Legacy) | v2 (Recommended) |
| ----------------------- | ----------------------- | --------------------------------- |
| **Events per endpoint** | Single | Multiple |
| **Full CRUD** | Create only | Create, read, update, delete |
| **Description field** | β | β |
| **Failure tracking** | β | `failureCount`, `lastDeliveredAt` |
| **Base path** | `/api/webhook/merchant` | `/api/webhook/v2/endpoints` |
v1 remains supported for existing integrations. For new implementations, use the [v2 API](/api-reference/webhook/v2/create-endpoint), which lets you subscribe to multiple event types per endpoint and manage authentication after creation.
## Getting started
Use the [Create Endpoint (v2)](/api-reference/webhook/v2/create-endpoint) API to register your endpoint URL and subscribe to one or more event types in a single call.
```bash theme={null}
curl --request POST \
--url https://api.atoa.me/api/webhook/v2/endpoints \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"url": "https://your-server.com/webhook",
"events": ["PAYMENTS_STATUS", "REFUND_STATUS"]
}'
```
Use [List Endpoints](/api-reference/webhook/v2/list-endpoints) to view, [Update Endpoint](/api-reference/webhook/v2/update-endpoint) to modify, or [Delete Endpoint](/api-reference/webhook/v2/delete-endpoint) to remove subscriptions.
Return HTTP `200` on success β any other response triggers [retries](#retry-mechanism). Each event type has a different payload structure β see the [Event types](#event-types) table for payload references.
Every webhook delivery is signed so you can verify it genuinely came from Atoa. See [Signature verification](#signature-verification) below for the two available methods.
## Testing your webhook
Send a test webhook to your sandbox URL without creating a real payment. The body and signature match a production event, so you can verify your integration end-to-end before going live.
Use the [Trigger test webhook event](/api-reference/webhook/test-webhook-event) endpoint:
```bash theme={null}
curl --request POST \
--url https://api.atoa.me/api/webhook/test \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"eventType": "PAYMENTS_STATUS"
}'
```
Override fields like `orderId`, `amount`, `paymentMethod`, and `status` to simulate specific scenarios. See the [Trigger test webhook event](/api-reference/webhook/test-webhook-event) reference for all available fields and per-event rules.
Test triggers are **sandbox-only** β production keys are rejected. You must have an active subscription for the event type before the trigger will deliver.
## Signature verification
Atoa supports two methods for verifying webhook authenticity. **We recommend V2 signing for all new integrations.**
| | V1 (Legacy) | V2 (Recommended) |
| ------------------ | ------------------------------------------ | ------------------------------ |
| **How it works** | `signatureHash` field inside the JSON body | `X-Atoa-Signature` HTTP header |
| **What is signed** | `orderId \| paymentRequestId` | The entire JSON request body |
| **Algorithm** | HMAC-SHA256 | HMAC-SHA256 |
### V2 Signing
V2 signing covers the entire request body with HMAC-SHA256, making it more secure than field-level V1 signing. V2 is **required** for `POS_PAYMENT_STATUS` webhooks.
1. Generate a signing secret from the [Atoa Dashboard](https://dashboard.paywithatoa.co.uk/my-account/settings/webhooks) under **Settings β Webhooks**.
2. For every incoming webhook, compute `HMAC-SHA256(signingSecret, rawRequestBody)` and compare the hex digest against the `X-Atoa-Signature` header value (after stripping the `v1=` prefix).
3. Use a timing-safe comparison function to prevent timing attacks.
See the [V2 Webhook Signing guide](/api-reference/webhook/v2-signing) for full setup instructions and code samples in Node.js, Python, Java, Go, and PHP.
### V1 Signing
V1 signing verifies authenticity using the `signatureHash` field included in the webhook body. It signs only the `orderId` and `paymentRequestId` (or `refundId` for refund webhooks).
See the [V1 Webhook Signing guide](/api-reference/webhook/v1-signing) for full verification steps and code samples.
If you enable V2 signing, the `signatureHash` and `signature` fields are removed from the webhook body and an `eventType` field is added instead. V1 and V2 cannot be used simultaneously for the same merchant.
## Endpoint authentication
Optionally configure authentication so Atoa includes credentials in every delivery, in addition to signature verification.
| Method | Description |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **None** | No additional authentication. Rely on [signature verification](#signature-verification) to validate deliveries. |
| **OAuth 2.0** | Atoa obtains access tokens from your authorisation server and includes them in webhook requests. Pass `clientId`, `clientSecret`, and `authUrl` when creating the webhook. |
| **Basic Auth** | Atoa includes a username and password in the Authorization header. Pass `username` and `password` when creating the webhook. |
Signature verification cryptographically proves requests come from Atoa. For maximum security, combine it with OAuth or Basic Authentication.
## Retry mechanism
When a webhook response is anything other than HTTP `200`, it is considered a failure. Our system retries using an exponential back-off strategy. If the webhook continues to fail over 24 hours, Atoa stops the retry attempts and sends an email notification to the business owner with the failing webhook URL.
# Payment Method Status Webhook Payload
Source: https://docs.paywithatoa.co.uk/api-reference/webhook/payment-method-status-webhook-response
Reference for the Atoa 'Payment Method Status Webhook' payload, the fields Atoa sends and an example JSON body to handle.
Delivered when a customer saves a payment method for future payments, or removes a saved one. Use it to keep your own record of a customer's saved payment methods in step with Atoa.
| Status | Description |
| ----------- | ------------------------------------------------------------------------- |
| **ADDED** | Payment method was saved against the customer and can be charged on file. |
| **DELETED** | Payment method was removed from the customer's saved payment methods. |
`type` tells you the kind of payment method, and which detail object the payload carries. `CARD` carries `cardDetails`.
`metadata` is the key/value data you attached when saving the card (see [Authorize Card](/api-reference/card-on-file/authorize-card)), and `createdAt` is when the card was saved. Both match what the [List Payment Methods](/api-reference/payment-methods/list-payment-methods) API returns, and are omitted when the card carried no metadata.
```json Added theme={null}
{
"status": "ADDED",
"type": "CARD",
"customerId": "6439bb4f-46c6-4ff6-b58a-fe811ddfa8eb",
"paymentMethodId": "card_0508c108caa91d0a937c028da2756ff1",
"businessId": "f96cd01b-b13c-4c21-9d52-c1de4e8721ac",
"metadata": { "internalId": "42" },
"createdAt": "2025-06-15T10:30:00.000Z",
"cardDetails": {
"lastFourDigits": "1111",
"brand": "VISA",
"cardType": "DEBIT",
"expiryMonth": "04",
"expiryYear": "2031"
}
}
```
```json Deleted theme={null}
{
"status": "DELETED",
"type": "CARD",
"customerId": "6439bb4f-46c6-4ff6-b58a-fe811ddfa8eb",
"paymentMethodId": "card_0508c108caa91d0a937c028da2756ff1",
"businessId": "f96cd01b-b13c-4c21-9d52-c1de4e8721ac",
"metadata": { "internalId": "42" },
"createdAt": "2025-06-15T10:30:00.000Z",
"cardDetails": {
"lastFourDigits": "1111",
"brand": "VISA",
"cardType": "DEBIT",
"expiryMonth": "04",
"expiryYear": "2031"
}
}
```
The detail object and its fields are optional β a field is omitted when the provider did not report it. Only `status`, `type`, `customerId`, `paymentMethodId` and `businessId` are guaranteed.
Use `paymentMethodId` to charge the saved payment method β see [Charge Saved Card](/api-reference/card-on-file/charge-saved-card). [Authorize Card](/api-reference/card-on-file/authorize-card) generates the link a customer uses to save one.
# POS Payment Status Webhook Payload
Source: https://docs.paywithatoa.co.uk/api-reference/webhook/pos-payment-status-webhook-response
Reference for the Atoa 'pos Payment Status Webhook Response' webhook payload, the fields Atoa sends and an example JSON body to handle.
Delivers notifications for all POS terminal (Pax machine) activity:
* **Payments** β Pay by Bank and card payments at the terminal.
* **Refunds (voids)** β refunds from the POS terminal. Not delivered under `REFUND_STATUS`.
API or Dashboard refunds are delivered under `REFUND_STATUS`, not here.
## Payment statuses
| Status | Description |
| ------------- | ------------------------------------------------------------------------------------------- |
| **COMPLETED** | Payment processed successfully. Funds have been transferred to the merchant's account. |
| **FAILED** | Payment was unsuccessful, including reversed transactions. The customer will need to retry. |
| **EXPIRED** | Payment request expired before the customer completed the payment. |
## Refund statuses
| Status | Description |
| ------------- | ---------------------------------------------------------------------------------- |
| **COMPLETED** | Refund processed successfully. Funds have been credited to the customer's account. |
| **FAILED** | Refund could not be processed. The refund may need to be retried. |
Distinguish payment from refund deliveries by checking for `refundId`. Refund payloads use `refundId` and `refundAmount` instead of payment fields like `tipAmount` and `storeDetails`.
**V2 signing required:** Subscribing to `POS_PAYMENT_STATUS` requires a [V2 signing secret](/api-reference/webhook/v2-signing). All POS webhooks are delivered with V2 signing β the `X-Atoa-Signature` header contains the HMAC-SHA256 signature and the body includes an `eventType` field.
***
## Payloads
```json Payment - Completed theme={null}
{
"merchantId": "41a77f74-8b67-4f04-be50-c8dd5ad8836f",
"customerId": "Abcam",
"consumerId": "60513141-4d0f-4d3d-9c8a-cc1bd61f40fd",
"merchantName": "My Shop",
"paymentIdempotencyId": "ATOA1695808662681",
"status": "COMPLETED",
"statusDetails": null,
"paidAmount": 25.50,
"tipAmount": 0,
"currency": "GBP",
"createdAt": "2026-03-20T14:30:00.000Z",
"updatedAt": "2026-03-20T14:31:00.000Z",
"taxAmount": 0,
"serviceAmount": 0,
"transactionType": "CARD",
"storeDetails": {
"id": "d77e02d5-4e93-46cf-a8be-50da650df562",
"address": "London",
"locationName": "London Store"
},
"orderId": "POS-ORDER-001",
"paymentRequestId": "9baa68d8-362a-4127-994d-2ea622ef35ee",
"errorDescription": null,
"redirectUrlParams": "{}",
"splitBill": false,
"notes": "POS terminal payment",
"metadata": {
"bookingId": "BK-42427",
"orderReference": "ORD-1"
},
"customFields": [
{
"fieldName": "Invoice Number",
"value": "INV-2026-0042",
"filledBy": "BUSINESS"
}
],
"eventType": "POS_PAYMENT_STATUS"
}
```
```json Payment - Failed theme={null}
{
"merchantId": "41a77f74-8b67-4f04-be50-c8dd5ad8836f",
"customerId": "Abcam",
"consumerId": "60513141-4d0f-4d3d-9c8a-cc1bd61f40fd",
"merchantName": "My Shop",
"paymentIdempotencyId": "ATOA1695808662681",
"status": "FAILED",
"statusDetails": null,
"paidAmount": 25.50,
"tipAmount": 0,
"currency": "GBP",
"createdAt": "2026-03-20T14:30:00.000Z",
"updatedAt": "2026-03-20T14:30:05.000Z",
"taxAmount": 0,
"serviceAmount": 0,
"transactionType": "CARD",
"storeDetails": {
"id": "d77e02d5-4e93-46cf-a8be-50da650df562",
"address": "London",
"locationName": "London Store"
},
"orderId": "POS-ORDER-002",
"paymentRequestId": "9baa68d8-362a-4127-994d-2ea622ef35ee",
"errorDescription": "Card processing error. Please try a different card.",
"redirectUrlParams": "{}",
"splitBill": false,
"notes": "POS terminal payment",
"metadata": {
"bookingId": "BK-42427",
"orderReference": "ORD-1"
},
"customFields": [
{
"fieldName": "Invoice Number",
"value": "INV-2026-0042",
"filledBy": "BUSINESS"
}
],
"eventType": "POS_PAYMENT_STATUS"
}
```
```json Payment - Expired theme={null}
{
"merchantId": "41a77f74-8b67-4f04-be50-c8dd5ad8836f",
"customerId": "Abcam",
"status": "EXPIRED",
"paidAmount": 25.50,
"currency": "GBP",
"storeDetails": {
"id": "d77e02d5-4e93-46cf-a8be-50da650df562",
"address": "London",
"locationName": "London Store"
},
"orderId": "POS-ORDER-003",
"paymentRequestId": "9baa68d8-362a-4127-994d-2ea622ef35ee",
"redirectUrl": "https://paywithatoa.co.uk",
"redirectUrlParams": {},
"metadata": {
"bookingId": "BK-42427",
"orderReference": "ORD-1"
},
"customFields": [
{
"fieldName": "Invoice Number",
"value": "INV-2026-0042",
"filledBy": "BUSINESS"
}
],
"eventType": "POS_PAYMENT_STATUS"
}
```
```json Refund - Completed theme={null}
{
"refundId": "c4e92a73-5f12-47c9-8b31-6e54f2d8a91c",
"paymentRequestId": "8f2471db-3456-9cd8-eff2-1a3b5c7d9e0f",
"paymentIdempotencyId": "ATOA1893245678912",
"status": "COMPLETED",
"paidAmount": 25.50,
"refundAmount": 10.00,
"currency": "GBP",
"refundNotes": "Customer requested partial refund",
"orderId": "POS-ORDER-001",
"createdAt": "2026-03-20T15:00:00.000Z",
"updatedAt": "2026-03-20T15:01:30.000Z",
"metadata": {
"refundReason": "damaged-item",
"ticketId": "SUP-9981"
},
"customFields": [
{
"fieldName": "Invoice Number",
"value": "INV-2026-0042",
"filledBy": "BUSINESS"
}
],
"eventType": "POS_PAYMENT_STATUS"
}
```
```json Refund - Failed theme={null}
{
"refundId": "c4e92a73-5f12-47c9-8b31-6e54f2d8a91c",
"paymentRequestId": "8f2471db-3456-9cd8-eff2-1a3b5c7d9e0f",
"paymentIdempotencyId": "ATOA1893245678912",
"status": "FAILED",
"paidAmount": 25.50,
"refundAmount": 10.00,
"currency": "GBP",
"refundNotes": "Customer requested partial refund",
"orderId": "POS-ORDER-001",
"createdAt": "2026-03-20T15:00:00.000Z",
"updatedAt": "2026-03-20T15:01:30.000Z",
"errorMessage": "The customer's bank rejected the refund. Please try again.",
"metadata": {
"refundReason": "damaged-item",
"ticketId": "SUP-9981"
},
"customFields": [
{
"fieldName": "Invoice Number",
"value": "INV-2026-0042",
"filledBy": "BUSINESS"
}
],
"eventType": "POS_PAYMENT_STATUS"
}
```
***
## Custom fields
Custom fields collected during the terminal payment appear as a `customFields` array. For refunds, custom fields from the original payment are carried over.
| Field | Type | Description |
| ----------- | ------ | ---------------------------------------------------- |
| `fieldName` | string | The name of the custom field (e.g. "Invoice Number") |
| `value` | string | The value entered for the field |
| `filledBy` | string | Who provided the value β `BUSINESS` or `PAYEE` |
Only present when custom fields were entered at the terminal. Not included in `PAYMENTS_STATUS` or `EXPIRED_STATUS` webhooks.
***
## Metadata
Merchant-supplied metadata set when creating the payment appears as a `metadata` object β a flat map of string keys to string values. Unlike custom fields (entered at the terminal), metadata is supplied programmatically and is frozen at payment creation.
Only present when metadata was supplied on the payment. See [Process Payment](/api-reference/payment/process-payment) for the field limits.
***
## Signature verification
All `POS_PAYMENT_STATUS` webhooks use V2 signing. To verify the signature, use the `X-Atoa-Signature` header as described in the [V2 Webhook Signing guide](/api-reference/webhook/v2-signing).
# Payment Status Webhook Payload
Source: https://docs.paywithatoa.co.uk/api-reference/webhook/process-payment-webhook-response
Reference for the Atoa 'process Payment Webhook Response' webhook payload, the fields Atoa sends and an example JSON body to handle.
Delivered when a payment is completed, pending, or fails. Same payload structure for Pay by Bank and Card on File.
| Status | Description |
| -------------- | -------------------------------------------------------------------------------------------------------------------------- |
| **COMPLETED** | Payment processed successfully. Funds have been transferred to the merchant's account. |
| **PENDING** | Payment is in progress and awaiting final confirmation from the bank. |
| **FAILED** | Transaction was unsuccessful. The customer will have to retry the payment. |
| **AUTHORIZED** | Card payment has been authorized but not yet captured. Only for `MANUAL_CAPTURE` or `CAPTURE_BEFORE_EXPIRY` card payments. |
| **CANCELLED** | An authorized card payment was cancelled before capture. |
**Card on File:** Card payments use the same payload with card-specific statuses (`AUTHORIZED`, `CANCELLED`). Use `transactionType` (`CARD` vs `OPEN_BANKING`) to distinguish payment types. See the [Card on File guide](/api-reference/card-on-file/introduction) for details.
**V2 Webhook Signing:** If you have enabled [V2 webhook signing](/api-reference/webhook/v2-signing), the `signatureHash` and `signature` fields are removed from the payload and an `eventType` field is added. The webhook signature is delivered in the `X-Atoa-Signature` HTTP header instead.
```json Pay by Bank theme={null}
{
"merchantId": "41a77f74-8b67-4f04-be50-c8dd5ad8836f",
"customerId": "Abcam",
"consumerId": "60513141-4d0f-4d3d-9c8a-cc1bd61f40fd",
"merchantName": "John doe",
"paymentIdempotencyId": "ATOA1695808662681",
"status": "COMPLETED",
"statusDetails": null,
"paidAmount": "1005",
"tipAmount": "5.00",
"currency": "GBP",
"createdAt": "2023-09-27T09:57:50.067Z",
"updatedAt": "2023-09-27T09:57:50.067Z",
"taxAmount": 50.25,
"serviceAmount": 30.15,
"storeDetails": {
"id": "ee39ecfa-e336-461c-a957-1adc76ac087c",
"address": "Welwyn Garden City, Hertfordshire",
"locationName": "DEFAULT"
},
"orderId": "003decb3-c35",
"paymentRequestId": "6d1803f8-038a-48f3-8cd4-b44d37a3ab91",
"signatureHash": "1ea6ff07ca90ec4ca34dfd3b420dcdfe5db2d73e3a5de05e5379e8aa325a5e2e",
"errorDescription": "null",
"redirectUrlParams": "{}",
"redirectUrl": "https://paywithatoa.co.uk",
"splitBill": false,
"notes": "Transaction notes",
"metadata": {
"bookingId": "BK-42427",
"orderReference": "ORD-1"
}
}
```
Only present when metadata was supplied on the payment. See [Process Payment](/api-reference/payment/process-payment) for the field limits.
```json Card - Completed theme={null}
{
"merchantId": "41a77f74-8b67-4f04-be50-c8dd5ad8836f",
"customerId": "550e8400-e29b-41d4-a716-446655440000",
"consumerId": "60513141-4d0f-4d3d-9c8a-cc1bd61f40fd",
"merchantName": "My Shop",
"paymentIdempotencyId": "ATOA1695808662681",
"status": "COMPLETED",
"statusDetails": null,
"paidAmount": 10.50,
"tipAmount": 0,
"currency": "GBP",
"createdAt": "2025-06-15T10:30:00.000Z",
"updatedAt": "2025-06-15T10:31:00.000Z",
"taxAmount": 0,
"serviceAmount": 0,
"transactionType": "CARD",
"storeDetails": {
"id": "d77e02d5-4e93-46cf-a8be-50da650df562",
"address": "London",
"locationName": "London Store"
},
"orderId": "ORDER-001",
"paymentRequestId": "9baa68d8-362a-4127-994d-2ea622ef35ee",
"signatureHash": "1ea6ff07ca90ec4ca34dfd3b420dcdfe5db2d73e3a5de05e5379e8aa325a5e2e",
"errorDescription": null,
"redirectUrlParams": "{}",
"splitBill": false,
"notes": "Payment for order #001",
"metadata": {
"bookingId": "BK-42427",
"orderReference": "ORD-1"
}
}
```
```json Card - Authorized theme={null}
{
"merchantId": "41a77f74-8b67-4f04-be50-c8dd5ad8836f",
"customerId": "550e8400-e29b-41d4-a716-446655440000",
"consumerId": "60513141-4d0f-4d3d-9c8a-cc1bd61f40fd",
"merchantName": "My Shop",
"paymentIdempotencyId": "ATOA1695808662681",
"status": "AUTHORIZED",
"statusDetails": null,
"paidAmount": 10.50,
"tipAmount": 0,
"currency": "GBP",
"createdAt": "2025-06-15T10:30:00.000Z",
"updatedAt": "2025-06-15T10:30:05.000Z",
"taxAmount": 0,
"serviceAmount": 0,
"transactionType": "CARD",
"storeDetails": {
"id": "d77e02d5-4e93-46cf-a8be-50da650df562",
"address": "London",
"locationName": "London Store"
},
"orderId": "ORDER-002",
"paymentRequestId": "9baa68d8-362a-4127-994d-2ea622ef35ee",
"signatureHash": "1ea6ff07ca90ec4ca34dfd3b420dcdfe5db2d73e3a5de05e5379e8aa325a5e2e",
"errorDescription": null,
"redirectUrlParams": "{}",
"splitBill": false,
"notes": "Manual capture payment"
}
```
```json Card - Failed theme={null}
{
"merchantId": "41a77f74-8b67-4f04-be50-c8dd5ad8836f",
"customerId": "550e8400-e29b-41d4-a716-446655440000",
"consumerId": "60513141-4d0f-4d3d-9c8a-cc1bd61f40fd",
"merchantName": "My Shop",
"paymentIdempotencyId": "ATOA1695808662681",
"status": "FAILED",
"statusDetails": null,
"paidAmount": 10.50,
"tipAmount": 0,
"currency": "GBP",
"createdAt": "2025-06-15T10:30:00.000Z",
"updatedAt": "2025-06-15T10:30:05.000Z",
"taxAmount": 0,
"serviceAmount": 0,
"transactionType": "CARD",
"storeDetails": {
"id": "d77e02d5-4e93-46cf-a8be-50da650df562",
"address": "London",
"locationName": "London Store"
},
"orderId": "ORDER-003",
"paymentRequestId": "9baa68d8-362a-4127-994d-2ea622ef35ee",
"signatureHash": "1ea6ff07ca90ec4ca34dfd3b420dcdfe5db2d73e3a5de05e5379e8aa325a5e2e",
"errorDescription": "Card processing error. Please try a different card.",
"redirectUrlParams": "{}",
"splitBill": false,
"notes": "Payment attempt",
"metadata": {
"bookingId": "BK-42427",
"orderReference": "ORD-1"
}
}
```
# Refund Status Webhook Payload
Source: https://docs.paywithatoa.co.uk/api-reference/webhook/refund-status-webhook-response
Reference for the Atoa 'refund Status Webhook Response' webhook payload, the fields Atoa sends and an example JSON body to handle.
Delivered when a refund is completed, fails, or is cancelled. Excludes POS terminal voids, which are delivered under [`POS_PAYMENT_STATUS`](/api-reference/webhook/pos-payment-status-webhook-response).
| Status | Description |
| ------------- | ---------------------------------------------------------------------- |
| **COMPLETED** | Refund has been completed and credited to the customer's bank account. |
| **FAILED** | Refund could not be processed at this time. |
| **CANCELLED** | Refund has been cancelled by one of your users. |
**V2 Webhook Signing:** If you have enabled [V2 webhook signing](/api-reference/webhook/v2-signing), the `signatureHash` field is removed from the payload and an `eventType` field is added. The webhook signature is delivered in the `X-Atoa-Signature` HTTP header instead.
```json Completed theme={null}
{
"refundId": "c4e92a73-5f12-47c9-8b31-6e54f2d8a91c",
"paymentRequestId": "8f2471db-3456-9cd8-eff2-1a3b5c7d9e0f",
"paymentIdempotencyId": "ATOA1893245678912",
"status": "COMPLETED",
"paidAmount": 785.25,
"refundAmount": 50.75,
"currency": "GBP",
"refundNotes": "Customer requested partial refund",
"orderId": "892365417",
"createdAt": "2025-04-12T09:23:45.612Z",
"updatedAt": "2025-04-12T09:25:18.743Z",
"signatureHash": "9a2b8c4d6e3f1a5b7c9d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2b",
"metadata": {
"refundReason": "damaged-item",
"ticketId": "SUP-9981"
}
}
```
```json Failed theme={null}
{
"refundId": "c4e92a73-5f12-47c9-8b31-6e54f2d8a91c",
"paymentRequestId": "8f2471db-3456-9cd8-eff2-1a3b5c7d9e0f",
"paymentIdempotencyId": "ATOA1893245678912",
"status": "FAILED",
"paidAmount": 785.25,
"refundAmount": 50.75,
"currency": "GBP",
"refundNotes": "Customer requested partial refund",
"orderId": "892365417",
"createdAt": "2025-04-12T09:23:45.612Z",
"updatedAt": "2025-04-12T09:25:18.743Z",
"errorMessage": "The customerβs bank rejected the refund. Please try again.",
"signatureHash": "9a2b8c4d6e3f1a5b7c9d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2b",
"metadata": {
"refundReason": "damaged-item",
"ticketId": "SUP-9981"
}
}
```
```json Cancelled theme={null}
{
"refundId": "c4e92a73-5f12-47c9-8b31-6e54f2d8a91c",
"paymentRequestId": "8f2471db-3456-9cd8-eff2-1a3b5c7d9e0f",
"paymentIdempotencyId": "ATOA1893245678912",
"status": "CANCELLED",
"paidAmount": 785.25,
"refundAmount": 50.75,
"currency": "GBP",
"refundNotes": "Customer requested partial refund",
"orderId": "892365417",
"createdAt": "2025-04-12T09:23:45.612Z",
"updatedAt": "2025-04-12T09:25:18.743Z",
"signatureHash": "9a2b8c4d6e3f1a5b7c9d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2b",
"metadata": {
"refundReason": "damaged-item",
"ticketId": "SUP-9981"
}
}
```
For V1 signing, `signatureHash` is computed from `orderId` and `refundId` β see the [V1 Signing guide](/api-reference/webhook/v1-signing). With [V2 signing](/api-reference/webhook/v2-signing) enabled, `signatureHash` is removed and replaced by the `X-Atoa-Signature` header.
Only present when metadata was supplied on the refund. See [Initiate Refund](/api-reference/refund/initiate-refund) for the field limits.
# Trigger test webhook event
Source: https://docs.paywithatoa.co.uk/api-reference/webhook/test-webhook-event
Post https://api.atoa.me/api/webhook/test
Send a test webhook event via the Atoa Webhooks API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Sends a test webhook to your sandbox URL. Body and signature match a production event β verify your endpoint without creating a real payment.
**Sandbox-only** β production keys are rejected. Delivers to all sandbox endpoints subscribed to the specified `eventType`.
### Authorization
Bearer ``
**Request Body Schema**
The webhook event to simulate.
* `PAYMENTS_STATUS` β payment-status webhook (pay-by-bank or card)
* `EXPIRED_STATUS` β payment expired (customer didn't pay in time)
* `REFUND_STATUS` β refund-status webhook
* `POS_PAYMENT_STATUS` β POS event β choose which event to simulate via `type`
Override the `orderId`. Defaults to a generated test value.
Override the `paidAmount` in pounds (e.g. `10.05` for Β£10.05). Defaults to a test value.
Override the `paymentMethod`.
* `CARD`
* `PAY_BY_BANK`
Override the `status`. Valid values depend on `eventType`:
* `PAYMENTS_STATUS` β `COMPLETED` | `AUTHORIZED` (CARD only) | `FAILED`
* `REFUND_STATUS` β `COMPLETED` | `CANCELLED` | `FAILED`
* `EXPIRED_STATUS` β ignored (always emits `EXPIRED`)
* `POS_PAYMENT_STATUS` β depends on `type` (see below)
**`POS_PAYMENT_STATUS` only** β selects which POS event to simulate.
* `PAYMENTS_STATUS` β a completed POS payment (default)
* `REFUND_STATUS` β a POS refund
* `EXPIRED_STATUS` β an expired POS payment
Rejected for non-POS events.
**`POS_PAYMENT_STATUS` only** β custom field entries for the payload. Defaults to a test fixture. Rejected for non-POS events.
The custom field value (e.g. a customer ID).
The label for the field (e.g. "Customer ID").
**Response**
Confirmation that the test webhook was dispatched. Payload arrives at your registered URL asynchronously.
```bash cURL theme={null}
curl --request POST \
--url https://api.atoa.me/api/webhook/test \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"eventType": "PAYMENTS_STATUS"
}'
```
```bash cURL (override fields) theme={null}
curl --request POST \
--url https://api.atoa.me/api/webhook/test \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"eventType": "PAYMENTS_STATUS",
"orderId": "test-order-001",
"amount": 10.05,
"paymentMethod": "CARD",
"status": "AUTHORIZED"
}'
```
```bash cURL (POS event) theme={null}
curl --request POST \
--url https://api.atoa.me/api/webhook/test \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"eventType": "POS_PAYMENT_STATUS",
"type": "PAYMENTS_STATUS",
"status": "COMPLETED",
"customFields": [{ "value": "CUST_001", "fieldName": "Customer ID" }]
}'
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/webhook/test"
payload = {
"eventType": "PAYMENTS_STATUS",
"orderId": "test-order-001",
"amount": 10.05,
"paymentMethod": "CARD",
"status": "AUTHORIZED"
}
headers = {
"Authorization": "Bearer ",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
```
```javascript JavaScript theme={null}
const response = await fetch("https://api.atoa.me/api/webhook/test", {
method: "POST",
headers: {
Authorization: "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
eventType: "PAYMENTS_STATUS",
orderId: "test-order-001",
amount: 10.05,
paymentMethod: "CARD",
status: "AUTHORIZED",
}),
});
const data = await response.json();
console.log(data);
```
```json 200 theme={null}
{
"message": "Test webhook triggered successfully."
}
```
```json 400 (production token) theme={null}
{
"name": "BAD_REQUEST",
"message": "Webhook test triggers are only supported for sandbox tokens.",
"status": 400
}
```
```json 400 (invalid status for event) theme={null}
{
"name": "BAD_REQUEST",
"message": "status=\"AUTHORIZED\" is not valid for REFUND_STATUS. Valid: COMPLETED, CANCELLED, FAILED.",
"status": 400,
"errors": []
}
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": []
}
```
```json 404 (no subscription found) theme={null}
{
"name": "NOT_FOUND",
"message": "No webhook subscription is configured for this merchant and event.",
"status": 404
}
```
# V1 Webhook Signing (Legacy)
Source: https://docs.paywithatoa.co.uk/api-reference/webhook/v1-signing
How to verify Atoa v1 webhook signatures so you can trust incoming events, with the signing scheme and example code.
V1 signing verifies webhook authenticity using a `signatureHash` field in the JSON body, signing only the `orderId` combined with `paymentRequestId` (payments) or `refundId` (refunds).
**V2 signing is recommended for all new integrations.** V2 signs the entire request body and uses a dedicated signing secret. See the [V2 Webhook Signing guide](/api-reference/webhook/v2-signing) for setup instructions.
***
## How v1 signature verification works
| | Details |
| ---------------------- | --------------------------------------------------------------------------- |
| **Signature location** | `signatureHash` field in the JSON body |
| **What is signed** | `orderId \| paymentRequestId` (payments) or `orderId \| refundId` (refunds) |
| **Algorithm** | HMAC-SHA256 |
| **Key** | Your Atoa API secret (the same secret used for API authentication) |
***
## Payment signature verification
To verify a payment webhook:
1. **Retrieve the `orderId`** from your server β the one you passed when creating the payment request (not `atoaOrderId`).
2. **Get the `paymentRequestId`** from the webhook payload.
3. **Compute** `HMAC-SHA256(orderId + "|" + paymentRequestId, atoaSecret)`.
4. **Compare** the result against the `signatureHash` field in the payload.
```javascript Node.js theme={null}
const crypto = require('crypto');
function verifyV1Signature(orderId, paymentRequestId, atoaSecret, signatureHash) {
const data = orderId + '|' + paymentRequestId;
const generatedSignature = crypto
.createHmac('sha256', atoaSecret)
.update(data)
.digest('hex');
return generatedSignature === signatureHash;
}
// Usage
app.post('/webhook', express.json(), (req, res) => {
const { orderId, paymentRequestId, signatureHash } = req.body;
const yourOrderId = getOrderIdFromYourServer(); // Your original orderId, not atoaOrderId
if (!verifyV1Signature(yourOrderId, paymentRequestId, process.env.ATOA_SECRET, signatureHash)) {
return res.status(401).send('Invalid signature');
}
// Process the event...
res.status(200).send('OK');
});
```
```python Python theme={null}
import hmac
import hashlib
def verify_v1_signature(order_id, payment_request_id, atoa_secret, signature_hash):
data = f"{order_id}|{payment_request_id}"
generated_signature = hmac.new(
atoa_secret.encode("utf-8"),
data.encode("utf-8"),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(generated_signature, signature_hash)
# Flask example
@app.route("/webhook", methods=["POST"])
def webhook():
event = request.get_json()
your_order_id = get_order_id_from_your_server() # Your original orderId, not atoaOrderId
if not verify_v1_signature(
your_order_id, event["paymentRequestId"], ATOA_SECRET, event["signatureHash"]
):
return "Invalid signature", 401
# Process the event...
return "OK", 200
```
```java Java theme={null}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
public class V1WebhookVerifier {
private static final String HMAC_SHA256 = "HmacSHA256";
public static boolean verifySignature(
String orderId, String paymentRequestId,
String atoaSecret, String signatureHash) {
String data = orderId + "|" + paymentRequestId;
try {
SecretKeySpec keySpec = new SecretKeySpec(
atoaSecret.getBytes("UTF-8"), HMAC_SHA256);
Mac mac = Mac.getInstance(HMAC_SHA256);
mac.init(keySpec);
byte[] hash = mac.doFinal(data.getBytes("UTF-8"));
StringBuilder hex = new StringBuilder();
for (byte b : hash) {
hex.append(String.format("%02x", b));
}
return MessageDigest.isEqual(
signatureHash.getBytes(), hex.toString().getBytes());
} catch (Exception e) {
return false;
}
}
}
```
```go Go theme={null}
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func verifyV1Signature(orderId, paymentRequestId, atoaSecret, signatureHash string) bool {
data := orderId + "|" + paymentRequestId
mac := hmac.New(sha256.New, []byte(atoaSecret))
mac.Write([]byte(data))
generatedSignature := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(generatedSignature), []byte(signatureHash))
}
```
```php PHP theme={null}
***
## Refund signature verification
For refund webhooks, the signature uses `refundId` instead of `paymentRequestId`:
```
hmac_sha256(orderId + "|" + refundId, atoaSecret)
```
The verification logic is identical β just substitute `paymentRequestId` with `refundId` from the refund webhook payload.
```javascript Node.js theme={null}
function verifyRefundSignature(orderId, refundId, atoaSecret, signatureHash) {
const data = orderId + '|' + refundId;
const generatedSignature = crypto
.createHmac('sha256', atoaSecret)
.update(data)
.digest('hex');
return generatedSignature === signatureHash;
}
```
***
## Why V2 is recommended
V2 signing improves on V1 in several ways:
* **Full-body coverage** β V2 signs the entire request body, protecting all fields in the payload.
* **Dedicated signing secret** β V2 uses a separate signing secret rather than your API key, so your credentials stay compartmentalised.
* **POS support** β `POS_PAYMENT_STATUS` webhooks require V2 signing.
***
## Migration to V2
To migrate from V1 to V2 signing:
1. Generate a signing secret from the [Atoa Dashboard](https://dashboard.paywithatoa.co.uk/my-account/settings/webhooks) under **Settings β Webhooks**.
2. Update your webhook verification logic to use the `X-Atoa-Signature` header instead of the `signatureHash` body field.
3. Once a signing secret is generated, the `signatureHash` and `signature` fields are removed from webhook payloads and an `eventType` field is added.
See the [V2 Webhook Signing guide](/api-reference/webhook/v2-signing) for full setup instructions and code samples.
V1 and V2 cannot be used simultaneously for the same merchant. Generating a signing secret switches all your webhook deliveries to V2.
# V2 Webhook Signing
Source: https://docs.paywithatoa.co.uk/api-reference/webhook/v2-signing
How to verify Atoa v2 webhook signatures so you can trust incoming events, with the signing scheme and example code.
V2 signing verifies webhook authenticity with a single HMAC-SHA256 signature covering the entire request body, replacing the per-field `signatureHash` used in V1.
***
## How v2 signature verification works
Generating a signing secret switches all webhook deliveries to V2. Here is what changes:
| | V1 (Legacy) | V2 (Signing Secret) |
| ---------------------- | ------------------------------------------ | ---------------------------------------------------------- |
| **Signature location** | `signatureHash` field inside the JSON body | `X-Atoa-Signature` HTTP header |
| **What is signed** | `orderId \| paymentRequestId` | The entire JSON request body |
| **Algorithm** | HMAC-SHA256 | HMAC-SHA256 |
| **Extra body fields** | `signatureHash`, `signature` present | `eventType` added; `signatureHash` and `signature` removed |
When Atoa delivers a webhook to a V2-enabled endpoint:
1. The `signatureHash` and `signature` fields are removed from the body.
2. An `eventType` field is added to the body (e.g. `"PAYMENTS_STATUS"`, `"POS_PAYMENT_STATUS"`).
3. The full JSON body is signed: `HMAC-SHA256(signingSecret, requestBody)`.
4. The signature is sent in the `X-Atoa-Signature` header as `v1=`.
***
## Getting started
Go to the [Atoa Dashboard](https://dashboard.paywithatoa.co.uk/my-account/settings/webhooks) and navigate to **Settings β Webhooks**. Click **Generate a signing key**. You will be asked to confirm your password.
Once generated, the secret is displayed **once** in the format:
```
whsec_
```
Copy and store it securely. You will not be able to view it again.
Store the signing secret in an environment variable or a secrets manager. Never hard-code it in your source code or commit it to version control.
For every incoming webhook request, compute the expected signature and compare it to the value in the `X-Atoa-Signature` header. See the verification examples below.
***
## Verifying the signature
To verify an incoming webhook:
1. **Extract the signature** β Read the `X-Atoa-Signature` header value and strip the `v1=` prefix to get the received hex signature.
2. **Decode your signing secret** β The secret is in the format `whsec_`. Strip the `whsec_` prefix and base64-decode the remaining value to get the raw signing key bytes.
3. **Compute the expected signature** β Calculate `HMAC-SHA256(key = decoded secret bytes, data = raw request body)` and hex-encode the result.
4. **Compare signatures** β Use a timing-safe comparison to check the received signature against the expected signature. If they match, the webhook is authentic.
```javascript Node.js theme={null}
const crypto = require('crypto');
function verifyWebhookSignature(signingSecret, rawBody, signatureHeader) {
// signatureHeader is the value of X-Atoa-Signature, e.g. "v1=abc123..."
const receivedSig = signatureHeader.replace('v1=', '');
// Strip whsec_ prefix and base64-decode to get raw signing key
const secret = Buffer.from(signingSecret.split('_')[1], 'base64');
const expectedSig = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(receivedSig, 'hex'),
Buffer.from(expectedSig, 'hex')
);
}
// Express.js example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-atoa-signature'];
const rawBody = req.body.toString('utf8');
if (!verifyWebhookSignature(process.env.ATOA_SIGNING_SECRET, rawBody, signature)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(rawBody);
// Process the event...
res.status(200).send('OK');
});
```
```python Python theme={null}
import hmac
import hashlib
import base64
def verify_webhook_signature(signing_secret, raw_body, signature_header):
"""Verify the X-Atoa-Signature header."""
received_sig = signature_header.replace("v1=", "")
# Strip whsec_ prefix and base64-decode to get raw signing key
secret = base64.b64decode(signing_secret.split("_", 1)[1])
expected_sig = hmac.new(
secret,
raw_body.encode("utf-8"),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(received_sig, expected_sig)
# Flask example
@app.route("/webhook", methods=["POST"])
def webhook():
signature = request.headers.get("X-Atoa-Signature", "")
raw_body = request.get_data(as_text=True)
if not verify_webhook_signature(SIGNING_SECRET, raw_body, signature):
return "Invalid signature", 401
event = request.get_json()
# Process the event...
return "OK", 200
```
```java Java theme={null}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.Base64;
public class WebhookVerifier {
private static final String HMAC_SHA256 = "HmacSHA256";
public static boolean verifySignature(
String signingSecret, String rawBody, String signatureHeader) {
String receivedSig = signatureHeader.replace("v1=", "");
try {
// Strip whsec_ prefix and base64-decode to get raw signing key
byte[] secret = Base64.getDecoder().decode(
signingSecret.split("_", 2)[1]);
SecretKeySpec keySpec = new SecretKeySpec(secret, HMAC_SHA256);
Mac mac = Mac.getInstance(HMAC_SHA256);
mac.init(keySpec);
byte[] hash = mac.doFinal(rawBody.getBytes("UTF-8"));
StringBuilder hex = new StringBuilder();
for (byte b : hash) {
hex.append(String.format("%02x", b));
}
return MessageDigest.isEqual(
receivedSig.getBytes(), hex.toString().getBytes());
} catch (Exception e) {
return false;
}
}
}
```
```go Go theme={null}
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"io"
"net/http"
"strings"
)
func verifySignature(signingSecret, rawBody, signatureHeader string) bool {
receivedSig := strings.TrimPrefix(signatureHeader, "v1=")
// Strip whsec_ prefix and base64-decode to get raw signing key
parts := strings.SplitN(signingSecret, "_", 2)
secret, _ := base64.StdEncoding.DecodeString(parts[1])
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(rawBody))
expectedSig := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(receivedSig), []byte(expectedSig))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("X-Atoa-Signature")
body, _ := io.ReadAll(r.Body)
if !verifySignature(signingSecret, string(body), signature) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process the event...
w.WriteHeader(http.StatusOK)
}
```
```php PHP theme={null}
Always use a timing-safe comparison function (such as `crypto.timingSafeEqual`
in Node.js or `hmac.compare_digest` in Python) when comparing signatures.
This prevents timing attacks that could allow an attacker to guess the
signature byte by byte.
***
## Key rotation
You can rotate your signing secret at any time from the Atoa Dashboard under **Settings β Webhooks**. Rotation requires password confirmation.
When you rotate:
* A new secret is generated and displayed once β copy it immediately.
* The previous secret is **immediately invalidated**. All subsequent deliveries use the new secret.
* Update the secret in your server environment before confirming the rotation to avoid failed verifications.
***
## Backward compatibility
V2 signing is opt-in per merchant. Without a signing secret, webhooks continue in V1 format with `signatureHash` and `signature` fields. Generating a secret switches your deliveries to V2 without affecting other merchants.
Subscribing to the `POS_PAYMENT_STATUS` event type requires a V2 signing
secret. You must generate a signing secret before you can create a
`POS_PAYMENT_STATUS` webhook subscription.
Generating a signing secret switches **all** webhook deliveries to V2 β including those sent to your WooCommerce or Magento plugin. To keep plugin webhooks working:
1. Update the Atoa plugin to the latest version.
2. Paste your signing secret into the plugin's webhook signing key field and save.
See the [WooCommerce](/woo-commerce) or [Magento](/magento) setup guide for where to find this setting.
# Create Endpoint | Webhooks API
Source: https://docs.paywithatoa.co.uk/api-reference/webhook/v2/create-endpoint
Post https://api.atoa.me/api/webhook/v2/endpoints
Create endpoint via the Atoa Webhooks API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Register a webhook endpoint and subscribe to one or more event types in a single call. Use [List event types](/api-reference/webhook/v2/list-event-types) to get the full list of available event type names.
**Initial registration only.** Use this endpoint to register a new URL with its initial event subscriptions. To change events or authentication on an existing endpoint, use [Update endpoint](/api-reference/webhook/v2/update-endpoint). To change the URL, delete the endpoint and create a new one.
### Authorization
Bearer ``
**Request Body Schema**
The HTTPS endpoint URL that will receive webhook deliveries. Must return HTTP `200` on success β failures are retried with exponential back-off for up to 24 hours. **This URL cannot be changed after creation** β to use a different URL, delete the endpoint and create a new one.
One or more event types to subscribe to. Valid values: `PAYMENTS_STATUS`, `EXPIRED_STATUS`, `REFUND_STATUS`, `POS_PAYMENT_STATUS`, `PAYMENT_METHOD_STATUS`. Subscribing to `POS_PAYMENT_STATUS` requires a webhook signing key to be generated first.
Each event type supports a maximum of **3 registered endpoints**. If an event type already has 3 endpoints, adding another endpoint subscribed to that event returns a `400` error. Use [List endpoints](/api-reference/webhook/v2/list-endpoints) to check current registrations before creating.
Optional label for this endpoint (max 100 characters). Shown in the Atoa Dashboard to help identify the endpoint.
Optional. Credentials Atoa will include in every webhook delivery to authenticate against your server. Two methods are supported β choose one. Fields from different methods cannot be mixed.
Omit the `authentication` field entirely. No credentials will be sent with deliveries. Use [signature verification](/api-reference/webhook/introduction#signature-verification) to validate requests on your side.
Atoa includes an `Authorization: Basic ` header in every delivery request. Pass both fields together.
Username for HTTP Basic Authentication.
Password for HTTP Basic Authentication.
Before each delivery, Atoa calls your token endpoint using the `client_credentials` grant to obtain a short-lived access token, then includes it as `Authorization: Bearer ` in the delivery request. Pass all three fields together.
Client ID registered with your authorisation server.
Client secret for the above client ID.
Your token endpoint URL (e.g. `https://auth.your-server.com/oauth/token`). Atoa will POST to this URL to fetch an access token before each delivery.
**Response**
Unique identifier for the endpoint.
The registered endpoint URL.
The endpoint description, or `null` if not set.
`SANDBOX` or `PRODUCTION`, derived from the API key used.
Subscribed event types.
Event type name (e.g. `PAYMENTS_STATUS`).
Human-readable label (e.g. `Payment Status`).
Whether endpoint authentication is configured.
`BASIC`, `OAUTH2`, or `null` if no authentication is configured.
Consecutive failed deliveries since the last successful delivery. Resets to `0` on success.
ISO 8601 timestamp of the last successful delivery, or `null` if never delivered.
```bash No Auth theme={null}
curl --request POST \
--url https://api.atoa.me/api/webhook/v2/endpoints \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"url": "https://your-server.com/webhooks",
"events": ["PAYMENTS_STATUS", "REFUND_STATUS"],
"description": "Production orders endpoint"
}'
```
```bash Basic Auth theme={null}
curl --request POST \
--url https://api.atoa.me/api/webhook/v2/endpoints \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"url": "https://your-server.com/webhooks",
"events": ["PAYMENTS_STATUS", "REFUND_STATUS"],
"description": "Production orders endpoint",
"authentication": {
"username": "your-username",
"password": "your-password"
}
}'
```
```bash OAuth 2.0 theme={null}
curl --request POST \
--url https://api.atoa.me/api/webhook/v2/endpoints \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"url": "https://your-server.com/webhooks",
"events": ["PAYMENTS_STATUS", "REFUND_STATUS"],
"description": "Production orders endpoint",
"authentication": {
"clientId": "your-client-id",
"clientSecret": "your-client-secret",
"authUrl": "https://auth.your-server.com/oauth/token"
}
}'
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/webhook/v2/endpoints"
payload = {
"url": "https://your-server.com/webhooks",
"events": ["PAYMENTS_STATUS", "REFUND_STATUS"],
"description": "Production orders endpoint"
}
headers = {
"Authorization": "Bearer ",
"Content-Type": "application/json"
}
response = requests.request("POST", url, json=payload, headers=headers)
print(response.text)
```
```javascript JavaScript theme={null}
const options = {
method: 'POST',
headers: { Authorization: 'Bearer ', 'Content-Type': 'application/json' },
body: JSON.stringify({
url: 'https://your-server.com/webhooks',
events: ['PAYMENTS_STATUS', 'REFUND_STATUS'],
description: 'Production orders endpoint'
})
};
fetch('https://api.atoa.me/api/webhook/v2/endpoints', options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
```php PHP theme={null}
"https://api.atoa.me/api/webhook/v2/endpoints",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
"url" => "https://your-server.com/webhooks",
"events" => ["PAYMENTS_STATUS", "REFUND_STATUS"],
"description" => "Production orders endpoint"
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer ",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
```
```go Go theme={null}
package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.atoa.me/api/webhook/v2/endpoints"
payload := strings.NewReader(`{
"url": "https://your-server.com/webhooks",
"events": ["PAYMENTS_STATUS", "REFUND_STATUS"],
"description": "Production orders endpoint"
}`)
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer ")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.post("https://api.atoa.me/api/webhook/v2/endpoints")
.header("Authorization", "Bearer ")
.header("Content-Type", "application/json")
.body("{\n \"url\": \"https://your-server.com/webhooks\",\n \"events\": [\"PAYMENTS_STATUS\", \"REFUND_STATUS\"],\n \"description\": \"Production orders endpoint\"\n}")
.asString();
```
```json 200 theme={null}
{
"id": "4a124f25-fe8d-47a0-92c8-16d94cbfe24c",
"url": "https://your-server.com/webhooks",
"description": "Production orders endpoint",
"environment": "PRODUCTION",
"events": [
{ "event": "PAYMENTS_STATUS", "label": "Payment Status" },
{ "event": "REFUND_STATUS", "label": "Refund Status" }
],
"hasAuthentication": false,
"authenticationType": null,
"failureCount": 0,
"lastDeliveredAt": null
}
```
```json 400 (events required) theme={null}
{
"name": "BAD_REQUEST",
"message": "At least one event type is required.",
"status": 400
}
```
```json 400 (duplicate events) theme={null}
{
"name": "BAD_REQUEST",
"message": "Duplicate event types are not allowed.",
"status": 400
}
```
```json 400 (URL not HTTPS) theme={null}
{
"name": "BAD_REQUEST",
"message": "Endpoint URL must use HTTPS.",
"status": 400
}
```
```json 400 (URL is localhost) theme={null}
{
"name": "BAD_REQUEST",
"message": "Endpoint URL cannot point to localhost.",
"status": 400
}
```
```json 400 (URL is IP address) theme={null}
{
"name": "BAD_REQUEST",
"message": "Endpoint URL cannot use an IP address.",
"status": 400
}
```
```json 400 (URL already registered) theme={null}
{
"name": "BAD_REQUEST",
"message": "This webhook URL is already registered. Use the update API to modify the events or authentication details for this endpoint.",
"status": 400
}
```
```json 400 (capacity exceeded) theme={null}
{
"name": "BAD_REQUEST",
"message": "Payment Status has reached the maximum limit of 3 endpoints per event type.",
"status": 400
}
```
```json 400 (POS without signing secret) theme={null}
{
"name": "BAD_REQUEST",
"message": "POS Payment Status requires V2 signing. Please generate a signing secret from the dashboard to proceed.",
"status": 400
}
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": "[]"
}
```
# Delete Endpoint | Webhooks API
Source: https://docs.paywithatoa.co.uk/api-reference/webhook/v2/delete-endpoint
Delete https://api.atoa.me/api/webhook/v2/endpoints/{endpointId}
Delete endpoint via the Atoa Webhooks API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Permanently deletes a webhook endpoint and all its event subscriptions. Webhook deliveries stop immediately.
### Authorization
Bearer ``
**Path Parameters**
The unique identifier of the endpoint to delete.
**Response**
Returns `200 OK` with a JSON body on success.
```bash cURL theme={null}
curl --request DELETE \
--url https://api.atoa.me/api/webhook/v2/endpoints/4a124f25-fe8d-47a0-92c8-16d94cbfe24c \
--header 'Authorization: Bearer '
```
```python Python theme={null}
import requests
endpoint_id = "4a124f25-fe8d-47a0-92c8-16d94cbfe24c"
url = f"https://api.atoa.me/api/webhook/v2/endpoints/{endpoint_id}"
headers = { "Authorization": "Bearer " }
response = requests.request("DELETE", url, headers=headers)
print(response.text)
```
```javascript JavaScript theme={null}
const endpointId = "4a124f25-fe8d-47a0-92c8-16d94cbfe24c";
fetch(`https://api.atoa.me/api/webhook/v2/endpoints/${endpointId}`, {
method: 'DELETE',
headers: { Authorization: 'Bearer ' }
})
.then(response => console.log(response.status)) // 200 on success
.catch(err => console.error(err));
```
```php PHP theme={null}
"https://api.atoa.me/api/webhook/v2/endpoints/{$endpointId}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer "
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
```
```go Go theme={null}
package main
import (
"fmt"
"net/http"
"io"
)
func main() {
endpointId := "4a124f25-fe8d-47a0-92c8-16d94cbfe24c"
url := "https://api.atoa.me/api/webhook/v2/endpoints/" + endpointId
req, _ := http.NewRequest("DELETE", url, nil)
req.Header.Add("Authorization", "Bearer ")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.delete("https://api.atoa.me/api/webhook/v2/endpoints/{endpointId}")
.header("Authorization", "Bearer ")
.routeParam("endpointId", "4a124f25-fe8d-47a0-92c8-16d94cbfe24c")
.asString();
```
```json 200 theme={null}
{
"success": true,
"message": "Webhook endpoint deleted successfully"
}
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": "[]"
}
```
```json 404 theme={null}
{
"name": "NOT_FOUND",
"message": "Webhook not found",
"status": 404
}
```
`true` when the endpoint was successfully deleted.
Webhook endpoint deleted successfully
# List Endpoints | Webhooks API
Source: https://docs.paywithatoa.co.uk/api-reference/webhook/v2/list-endpoints
Get https://api.atoa.me/api/webhook/v2/endpoints
List endpoints via the Atoa Webhooks API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Returns a paginated list of all webhook endpoints for your business account. The environment (`SANDBOX` or `PRODUCTION`) is determined by the API key used β you will only see endpoints for that environment.
### Authorization
Bearer ``
**Query Parameters**
Zero-based page number. Defaults to `0`.
Number of items per page (max 50). Defaults to `10`.
**Response**
Array of endpoint objects. See [Create endpoint](/api-reference/webhook/v2/create-endpoint#response) for the full endpoint shape.
Total number of endpoints for your account in this environment across all pages.
Current zero-based page number.
Page size used for this response.
```bash cURL theme={null}
curl --request GET \
--url 'https://api.atoa.me/api/webhook/v2/endpoints?page=0&size=10' \
--header 'Authorization: Bearer '
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/webhook/v2/endpoints"
params = { "page": 0, "size": 10 }
headers = { "Authorization": "Bearer " }
response = requests.request("GET", url, headers=headers, params=params)
print(response.text)
```
```javascript JavaScript theme={null}
const params = new URLSearchParams({ page: 0, size: 10 });
fetch(`https://api.atoa.me/api/webhook/v2/endpoints?${params}`, {
method: 'GET',
headers: { Authorization: 'Bearer ' }
})
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
```php PHP theme={null}
"https://api.atoa.me/api/webhook/v2/endpoints?page=0&size=10",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer "
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
```
```go Go theme={null}
package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.atoa.me/api/webhook/v2/endpoints?page=0&size=10"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer ")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.get("https://api.atoa.me/api/webhook/v2/endpoints")
.header("Authorization", "Bearer ")
.queryString("page", 0)
.queryString("size", 10)
.asString();
```
```json 200 theme={null}
{
"data": [
{
"id": "4a124f25-fe8d-47a0-92c8-16d94cbfe24c",
"url": "https://your-server.com/webhooks",
"description": "Production orders endpoint",
"environment": "PRODUCTION",
"events": [
{ "event": "PAYMENTS_STATUS", "label": "Payment Status" },
{ "event": "REFUND_STATUS", "label": "Refund Status" }
],
"hasAuthentication": false,
"authenticationType": null,
"failureCount": 0,
"lastDeliveredAt": "2025-06-01T12:00:00.000Z"
}
],
"totalCount": 1,
"page": 0,
"size": 10
}
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": "[]"
}
```
# List Event Types | Webhooks API
Source: https://docs.paywithatoa.co.uk/api-reference/webhook/v2/list-event-types
Get https://api.atoa.me/api/webhook/v2/event-types
List event types via the Atoa Webhooks API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Returns all available event types that can be used when creating or updating a webhook endpoint.
### Authorization
Bearer ``
**Response**
Array of event type objects.
Event type name to use in create/update requests (e.g. `PAYMENTS_STATUS`).
Human-readable label (e.g. `Payment Status`).
A short description of when this event is triggered.
When `true`, a webhook signing key must be generated before subscribing to this event type. See [Webhook Signing Key](/api-reference/webhook/introduction#signature-verification).
```bash cURL theme={null}
curl --request GET \
--url https://api.atoa.me/api/webhook/v2/event-types \
--header 'Authorization: Bearer '
```
```python Python theme={null}
import requests
url = "https://api.atoa.me/api/webhook/v2/event-types"
headers = { "Authorization": "Bearer " }
response = requests.request("GET", url, headers=headers)
print(response.text)
```
```javascript JavaScript theme={null}
fetch('https://api.atoa.me/api/webhook/v2/event-types', {
method: 'GET',
headers: { Authorization: 'Bearer ' }
})
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
```php PHP theme={null}
"https://api.atoa.me/api/webhook/v2/event-types",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer "
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
```
```go Go theme={null}
package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.atoa.me/api/webhook/v2/event-types"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer ")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.get("https://api.atoa.me/api/webhook/v2/event-types")
.header("Authorization", "Bearer ")
.asString();
```
```json 200 theme={null}
[
{ "event": "PAYMENTS_STATUS", "label": "Payment Status", "description": "Triggered when a payment is completed successfully.", "requiresSigningSecret": false },
{ "event": "EXPIRED_STATUS", "label": "Expired Status", "description": "Triggered when a payment request expires without completion.", "requiresSigningSecret": false },
{ "event": "REFUND_STATUS", "label": "Refund Status", "description": "Triggered when a refund is initiated or completed.", "requiresSigningSecret": false },
{ "event": "POS_PAYMENT_STATUS", "label": "POS Payment Status", "description": "Triggered when a POS payment status changes. Requires a webhook signing secret.", "requiresSigningSecret": true },
{ "event": "PAYMENT_METHOD_STATUS", "label": "Payment Method Status", "description": "Triggered when a customer's card is saved for future payments or removed from their saved cards", "requiresSigningSecret": false }
]
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": "[]"
}
```
# Update Endpoint | Webhooks API
Source: https://docs.paywithatoa.co.uk/api-reference/webhook/v2/update-endpoint
Put https://api.atoa.me/api/webhook/v2/endpoints/{endpointId}
Update endpoint via the Atoa Webhooks API, request parameters, response schema and code samples in cURL, Python, JavaScript, PHP, Go and Java.
Update an existing webhook endpoint's description, event subscriptions, or authentication credentials.
The endpoint URL cannot be changed after creation. To use a different URL, delete the endpoint and create a new one.
### Authorization
Bearer ``
**Path Parameters**
The unique identifier of the endpoint to update.
**Request Body Schema**
The complete set of event types to subscribe to. This **replaces** the existing subscriptions β any event not included is removed. At least one event is required.
If you are adding a new event type to this endpoint, each event type supports a maximum of **3 registered endpoints** across your account. Adding an event that already has 3 other endpoints subscribed returns a `400` error. Use [List endpoints](/api-reference/webhook/v2/list-endpoints) to check current registrations before updating.
Updated description for this endpoint (max 100 characters). Omit to leave the current description unchanged.
Optional. Updated authentication configuration. Two methods are supported β choose one. Fields from different methods cannot be mixed. Pass `null` explicitly to remove existing authentication. Omit the field entirely to leave authentication unchanged.
Pass `"authentication": null` to remove any existing authentication from the endpoint.
Atoa includes an `Authorization: Basic ` header in every delivery request. Pass both fields together.
Username for HTTP Basic Authentication.
Password for HTTP Basic Authentication.
Before each delivery, Atoa calls your token endpoint using the `client_credentials` grant to obtain a short-lived access token, then includes it as `Authorization: Bearer ` in the delivery request. Pass all three fields together.
Client ID registered with your authorisation server.
Client secret for the above client ID.
Your token endpoint URL (e.g. `https://auth.your-server.com/oauth/token`). Atoa will POST to this URL to fetch an access token before each delivery.
**Response**
Returns the updated endpoint object. Same shape as [Create endpoint](/api-reference/webhook/v2/create-endpoint#response).
```bash No Auth theme={null}
curl --request PUT \
--url https://api.atoa.me/api/webhook/v2/endpoints/4a124f25-fe8d-47a0-92c8-16d94cbfe24c \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"events": ["PAYMENTS_STATUS", "REFUND_STATUS", "EXPIRED_STATUS"],
"description": "Updated production endpoint"
}'
```
```bash Basic Auth theme={null}
curl --request PUT \
--url https://api.atoa.me/api/webhook/v2/endpoints/4a124f25-fe8d-47a0-92c8-16d94cbfe24c \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"events": ["PAYMENTS_STATUS", "REFUND_STATUS", "EXPIRED_STATUS"],
"authentication": {
"username": "your-username",
"password": "your-password"
}
}'
```
```bash OAuth 2.0 theme={null}
curl --request PUT \
--url https://api.atoa.me/api/webhook/v2/endpoints/4a124f25-fe8d-47a0-92c8-16d94cbfe24c \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"events": ["PAYMENTS_STATUS", "REFUND_STATUS", "EXPIRED_STATUS"],
"authentication": {
"clientId": "your-client-id",
"clientSecret": "your-client-secret",
"authUrl": "https://auth.your-server.com/oauth/token"
}
}'
```
```bash Remove Auth theme={null}
curl --request PUT \
--url https://api.atoa.me/api/webhook/v2/endpoints/4a124f25-fe8d-47a0-92c8-16d94cbfe24c \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"events": ["PAYMENTS_STATUS", "REFUND_STATUS", "EXPIRED_STATUS"],
"authentication": null
}'
```
```python Python theme={null}
import requests
endpoint_id = "4a124f25-fe8d-47a0-92c8-16d94cbfe24c"
url = f"https://api.atoa.me/api/webhook/v2/endpoints/{endpoint_id}"
payload = {
"events": ["PAYMENTS_STATUS", "REFUND_STATUS", "EXPIRED_STATUS"],
"description": "Updated production endpoint"
}
headers = {
"Authorization": "Bearer ",
"Content-Type": "application/json"
}
response = requests.request("PUT", url, json=payload, headers=headers)
print(response.text)
```
```javascript JavaScript theme={null}
const endpointId = "4a124f25-fe8d-47a0-92c8-16d94cbfe24c";
const options = {
method: 'PUT',
headers: { Authorization: 'Bearer ', 'Content-Type': 'application/json' },
body: JSON.stringify({
events: ['PAYMENTS_STATUS', 'REFUND_STATUS', 'EXPIRED_STATUS'],
description: 'Updated production endpoint'
})
};
fetch(`https://api.atoa.me/api/webhook/v2/endpoints/${endpointId}`, options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
```php PHP theme={null}
"https://api.atoa.me/api/webhook/v2/endpoints/{$endpointId}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
"events" => ["PAYMENTS_STATUS", "REFUND_STATUS", "EXPIRED_STATUS"],
"description" => "Updated production endpoint"
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer ",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
```
```go Go theme={null}
package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
endpointId := "4a124f25-fe8d-47a0-92c8-16d94cbfe24c"
url := "https://api.atoa.me/api/webhook/v2/endpoints/" + endpointId
payload := strings.NewReader(`{
"events": ["PAYMENTS_STATUS", "REFUND_STATUS", "EXPIRED_STATUS"],
"description": "Updated production endpoint"
}`)
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("Authorization", "Bearer ")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.put("https://api.atoa.me/api/webhook/v2/endpoints/{endpointId}")
.header("Authorization", "Bearer ")
.header("Content-Type", "application/json")
.routeParam("endpointId", "4a124f25-fe8d-47a0-92c8-16d94cbfe24c")
.body("{\n \"events\": [\"PAYMENTS_STATUS\", \"REFUND_STATUS\", \"EXPIRED_STATUS\"],\n \"description\": \"Updated production endpoint\"\n}")
.asString();
```
```json 200 theme={null}
{
"id": "4a124f25-fe8d-47a0-92c8-16d94cbfe24c",
"url": "https://your-server.com/webhooks",
"description": "Updated production endpoint",
"environment": "PRODUCTION",
"events": [
{ "event": "PAYMENTS_STATUS", "label": "Payment Status" },
{ "event": "REFUND_STATUS", "label": "Refund Status" },
{ "event": "EXPIRED_STATUS", "label": "Expired Status" }
],
"hasAuthentication": false,
"authenticationType": null,
"failureCount": 0,
"lastDeliveredAt": "2025-06-01T12:00:00.000Z"
}
```
```json 400 (events required) theme={null}
{
"name": "BAD_REQUEST",
"message": "At least one event type is required.",
"status": 400
}
```
```json 400 (duplicate events) theme={null}
{
"name": "BAD_REQUEST",
"message": "Duplicate event types are not allowed.",
"status": 400
}
```
```json 400 (capacity exceeded) theme={null}
{
"name": "BAD_REQUEST",
"message": "Payment Status has reached the maximum limit of 3 endpoints per event type.",
"status": 400
}
```
```json 400 (POS without signing secret) theme={null}
{
"name": "BAD_REQUEST",
"message": "POS Payment Status requires V2 signing. Please generate a signing secret from the dashboard to proceed.",
"status": 400
}
```
```json 401 theme={null}
{
"name": "UNAUTHORIZED",
"message": "Unauthorized",
"status": 401,
"errors": "[]"
}
```
```json 404 theme={null}
{
"name": "NOT_FOUND",
"message": "Webhook not found",
"status": 404
}
```
# Atoa Sandbox: Test Payments Before Go-Live
Source: https://docs.paywithatoa.co.uk/atoa-sandbox
Test Atoa end to end in the sandbox before going live, simulate Pay by Bank and card flows using test credentials and banks.
The Sandbox environment lets you develop and test your integration without touching live payments.
## Sandbox Environment
Atoa's sandbox environment is a self-contained environment that simulates the live production environment. The sandbox provides a space where you can initiate, experiment and watch transactions without touching a live bank account. We highly recommend that you use the sandbox before generating the production keys.
The sandbox mirrors all the features of the live environment but has some minor differences that make it easy to test flows. Please follow the sandbox guide to know more.
## Atoa Sandbox environment guide
The Sandbox serves as a testing environment enabling merchants to simulate the end-to-end payment process. Merchants can use sandbox credentials to simulate different scenarios, such as successful payments, failed payments, and pending payments.
Refer to the [getting started guide](introduction) for signup, obtaining an
access token, and creating a payment request. Then, proceed with the steps
below to simulate different payment scenarios.
**Step 1:** Generate a Payment Request using sandbox access.
**Step 2:** Open the payment URL or scan the QR code URL using your phoneβs camera.
**Step 3:** Simulate the Payment Scenarios.
The actual sandbox provided by the bank may not work sometimes, so we prefer
using the **Atoa Test bank** to simulate all scenarios, such as successful,
failed, or canceled payments, as given in the table below.
| Payment Scenario | Description |
| ---------------- | -------------------------------------------------------------------------------------------------------------- |
| COMPLETED | shows that the payment has been processed successfully. Funds have been transferred to the merchant's account. |
| PENDING | shows that the payment is currently in progress and awaiting final confirmation from the bank. |
| FAILED | Unsuccessful transactions are marked as failed. The customer will have to retry the payment. |
## Simulating Payment Scenarios in Sandbox
You can simulate all payment outcomes directly using the **Atoa Test Bank**.
**Steps:**
1. Select **Atoa Test Bank** when prompted to choose a bank.
2. After redirection, you will see the **Select Payment Status** screen.
3. Use the dropdown to choose the desired payment outcome:
* **COMPLETED** β to simulate a successful payment
* **FAILED** β to simulate a failed payment
* **PENDING** β to simulate a pending payment
4. After selecting the status, click **Submit** to complete the simulation.
# Atoa CLI | Payments from Your Terminal
Source: https://docs.paywithatoa.co.uk/cli
The Atoa CLI manages payments, customers and webhooks from your terminal. Install it, authenticate and run commands, with examples.
The **Atoa CLI** is a command-line tool for interacting with the Atoa payment API from your terminal. Create payments, manage customers, test webhooks, and inspect transactions β without leaving the shell, and without writing a script.
It's scriptable for CI/CD, automated test pipelines, support workflows, and one-off operations.
**Source code:** [github.com](https://github.com/ATOAPaymentsLimited/Atoa-CLI)
`atoa login` opens the dashboard, you approve, done. No token to copy-paste.
`atoa webhooks trigger PAYMENTS_STATUS` fires a fake event at your sandbox URL β no real payment needed.
Your browser login runs account commands; an SDK key runs payments/data commands.
TAB-completes commands, flags, profile names, and key IDs across bash, zsh, and PowerShell.
***
## Prerequisites
* **Node.js 20 or later** β verify with `node --version`
* An **Atoa merchant account** and a **desktop browser** on the same machine (login is browser-based). No account yet? Run `atoa signup`, or see the [Getting Started](/introduction#step-1-sign-up-for-developer-access) guide.
***
## Installation
```bash theme={null}
npm install -g @atoapayments/atoa-cli
```
```bash theme={null}
yarn global add @atoapayments/atoa-cli
```
```bash theme={null}
pnpm add -g @atoapayments/atoa-cli
```
Verify the install:
```bash theme={null}
atoa --version
```
The CLI stores credentials locally under `~/.atoa/auth/`, readable only by
your user account.
***
## First-time setup
Pair the CLI with your Atoa account through your browser:
```bash theme={null}
atoa login
```
Your default browser opens the Atoa dashboard, you approve the request, and the CLI is logged in. If your account has multiple businesses, you pick one (or run `atoa business use ` afterwards). A **profile** is created, named after your business.
`atoa login` requires an interactive terminal (TTY) and a desktop browser
on the same machine β there is **no headless/CI login path** (see
[CI / automation](#using-the-cli-in-ci)).
No account yet? `atoa signup` walks you through account creation (email + one-time code) and business onboarding β no prior login needed.
### Confirm you're logged in
```bash theme={null}
atoa whoami
```
```
Signed in as Acme Ltd
Email jane@example.com
Profile acme
```
***
## The dual-credential model
The CLI holds **two independent credential types** per profile:
```
atoa login (browser)
β
βββΊ Login session βββΊ account-management commands
β business, sessions, keys, staff, roles,
β kyb, payment-links, bank, stores,
β addons, comms, custom-branding,
β custom-sms, direct-debit
β
βββΊ SDK API key (optional, via `atoa keys create`)
βββΊ payments / data commands
payments, refunds, customers, payment-methods,
card-on-file, webhooks, bank-feed, payouts, institutions,
get, post, delete
```
A single `atoa login` always creates the **login session**. The **SDK key** is optional β add it whenever you need the payments/data commands:
```bash theme={null}
atoa keys create --env sandbox # mints a revocable SDK key
```
`atoa keys create` requires an admin role on the business. The `apiSecret` is stored locally under `~/.atoa/auth/`, so the CLI picks it up automatically on later commands.
### Which login does each command need?
| Auth required | Commands |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Login session** (`atoa login`) | `business`, `sessions`, `keys create/list`, `kyb`, `staff`, `roles`, `payment-links`, `bank`, `stores`, `addons`, `comms`, `custom-branding`, `custom-sms`, `direct-debit`, `google` |
| **SDK key** (`atoa keys create`) | `payments`, `refunds`, `customers`, `payment-methods`, `card-on-file`, `webhooks`, `bank-feed`, `payouts`, `institutions`, `get`, `post`, `delete` |
The generic verbs `atoa get/post/delete` authenticate with the **SDK key**, not
the browser session β they're for calling the payments API directly, so
`atoa keys create` must have been run first.
***
## Environments and profiles
A single machine can hold credentials for **many businesses** (profiles) **and both environments** (sandbox + production). The CLI keeps them separate so you can't accidentally run a production refund while testing locally.
```bash theme={null}
atoa profile list # see every profile and which envs are configured
atoa profile use acme # switch active profile
atoa whoami # confirm which account and profile are active
```
SDK keys **are** per-env and **not interchangeable** β a sandbox key won't
work against production. Mint one per env with `atoa keys create --env `.
***
## Per-profile settings
CLI state is stored entirely under profiles. To change which env a profile defaults to, use `atoa profile set`. To wipe everything, use `atoa reset`.
### Inspect state
```bash theme={null}
atoa profile list # every profile + which envs each one has
atoa profile show # full metadata for the active profile
atoa profile show acme # metadata for a specific profile
```
### Change the default env on a profile
```bash theme={null}
atoa profile set env=production # active profile; prompts to confirm
atoa profile set env=sandbox --profile acme --yes # scripted / CI
```
The keys you can set:
| Key | Allowed values | Notes |
| ----- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `env` | `sandbox` \| `production` | Sets which env commands target by default. Prompts for confirmation; pass `--yes` to skip. Mint the env's SDK key with `atoa keys create --env ` if you don't have one yet. |
### Reset everything
```bash theme={null}
atoa reset --yes # wipe local profiles + tokens (local only)
atoa reset --revoke --yes # additionally revoke server-side keys (best-effort)
```
`atoa reset` is destructive β it removes every profile and clears
every stored credential for this CLI. Use `--revoke` only when you also want the
server-side keys deactivated (e.g. when offboarding a shared device).
***
## Core workflows
### Create a payment
```bash theme={null}
atoa payments create \
--amount 25.00 \
--orderId order-001 \
--customerId cust_123
```
Returns a `paymentRequestId`, the customer-facing `paymentLink`, and a `qrCodeUrl`.
### Check payment status (poll until terminal)
```bash theme={null}
atoa payments status b7c8e3a1-4d29-4f5a-8e6b-1f3a2c4d5e7f --poll
```
`--poll` keeps polling every 5 seconds until the status is no longer `PENDING`, up to 3 minutes.
### List recent transactions
```bash theme={null}
atoa payments transactions --from 2026-01-01 --to 2026-01-31 --status COMPLETED
```
### Refund a payment
```bash theme={null}
atoa refunds create \
--paymentRequestId b7c8e3a1-4d29-4f5a-8e6b-1f3a2c4d5e7f \
--amount 5.00 \
--currency GBP \
--notes "duplicate charge" \
--yes
```
### Manage customers and saved cards
```bash theme={null}
atoa customers create --fullName "Jane Doe" --email jane@example.com --type INDIVIDUAL
atoa customers list --search jane
atoa payment-methods list --customer cust_123
atoa card-on-file charge \
--customerId cust_123 \
--paymentMethodId card_456 \
--amount 10.50 \
--orderId order-007
```
***
## Account management
These run on your **browser login** β no SDK key needed.
### Switch the active business
If your account owns several businesses, account commands operate on one at a time:
```bash theme={null}
atoa business list # list businesses; marks the active one
atoa business use # switch the active business (validated server-side)
```
### Manage login sessions
Every `atoa login` registers a device session server-side. Audit and revoke them:
```bash theme={null}
atoa sessions list # active CLI sessions for this account
atoa sessions revoke # revoke one (use --yes to skip the prompt)
```
Revoking the **current** device's session logs this CLI out β you'll need to
`atoa login` again.
### SDK key lifecycle
```bash theme={null}
atoa keys create --name "CI server" # mint a key with a label
atoa keys list # list keys for this account (never shows secrets)
atoa keys regenerate --env sandbox --yes # rotate the active env's key
atoa keys revoke --yes # revoke a specific key (or omit the id to pick interactively)
```
***
## Team, onboarding & business setup
### Staff and roles
`atoa staff add` and `atoa staff invite` are the same command under two names β both create
the staff member and notify them.
```bash theme={null}
atoa roles list # role IDs available on this business
atoa staff list # current staff members
atoa staff add \
--firstName Alice --lastName Smith \
--email alice@example.com \
--role # role ID from `atoa roles list`
atoa staff add --store # restrict to store(s); repeat --store for several
atoa staff update # edit name, email, phone, role or permitted stores
atoa staff delete --yes
```
Run `atoa staff update` or `atoa roles update` with no arguments on a terminal and the CLI
prompts for each field with the **current value already filled in** β press Enter to keep it.
```bash theme={null}
atoa roles permissions # the permission catalogue: id, name, category, requires
atoa roles add --name "Shift lead" # prompts for the permissions to grant
atoa roles add --name "Shift lead" --permission # repeat --permission for several
atoa roles update # edit name, description or permissions
atoa roles delete --yes
```
`--permission` takes permission **ids**, and `atoa roles permissions` is where they come from β
every other view shows only names, so start there when scripting a role. Each row also carries a
`requires` list, which is why a role can end up with more permissions than you asked for.
Some permissions depend on others. When you tick one, the CLI automatically adds
the permissions it requires and prints which ones it added.
### KYB (Know Your Business) verification
```bash theme={null}
atoa kyb status # verification status for the active business
atoa kyb link # open the KYB form in your browser
atoa kyb card status # card-payment application status, plus what's blocking it
atoa kyb card link # open the card-payment application form
```
Card payments need a separate application, which stays blocked until KYB passes. Both `link`
commands open the browser for you and print the URL as well, so there's something to fall back on
if no browser can be launched.
### Bank accounts
```bash theme={null}
atoa bank list # bank accounts on the active business
atoa bank add
atoa bank add --sortCode --accountNumber \
--accountHolderName ""
atoa bank get
atoa bank delete --yes
```
Run `atoa bank add` with no flags on a terminal and it walks you through it β pick your bank, enter
the details, confirm. Your first bank account is set as the primary automatically.
The primary account is where Atoa settles your payments. Pass `--setPrimary` on a later account to
move settlement to it β this also re-points any store currently linked to the previous account.
If a one-time code is required, `atoa bank add` sends it and exits with code `9`. Re-run the same command with
`--otp` to complete it:
```bash theme={null}
atoa bank add --bankName "" --sortCode \
--accountNumber --accountHolderName "" --output json
# β exit code 9: "An OTP was sent to your registered contact. Re-run with --otp "
atoa bank add --otp --output json
```
### Stores
All `stores` subcommands need a browser login:
```bash theme={null}
atoa stores list # list merchant stores
atoa stores get # store details
atoa stores add # interactive: name, address, postcode
atoa stores add --locationName "" --addressLine1 "" \
--cityOrTown "" --addressPostalCode ""
atoa stores update # prompts with current values prefilled
atoa stores link-bank --bank # attach a bank account to a store
atoa stores image ./logo.png --storeId # upload/replace the store logo (PNG/JPG, β€6MB)
```
### Google Business listing
Pair a store with its Google Business listing so reviews flow back to Atoa:
```bash theme={null}
atoa google search "" # find listings, with addresses
atoa google link --store \
--search "" --placeId
atoa google locations # listings linked to this business
atoa google unlink --store --yes # remove a link
```
**Search first, then link with a `placeId` from those results.** The listing's name and address are
stored exactly as the CLI sends them and are never looked up from the id, so `--placeId` on its own
exits with code `3` β the search is what supplies them. Pass the same `--search` text to both commands.
Linking a store that already has a listing **replaces** it β there's no warning, so check with
`--dryRun` first. `atoa google unlink` reverses it.
`atoa google search` works on any account. `link`, `unlink` and `locations` need the **Google
Review add-on** and exits with code `8` without it β `atoa addons list` shows your plan's limits. Once the
plan allows it, `locations` still exits with code `4` until the merchant connects their Google Business
account, which happens in the dashboard rather than the CLI.
### Direct Debit
Atoa collects your subscription fees by Direct Debit. A business can hold **one** mandate β
once it's active, setup refuses rather than creating a second one.
```bash theme={null}
atoa direct-debit status # whether a mandate exists, and its status
atoa direct-debit setup # interactive: bank details + billing address, then the mandate
```
Run interactively and the CLI prefills the account holder name, email and billing address
already on the business, and masks the account number it has on file β press Enter to keep it.
To run it without a terminal, pass every field as a flag plus `--acceptMandate`:
```bash theme={null}
atoa direct-debit setup --accountNumber --sortCode \
--name "" --email \
--addressLine1 "" --city "" --postalCode "" \
--acceptMandate --output json
```
Only `--addressLine2` is optional. The name is the **account holder's own**, not the business's β
the mandate is signed by an individual β and it's capped at 20 characters here.
`--acceptMandate` is **required** and records the merchant's agreement to the Direct Debit
mandate terms: the request stores an affirmative acceptance with a timestamp. Without it the
command exits with code `3` and creates nothing. Only pass it once the merchant has explicitly agreed, and
tell them what they're agreeing to.
### Notification preferences
```bash theme={null}
atoa comms list # every topic, with each channel's state
atoa comms set --email off # takes the topic's ID or its display name
atoa comms set --sms on --push off
```
Each channel reads as `on`, `off`, or `unavailable` β Atoa doesn't send every channel for
every topic, and an unavailable channel can't be switched on.
### Add-on plans
```bash theme={null}
atoa addons list # current plan, feature usage, and what you can move to
atoa addons upgrade # pick a plan on a TTY, or pass a plan ID
atoa addons downgrade
atoa addons cancel-downgrade # cancel a downgrade that hasn't taken effect yet
```
A downgrade is refused while your usage exceeds the target plan's limits. The CLI
names which features are over the limit so you know what to reduce first.
### Checkout branding and SMS sender name
```bash theme={null}
atoa custom-branding get # current checkout theme colour
atoa custom-branding set '#FF0000' # 6-digit hex
atoa custom-branding reset # back to the Atoa default
atoa custom-sms list # current sender name and its review status
atoa custom-sms set AcmeLtd # request a sender name (3-11 chars; letters, numbers, spaces)
atoa custom-sms delete --yes
```
### Onboard a brand-new account
```bash theme={null}
atoa signup # interactive: email + one-time code, then a guided onboarding wizard
```
Every prompted value also has a flag, so signup can run with no terminal β useful in CI or from a
script. The code goes to your inbox, so it takes two runs:
```bash theme={null}
atoa signup --email you@example.com --output json
# β exit code 9: "An OTP has been sent to you@example.com."
atoa signup --email you@example.com --otp --acceptTerms \
--businessName "Acme Ltd" --industry "Retail - Other" \
--monthlyTurnover "Up to Β£10,000" --businessStructure "Limited Company" \
--vatNumber 123456789 --firstName Ada --lastName Lovelace \
--postalCode "SW1A 2AA" --addressLine1 "10 Downing Street" --output json
```
`--acceptTerms` records acceptance of the [Privacy Policy](https://paywithatoa.co.uk/atoa-business-privacy-policy/)
and [Terms of Service](https://paywithatoa.co.uk/terms/); `--marketing` is a separate opt-in.
`--startNew` creates a second business rather than resuming an existing signup.
`--businessStructure` is `Limited Company` or `Charity`. `--industry` and `--monthlyTurnover` are
server-defined lists that vary by environment, so the values above are illustrative β pass the
option's name and, if it doesn't match, the CLI exits with code `3` listing every valid one to choose from.
`atoa signup` is the one command that needs **no prior login** β it creates
the account and logs you in as part of the flow.
***
## Payment links
Create a hosted checkout link (browser login; `--storeId` is required):
```bash theme={null}
atoa payment-links create --amount 10.50 --storeId
atoa payment-links create --amount 10.50 --storeId --notes "Invoice #42" # notes max 30 chars
atoa payment-links get --storeId # fetch a link + its status
atoa payment-links delete --storeId # remove a link
```
`--amount` is in **GBP** (e.g. `10.50` for Β£10.50), not pence.
***
## Idempotency β duplicate-charge protection
The CLI auto-generates a fresh `Idempotency-Key: ` header on every POST/PUT/PATCH, so a network blip on the response never double-charges a customer. You don't need to think about this for interactive use.
For **CI / scripted retries** β where one logical operation may run more than once (a job restarts) β pass a **stable** `--idempotencyKey` so a re-run produces the same key and the server deduplicates:
```bash theme={null}
# Auto-generated key (default β safe for interactive use)
atoa refunds create --paymentRequestId pr_abc --amount 5.00
# Stable key (CI retry-safe β same key on every re-run of the same job)
atoa refunds create \
--paymentRequestId pr_abc --amount 5.00 \
--idempotencyKey "atoa-cli/refund/${RUN_ID}/${PAYMENT_ID}"
```
Available on `atoa payments create`, `atoa refunds create`, `atoa card-on-file charge`, and the generic `atoa post`. Every other POST still sends an auto-generated key, so all retries are safe by default.
***
## Trigger test webhook events
The CLI can fire a **synthetic webhook payload** at your registered sandbox URL β same shape as production β without you actually creating a real payment.
```bash theme={null}
atoa webhooks trigger PAYMENTS_STATUS
```
```
Test webhook triggered successfully.
```
Atoa's webhook service dispatches the body to whatever sandbox URL you registered with `atoa webhooks create`. Inspect the request on your endpoint (e.g. [webhook.site](https://webhook.site)) to verify your signature-check, routing logic, and response shape work end-to-end.
### Supported events
| Event | Description |
| -------------------- | ------------------------------------------------------- |
| `PAYMENTS_STATUS` | Payment-status webhook (pay-by-bank or card) |
| `REFUND_STATUS` | Refund-status webhook |
| `EXPIRED_STATUS` | Payment expired (customer didn't pay in time) |
| `POS_PAYMENT_STATUS` | POS event β choose which event to simulate via `--type` |
### Customising the dispatched body
| Flag | Use |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `--orderId` | Override the `orderId` in the dispatched payload |
| `--amount` | Override the `amount` in pounds, e.g. `10.05` for Β£10.05 |
| `--paymentMethod` | `CARD` or `PAY_BY_BANK` |
| `--status` | Force a specific status (`COMPLETED`, `AUTHORIZED`, `FAILED`, `CANCELLED`, `EXPIRED`) |
| `--type` | `POS_PAYMENT_STATUS` only β which POS event to simulate: `PAYMENTS_STATUS` (default) \| `REFUND_STATUS` \| `EXPIRED_STATUS` |
| `--customFields` | `POS_PAYMENT_STATUS` only β JSON array of `{value, fieldName}` objects |
```bash theme={null}
# Successful card payment
atoa webhooks trigger PAYMENTS_STATUS --paymentMethod CARD --status AUTHORIZED
# Failed pay-by-bank
atoa webhooks trigger PAYMENTS_STATUS --status FAILED
# Refund that failed at the issuer
atoa webhooks trigger REFUND_STATUS --status FAILED
# Custom order ID and amount
atoa webhooks trigger PAYMENTS_STATUS --orderId my-test-order --amount 50.00
# POS_PAYMENT_STATUS can simulate several POS events β pick one via --type
atoa webhooks trigger POS_PAYMENT_STATUS --type PAYMENTS_STATUS
atoa webhooks trigger POS_PAYMENT_STATUS --type REFUND_STATUS --status COMPLETED
atoa webhooks trigger POS_PAYMENT_STATUS --type EXPIRED_STATUS
atoa webhooks trigger POS_PAYMENT_STATUS --type PAYMENTS_STATUS \
--customFields '[{"value":"CUST_001","fieldName":"Customer ID"}]'
```
Test triggers are **sandbox-only by design**. If you pass `--env production`,
the CLI warns you and proceeds with your active profile's sandbox key
anyway β this prevents accidentally driving fake events at your live customers.
***
## Using the CLI in CI
`atoa login` needs an interactive browser, so a CI runner **cannot log in itself**. Instead, provision credentials on a workstation once, then hand the files to the runner:
1. On a workstation: `atoa login`, plus `atoa keys create` if the job hits SDK/data commands.
2. Copy `~/.atoa/auth/` to a location the runner can read.
3. Point the runner at it with `ATOA_HOME` (credentials live in `$ATOA_HOME/.atoa/auth/`).
```bash theme={null}
# ATOA_HOME β the dir that holds .atoa/auth/. Scope to a named profile, use a stable idempotency key.
atoa --profile ci payments create \
--amount 10.00 --orderId "$RUN_ID" --customerId cust_123 --redirectUrl https://x \
--idempotencyKey "ci-payment/$RUN_ID" --dryRun
# Read-only check, machine-readable output
atoa --profile ci payments transactions --output json --status COMPLETED
# Clean up β clears the login session (server revoke is automatic); --purgeKey also drops the SDK key
atoa logout --profile ci --purgeKey --yes
```
Every command accepts `--output json` for machine-readable output (the default is a human-readable table) and `--dryRun` to resolve the request without sending it. `--yes` is required on destructive commands in a non-TTY context.
Passing `--output` does more than pick a format β it also puts the CLI into **non-interactive
mode**, so no prompt is ever opened. A command that would have asked for a value reports the
missing flag by name and exits with code `3` instead. This is what makes the CLI safe to drive
from an AI agent: it never blocks on a prompt the agent cannot see, and a missing value comes back
as a named flag the agent can supply on the next run.
### Exit codes
The CLI uses POSIX-style exit codes so pipelines can branch on the failure mode:
| Code | Meaning | Trigger |
| ---: | --------------------- | ----------------------------------------------------------------------------------------------- |
| `0` | Success | Command completed |
| `1` | Generic failure | Anything not classified below |
| `2` | Auth / forbidden | HTTP 401 / 403 β token invalid, revoked, or lacks permission |
| `3` | Validation error | HTTP 400, or client-side input rejected |
| `4` | Not found | HTTP 404 |
| `5` | Rate limited | HTTP 429, or a one-time-code / sign-in throttle β **stop and wait**; retrying extends the block |
| `6` | Network / TLS / DNS | Couldn't reach the server |
| `7` | Business not selected | The account has several businesses and none is active β run `atoa business use ` |
| `8` | Plan limit | The add-on plan doesn't allow this β `atoa addons list` shows the limits |
| `9` | One-time code sent | Not a failure: a code was sent and nothing was written. Re-run with `--otp ` |
```bash theme={null}
if ! atoa refunds create --paymentRequestId "$PR" --amount 5.00 --idempotencyKey "$RUN_ID" --yes ; then
case $? in
2) echo "Token invalid β re-login required" ;;
3) echo "Bad input β fix the payload" ;;
5) echo "Throttled β wait it out; retrying extends the block" ;;
6) echo "Network blip β re-running with the same idempotency key is safe" ;;
esac
fi
```
***
## Environment variables
| Variable | Purpose |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `ATOA_HOME` | Override where config + credentials live. Defaults to `$HOME`; credentials land in `$ATOA_HOME/.atoa/auth/`. Useful for Docker / ephemeral CI. |
| `ATOA_PROFILE` | Default profile name β same as passing `--profile ` on every command. The explicit flag still wins. |
***
## Generic HTTP verbs
When you need to hit an endpoint the CLI doesn't yet wrap (or you want full control over the request body), use the raw verbs. They authenticate with the **SDK key**, so run `atoa keys create` first:
```bash theme={null}
atoa get /api/payments/stores
atoa get /api/customers -d page=0 -d size=20 --pageAll
atoa post /api/webhook/merchant --data @webhook.json
atoa post /api/something -d amount=1005 -d orderId=order-001
atoa delete /api/customers/cust_123 --yes
```
### How `-d` and `--data` work
These are two different ways to build the request body. Pick whichever fits the call:
| Form | What it means | Example |
| --------------------------- | ------------------------------------------------------------------------- | -------------------------------------------- |
| `-d key=value` (repeatable) | Build the body **one field at a time**. Same flag can be used many times. | `-d amount=1005 -d orderId=order-001` |
| `--data '{"key":"value"}'` | Pass the **entire body as an inline JSON string**. | `--data '{"orderId":"o-1","amount":1005}'` |
| `--data @path/to/file.json` | The `@` prefix means **read the body from a file** on disk. | `--data @webhook.json` |
| `--data -` | Read the body from **stdin** (pipe-in form for CI scripts). | `cat body.json \| atoa post /api/x --data -` |
So `atoa post /api/webhook/merchant --data @webhook.json` reads `webhook.json` from the directory you ran the command in, takes its contents as the JSON request body, and POSTs it to `/api/webhook/merchant`. Example file:
```json theme={null}
// webhook.json
{
"url": "https://your-server.example.com/atoa-hook",
"event": "PAYMENTS_STATUS"
}
```
`-d` and `--data` are mutually meaningful β they both contribute to the same
body. If you pass both, `--data` wins and `-d` fields are ignored. Use one or
the other per command.
### Passing nested objects and arrays
Some API fields aren't a single string or number β they're objects (e.g. `authentication`, `consumerDetails`) or arrays (e.g. `paymentMethod`, `storeIds`). How you pass these depends on which kind of command you're using.
**`-d` and `--data` are flags on the generic verbs only** (`atoa get`, `atoa post`, `atoa delete`). Typed commands like `atoa payments create` and `atoa customers create` accept only their named flags β they do **not** accept `-d key=value` or `--data @file.json`.
#### On typed commands (e.g. `atoa payments create`)
Use the command's own named flags. Quote complex values so the shell passes them to the CLI intact:
| Value shape | How to pass it | Concrete example |
| ----------------------- | -------------------------- | -------------------------------------------------------------------------------------- |
| **Object** | Inline JSON, single-quoted | `--consumerDetails '{"firstName":"Jane","lastName":"Doe","email":"jane@example.com"}'` |
| **Array (comma form)** | Comma-separated string | `--paymentMethod CARD,PAY_BY_BANK` |
| **Multi-value filters** | Comma-separated string | `--status COMPLETED,FAILED` Β· `--storeIds store_uk_1,store_uk_2` |
```bash theme={null}
# Object value via a typed flag (bash / zsh / git-bash)
atoa payments create \
--amount 25.00 --orderId order-001 --customerId cust_123 \
--redirectUrl https://shop.example.com/return \
--consumerDetails '{"firstName":"Jane","lastName":"Doe","email":"jane@example.com","phoneCountryCode":"44","phoneNumber":"7700900000"}'
# Comma-separated array via a typed flag
atoa payments transactions --status COMPLETED,FAILED --storeIds store_uk_1,store_uk_2
```
**PowerShell users:** Windows re-parses arguments when handing them to
native `.exe` / `.cmd` programs, so the bash-style `'{"key":"value"}'`
doesn't survive intact. Use **backslash-escaped double quotes inside the
single quotes** instead:
```pwsh theme={null}
# Object value via a typed flag (PowerShell β note the \" escapes)
atoa payments create --amount=25.00 --orderId=order-001 --customerId=cust_123 `
--redirectUrl=https://shop.example.com/return `
--consumerDetails '{\"firstName\":\"Jane\",\"lastName\":\"Doe\",\"email\":\"jane@example.com\",\"phoneCountryCode\":\"44\",\"phoneNumber\":\"7700900000\"}'
```
If a value contains whitespace (e.g. `"Jane Doe"`), wrap the whole
backslash-escaped JSON in an extra pair of outer double-quotes so Windows
doesn't split at the space:
```pwsh theme={null}
atoa payments create --consumerDetails '"{\"firstName\":\"Jane\",\"lastName\":\"Doe\"}"'
```
bash / zsh / git-bash users β the single-quoted plain form (top of
section) works directly, no escaping needed.
If a typed command doesn't expose a flag for the field you need, fall through to the generic verbs below.
#### On generic verbs (e.g. `atoa post /api/...`)
The generic verbs accept any JSON body. Three ways to specify it:
| Value shape | How to pass it | Concrete example |
| ------------------------- | --------------------------- | ----------------------------------------------------- |
| **Field by field** | `-d key=value` (repeatable) | `-d amount=1005 -d orderId=order-001` |
| **Object field** | `-d key=` | `-d authentication='{"username":"u","password":"p"}'` |
| **Array field (JSON)** | `-d key=` | `-d paymentMethods='["CARD","PAY_BY_BANK"]'` |
| **Whole body inline** | `--data ''` | `--data '{"orderId":"o-1","amount":1005}'` |
| **Whole body from file** | `--data @path/to/file.json` | `--data @webhook.json` |
| **Whole body from stdin** | `--data -` | `cat body.json \| atoa post /api/x --data -` |
```bash theme={null}
# Object value via -d (generic verb)
atoa post /api/webhook/merchant \
-d sandboxUrl=https://example.com/hook \
-d eventTypes=PAYMENTS_STATUS \
-d authentication='{"username":"u","password":"p"}'
# Whole body from a file (generic verb)
atoa post /api/payments/process-payment --data @payment.json
```
Wrap JSON values in **single quotes** so `{`, `"`, and spaces pass through
untouched.PowerShell: same rule, but escape any inner single quote by
doubling it (`'It''s urgent'` β `It's urgent`).
These use your active profile's credentials and respect every common flag (`--env`, `--output`, `--dryRun`, `--verbose`). Useful for:
* **New endpoints** that haven't gained a typed wrapper yet
* **Debugging** β `--verbose` prints the redacted request line so you can see exactly what was sent
* **CI scripts** where you want explicit control over the exact body sent
***
## Shell completion
Enable TAB-completion of commands, flags, profile names, and IDs:
```bash theme={null}
atoa completion bash >> ~/.bashrc && source ~/.bashrc
```
```bash theme={null}
atoa completion zsh > ~/.zsh/completions/_atoa
# Then in ~/.zshrc above `compinit`:
# fpath=(~/.zsh/completions $fpath)
# autoload -Uz compinit && compinit
```
```powershell theme={null}
# Current session
atoa completion pwsh | Out-String | Invoke-Expression
# Persist across sessions
Add-Content $PROFILE 'atoa completion pwsh | Out-String | Invoke-Expression'
```
After installing, hit `` mid-command:
```
atoa pa β payments, payment-methods, payouts
atoa payments β create, status, cancel, transactions
atoa profile use β live profile names from your config
```
***
## Command reference
| Command | Purpose | Auth |
| ------------- | ------------------------------------------------------------------------ | ---- |
| `atoa login` | Pair the CLI with an account via the browser | β |
| `atoa logout` | Clear a profile's login session (`--purgeKey` also removes its SDK keys) | β |
| `atoa whoami` | Show the current account identity and profile | β |
| `atoa signup` | Create a new account (email + OTP) and onboard a business | none |
| Command | Purpose | Auth |
| --------------------------------- | -------------------------------------------- | ----- |
| `atoa business list` | List businesses; marks the active one | login |
| `atoa business use ` | Switch the active business for account calls | login |
| `atoa sessions list` | List active CLI sessions | login |
| `atoa sessions revoke ` | Revoke a session | login |
| `atoa keys create` | Mint an SDK key | login |
| `atoa keys list` | List SDK keys (never shows secrets) | login |
| `atoa keys regenerate` | Rotate the active env's SDK key | login |
| `atoa keys revoke []` | Revoke an SDK key | login |
| Command | Purpose | Auth |
| ------------------------------- | ----------------------------------------------------- | ----- |
| `atoa staff list` | List staff members | login |
| `atoa staff add` | Add a staff member (`--role`, `--store`) | login |
| `atoa staff invite` | Alias of `staff add` | login |
| `atoa staff update []` | Edit name, email, phone, role or permitted stores | login |
| `atoa staff delete []` | Remove a staff member from the business | login |
| `atoa roles list` | List available roles | login |
| `atoa roles permissions` | Permission catalogue β the ids `--permission` takes | login |
| `atoa roles add` | Create a role (`--permission`, repeatable, takes ids) | login |
| `atoa roles update []` | Edit a role's name, description or permissions | login |
| `atoa roles delete []` | Delete a role | login |
| `atoa kyb status` | KYB verification status | login |
| `atoa kyb link` | Open the KYB form in the browser (prints the URL too) | login |
| `atoa kyb card status` | Card-payment application status and what blocks it | login |
| `atoa kyb card link` | Open the card-payment application form | login |
| `atoa bank list/get/add/delete` | Manage business bank accounts | login |
| Command | Purpose | Auth |
| ---------------------------------- | -------------------------------------------------- | ----- |
| `atoa addons list` | Current plan, feature usage, and available moves | login |
| `atoa addons upgrade []` | Move to a higher plan | login |
| `atoa addons downgrade []` | Move to a lower plan (refused if usage exceeds it) | login |
| `atoa addons cancel-downgrade` | Cancel a scheduled downgrade | login |
| Command | Purpose | Auth |
| -------------------------- | -------------------------------------------------- | ----- |
| `atoa direct-debit status` | Whether a mandate exists, and its status | login |
| `atoa direct-debit setup` | Set up the subscription mandate (one per business) | login |
| Command | Purpose | Auth |
| --------------------------------- | ----------------------------------------------------- | -------------- |
| `atoa google search ""` | Find Google listings by name and address | login |
| `atoa google link --store ` | Attach a listing to a store (`--search`, `--placeId`) | login + add-on |
| `atoa google locations` | Listings linked to this business | login + add-on |
| `atoa google unlink --store ` | Remove a store's listing | login + add-on |
| Command | Purpose | Auth |
| -------------------------------- | --------------------------------------------- | ----- |
| `atoa comms list` | Notification topics and each channel's state | login |
| `atoa comms set ` | Toggle `--email`/`--sms`/`--push` for a topic | login |
| `atoa custom-branding get` | Current checkout theme colour | login |
| `atoa custom-branding set ` | Set the checkout theme colour | login |
| `atoa custom-branding reset` | Restore the Atoa default colour | login |
| `atoa custom-sms list` | Custom SMS sender name and review status | login |
| `atoa custom-sms set ` | Request a custom SMS sender name | login |
| `atoa custom-sms delete` | Remove the custom sender name | login |
| Command | Purpose | Auth |
| -------------------------------- | ------------------------------------------------------- | ----- |
| `atoa payment-links create` | Create a hosted checkout link (`--amount`, `--storeId`) | login |
| `atoa payment-links get ` | Fetch a link and its status (`--storeId`) | login |
| `atoa payment-links delete ` | Delete a link (`--storeId`) | login |
| Command | Purpose |
| --------------------------------- | --------------------------------------- |
| `atoa profile list` | List all profiles on this machine |
| `atoa profile use ` | Set the active profile |
| `atoa profile show [name]` | Show profile metadata (never the token) |
| `atoa profile set =` | Change the default `env` on a profile |
| `atoa profile rename ` | Rename a profile |
| `atoa profile delete ` | Remove a profile and its tokens |
| `atoa reset` | Wipe all profiles and credentials |
| Command | Purpose |
| ---------------------------------------- | ------------------------------------------------------- |
| `atoa payments create` | Create a payment request |
| `atoa payments status ` | Check a payment's status (optional `--poll`) |
| `atoa payments cancel ` | Cancel a payment request |
| `atoa payments transactions` | List transactions (filter by date/status/store) |
| `atoa stores list` | List merchant stores (login) |
| `atoa stores get ` | Store details (login) |
| `atoa stores add` | Create a store (login) |
| `atoa stores update []` | Edit a store, prefilled with its current values (login) |
| `atoa stores link-bank --bank ` | Attach a bank account to a store (login) |
| `atoa stores image ` | Upload/replace a store logo (login) |
| Command | Purpose |
| ---------------------------------------------- | ------------------------------------ |
| `atoa customers create/list/get/update/delete` | Customer CRUD |
| `atoa payment-methods list/get/delete` | Saved cards per customer |
| `atoa card-on-file charge` | Charge a saved card |
| `atoa card-on-file capture ` | Settle a pre-authorized card payment |
| `atoa card-on-file cancel ` | Void a pre-authorization |
| Command | Purpose |
| -------------------------- | ---------------------------------------------- |
| `atoa refunds create` | Initiate a refund (full or partial) |
| `atoa refunds cancel ` | Cancel an `INITIATED` refund (production only) |
| `atoa refunds list` | List refunds for a payment |
| Command | Purpose |
| ------------------------------- | -------------------------------------------- |
| `atoa webhooks create` | Register a webhook endpoint |
| `atoa webhooks list` | List registered webhooks |
| `atoa webhooks delete ` | Remove a webhook |
| `atoa webhooks trigger ` | Fire a synthetic webhook at your sandbox URL |
| Command | Purpose |
| ---------------------------------- | -------------------------------------- |
| `atoa bank-feed initiate` | Start a bank authorization flow |
| `atoa bank-feed accounts` | List authorized accounts for a consent |
| `atoa bank-feed account ` | Account details |
| `atoa bank-feed balance ` | Account balance |
| `atoa bank-feed transactions ` | Account transactions in a date range |
| `atoa bank-feed revoke` | Revoke account access |
| Command | Purpose |
| -------------------------------- | --------------------------------------------- |
| `atoa payouts list` | List payouts (date / status / search filters) |
| `atoa payouts transactions ` | Transactions for a single payout |
| Command | Purpose |
| ------------------------- | ------------------------------------------------ |
| `atoa institutions list` | List supported banking institutions |
| `atoa get/post/delete` | Raw HTTP requests against any API path (SDK key) |
| `atoa completion ` | Print the shell-completion bootstrap |
| `atoa --help` | Top-level help; works on every subcommand too |
Most commands accept these **common flags** (per-command `--help` is authoritative):
| Flag | Use |
| ----------- | ---------------------------------------------------------------- |
| `--env` | Override env (`sandbox` \| `production`) for this single command |
| `--output` | `json` \| `table` \| `yaml` |
| `--verbose` | Print a redacted request log for debugging |
| `--dryRun` | Resolve the request but don't send it |
| `--yes` | Skip confirmation prompts |
| `--profile` | Operate against a specific profile |
***
## Troubleshooting
First-run state, or your session expired / was revoked (e.g. a later login
on the same device evicted it). Check who you are, then re-pair:
```bash theme={null}
atoa whoami # confirm the active profile
atoa login # log in again via your browser
```
If an **account command** complains you only have an SDK key (or vice versa),
obtain the missing credential β `atoa login` for a login session,
`atoa keys create` for an SDK key. See [the dual-credential
model](#the-dual-credential-model).
Another `atoa` process is mid-write. If none is running (e.g. one crashed),
remove the stale lockfile:
```bash theme={null}
rm ~/.atoa/auth/session.json.lock
```
The session file gained group/other read bits β fix with `chmod 600 ~/.atoa/auth/session.json`.
You need to register a sandbox webhook URL first:
```bash theme={null}
atoa webhooks create --url https://webhook.site/ --event PAYMENTS_STATUS
atoa webhooks trigger PAYMENTS_STATUS
```
The trigger uses the URL registered for the **same event type** under your active profile's sandbox.
1. Re-source your shell config after install (e.g. `source ~/.bashrc`, or open a new PowerShell window).
2. Confirm completion is registered:
```bash theme={null}
# bash
complete -p atoa
# zsh
echo $_comps[atoa]
# PowerShell
Get-PSReadLineKeyHandler -Bound | Where-Object Function -eq Complete
```
3. For PowerShell specifically, make sure you ran `| Out-String | Invoke-Expression` after `atoa completion pwsh` β the script must be **evaluated**, not just printed.
```bash theme={null}
atoa reset --yes # local only
atoa reset --revoke --yes # local + best-effort server-side revoke
```
***
## Need Help?
Contact our team at `hello@paywithatoa.co.uk` or use chat support on the [Dashboard](https://dashboard.paywithatoa.co.uk/).
# Design Guidelines for Atoa Payments
Source: https://docs.paywithatoa.co.uk/design-system
Atoa brand and UI guidelines: logos, colours, QR codes and 'Pay by bank' button variants for adding Atoa to your checkout.
At Atoa, our goal is to streamline the payment process to make it as smooth as possible. This guide outlines key design elements you can incorporate for better integration with Atoa's payment system.
Feel free to use the Figma file to customize and make any necessary
adjustments.
**Displaying Atoa QR Codes**
For POS systems and till screens, our [Payment-Process](../api-reference/payment/process-payment) API offers a unique feature. By specifying `EXTERNAL_DISPLAY` and `RECEIPT` in the template parameter, you will receive links to two distinct images. These images are designed for integration into your system:
**Till Screen:** Display Atoa's QR code directly on your kiosk or till screen to let customers scan and pay easily. This is ideal for self-serve restaurants with kiosk setups, or if your till features a customer-facing display.
**Invoice Receipts:** Add Atoa's dynamic QR code to invoice receipts for a smooth payment process. This method is particularly effective in restaurants, giving customers more time to pay without waiting for staff.
**Buttons in the Payment flow**
To add Atoa as a payment option to your system, we provide a set of assets designed for seamless integration and to align with Atoa's branding and user interface guidelines. You'll find these ready-to-use assets below, ready for implementation into your payment gateway or web integrations.
# Atoa Flutter SDK for Mobile Payments
Source: https://docs.paywithatoa.co.uk/flutter-sdk
Add Atoa Pay by Bank and card payments to your Flutter app. Install the SDK, start a payment and handle the result, with code samples.
## About the Flutter SDK
Integrate Atoa into your Flutter app in minutes using our dedicated SDK.
Easily add seamless Pay by Bank payments to new or existing Flutter apps. The SDK is exclusive to Flutter (not native Android/iOS) and available on [pub.dev](https://pub.dev/packages/atoa_sdk), where developers are welcome to contribute and provide feedback.
## Prerequisites
Before you start, ensure the following setup is complete:
* Create an Atoa account: Sign up using our Business App or Web Dashboard.
* Generate API keys: Go to API Access inside the Atoa Business App and generate keys for Sandbox (testing) and Production (live).
* Backend readiness: You must be able to create a Payment Request on your backend server using Atoa APIs before calling the Flutter SDK.
See the [Getting Started](/introduction#step-1-sign-up-for-developer-access) guide for more information.
## 1: Install the SDK
Run the following to add the Atoa SDK to your Flutter project
```sh theme={null}
flutter pub add atoa_sdk
```
For usage, sample code to integrate can be found in [Example app](https://github.com/ATOAPaymentsLimited/flutter_atoa_sdk/tree/main/demo_app).
## 2. Import the Package
```dart theme={null}
import 'package:atoa_sdk/atoa_sdk.dart';
```
## 3: Generate a Payment Request (Backend)
You can generate a payment using the [payment-process](/api-reference/payment/process-payment) API, which must be called from your server while creating an order. Refer to the getting started guide to know more
```sh theme={null}
https://api.atoa.me/api/payments/process-payment
```
```sh theme={null}
curl --location --request POST 'https://api.atoa.me/api/payments/process-payment' \
--header 'Authorization: Bearer {access-secret}' \
--header 'Content-Type: application/json' \
--data-raw '{
"customerId": "",
"orderId": "",
"amount": "",
"currency": "GBP",
"paymentType": "",
"autoRedirect": "",
"consumerDetails": {
"phoneCountryCode": "",
"phoneNumber": ""
}
}'
```
Note: `redirectUrl` can be used to redirect the user back to your app after the payment is completed. Follow the information given in the handle redirection steps below
## 4. Launch the Payment Dialog
After obtaining a valid paymentRequestId, you can now invoke the Flutter SDKβs Pay method to show the checkout experience:
```dart theme={null}
final transactionDetails = await AtoaSdk.pay(
context,
paymentId: 'your-payment-request-id',
showHowPaymentWorks: false,
customerDetails:
// pass customer details for pre-select bank
const CustomerDetails(
phoneCountryCode: '44',
phoneNumber: '8788899999',
email: 'aaa@gmail.com',
),
env: AtoaEnv.prod,
// or AtoaEnv.sandbox
onUserClose: (
{required String paymentRequestId,
Map? redirectUrlParams,
String? signature,
String? signatureHash}) {
// handle payment when user close the payment verification bottom sheet
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: RegalColors.darkOrange,
content: Text(
'User closed the payment for paymentRequestId: $paymentRequestId',
),
),
);
},
onPaymentStatusChange: (
{required String status,
Map? redirectUrlParams,
String? signature,
String? signatureHash}) {
// handle payment status
print('Payment Status Changed to $status');
},
onError: (error) {
// handle Atoa Mobile SDK error
print('Error in Atoa Mobile SDK ${error.message}');
},
);
```
Pass `customerDetails` if available. It pre-selects the user's previous bank
for faster checkout.
#### SDK Parameters
| Parameters | Type | Required | Description |
| :-------------------- | :-------------- | :------- | :------------------------------------------------- |
| env | AtoaEnv | Yes | Environment: AtoaEnv.sandbox or AtoaEnv.prod |
| paymentId | String | Yes | Unique payment request ID |
| showHowPaymentWorks | Boolean | Yes | Display onboarding sheet for users |
| customerDetails | CustomerDetails | No | Mobile + Email to fetch last-used bank |
| onError | Callback | Optional | Called on SDK errors |
| onPaymentStatusChange | Callback | Optional | Called when the payment status updates |
| onUserClose | CallBack | Optional | Called when user closes payment verification sheet |
Below are the payload structures you can expect inside each callback:
#### onError
Triggered when an error occurs during the payment process
```dart theme={null}
{
AtoaException error // Error object which contains error message,
}
```
#### onPaymentStatusChange
Triggered when the payment status updates. Handle COMPLETED, PENDING and FAILED statuses.
```dart theme={null}
{
String status, // Current payment status
String? atoaSignature, // Atoa signature for verification (optional)
String? atoaSignatureHash, // Atoa signature hash for verification (optional)
Map? redirectUrlParams, // Additional callback parameters (optional)
}
```
#### onUserClose
Triggered when the user closes the payment verification sheet
```dart theme={null}
{
String paymentRequestId, // The payment request ID
String? atoaSignature, // Atoa signature for verification (optional)
String? atoaSignatureHash, // Atoa signature hash for verification (optional)
Map? redirectUrlParams, // Additional callback parameters (optional)
}
```
## 5. Handle Payment Response
You can handle the payment success, failure, pending and other statuses based on payment response
```dart theme={null}
if (transactionDetails != null) {
if(transactionDetails.isCompleted) {
// handle success
} else {
// handle failure / pending statuses
}
} else {
// Bottom sheet was dismissed
}
```
Sample response can be seen [here](/introduction#step-3-handle-payment-status).
## 6. Handle Redirection (Deep link)
While calling payment-process API to generate a payment, you can specify a redirectUrl in your request body. The redirectUrl, which should be passed as body parameters, redirects to your website and then opens your app via deep linking. This enables users to open your application after payment.
Following the completion of the payment by the customerβs bank app, they will be redirected back to their browser. Atoa will transmit the idempotency id, amount and redirect URL with status as query parameters appended to your specified Redirected URL, forming a URL structure like:
```dart theme={null}
www.yourdeeplink.me?idempotency=ATOA783838728372&redirectUrl=https://pay.me&amount=2
```
Resources For Implementing deep-linking for your app
* [Flutter Docs](https://docs.flutter.dev/ui/navigation/deep-linking)
* [Code With Andrea](https://codewithandrea.com/articles/flutter-deep-links/)
* [Set up App links for Android](https://docs.flutter.dev/cookbook/navigation/set-up-app-links)
* [Set up Universal links for iOS](https://docs.flutter.dev/cookbook/navigation/set-up-universal-links)
### Subscribe to webhook (Recommended)
Atoa uses webhooks to notify your application whenever an event happens in your account. Webhooks are particularly useful for events such as changes in payment status, such as completion, failure, or pending.
To get started, you must [Register your webhook endpoint](/api-reference/webhook/create-webhook-event) so Atoa knows where to deliver events.
After registration, your endpoint will start receiving detailed webhook payloads. These will inform you of payment status updates, such as `COMPLETED`, `PENDING`, or `FAILED`.
### Store Fields in Server
Store the following in your backend once the transaction is complete.
```sh theme={null}
"signatureHash": string
"paymentIdempotencyId": string
```
## 7. Check Bank App Installation (Android/iOS Requirements)
Our mobile SDK checks if the bank(using for making payments) app is installed or not. For that, you need to add 'queries' tag for android and 'LSApplicationQueriesSchemes' key for iOS
In Android, you need to add 'queries' tag in `AndroidManifest.xml`
```xml theme={null}
```
In iOS, you need to add 'LSApplicationQueriesSchemes' key in `Info.plist`
```xml theme={null}
LSApplicationQueriesSchemes
pulsesecure
launchbmb
lloyds-retail
hsbc-pwnwguti5z
uk.co.santander.santanderUK
fb894703657238109
bos-retail
halifax-retail
monzo
starlingbank
tsbmobile
comfirstdirectbankingonthego
launchFT
virginmoneyimport
ybssavings
transferwise
tg
BOIOneAPP
ie.aib.mobilebanking
bos-commercial
chase-international
```
## 8. Verify Payment Signature
This is a mandatory step to confirm the authenticity of the details returned to the Checkout form for successful payments.
You can verify the signature by doing [this](/introduction#step-4-verify-signature).
## 9: Test in Sandbox
Use the sandbox for testing different flows
```dart theme={null}
final paymentDetails = AtoaSdk.pay(
context,
paymentId: 'your-payment-request-id',
env: AtoaEnv.sandbox,
// other config properties
);
```
Refer to the [Sandbox Guide](/atoa-sandbox) to simulate different outcomes.
## 10: Brand & Customize (Optional)
**Theme Color:** Choose your brandβs hex color in the dashboard. The widget will automatically reflect your theme color. [Update your theme color here](https://dashboard.paywithatoa.co.uk/add-ons/custom-branding)
**Design Guidelines:** Refer to our official branding guide to correctly showcase βPay by Bankβ in your checkout. [View Figma file](/design-system)
**Note:** You do **not** need to pass the theme color in the SDK. Simply configure these once from your Dashboard or Atoa Business App
## π§ Tips & Best Practices
β
To enable faster checkout, Atoa uses the customerβs email and mobile number to pre-select the previously used bank within our network. We recommend passing customer details in the Mobile SDK to support one-click checkout for returning users.
β
Always validate the payment status using webhooks or polling.
β
Do not display the payment widget before your backend has successfully created a valid paymentRequestId.
β
Before going LIVE, ensure to use the production access secret generated from your Business App or Web Dashboard.
β
Tipping is disabled when using the Mobile SDK. If tipping is part of your experience, we recommend implementing it on your end.
β
## π© Need Help?
If you encounter any issues or need help debugging, please refer to our public [GitHub repository](https://github.com/ATOAPaymentsLimited/flutter_atoa_sdk) for detailed setup instructions.
Weβre here to help β email us at [hello@paywithatoa.co.uk](mailto:hello@paywithatoa.co.uk) or chat with us via the dashboard's chat support
# Go-Live Checklist: Sandbox to Production
Source: https://docs.paywithatoa.co.uk/go-live
Atoa go-live checklist: move from sandbox to production, verify webhooks, keys and settings so you're ready to take real payments.
Before moving to production with Atoa, ensure you've completed the following steps:
## 1. Use Production API Keys
Generate your production `accessSecret` from the **Atoa Business App** or **Web Dashboard**. Replace all sandbox keys in your backend with this production key. Make sure these credentials are **stored securely** and **never exposed** in any frontend or public code.
## 2. Enable Webhooks (Recommended)
Set up your **webhook endpoints** to receive real-time payment updatesββincluding payment success, failure, and refunds.
In case of webhook delays or failures (e.g., due to network issues), implement **polling** as a fallback. This hybrid setup ensures reliable status tracking across all banks and platforms.
## 3. Use Atoa SDKs for Seamless Checkout
For a smooth, secure, and native user experience, we recommend integrating with Atoaβs Web or Mobile SDKs. These SDKs handle payment authentication, error states, and status callbacks nativelyβreducing development effort and improving reliability.
## 4. Whitelist Domains (Web SDK Only)
If you are integrating with the **Atoa Web SDK**, ensure that all your **production domains are whitelisted** in the Atoa dashboard.
Unlisted domains will prevent the SDK from or functioning correctly in live environments. Add both primary and any subdomains used during payment flows.
## 5. Test the Entire Flow in Production
Before going fully live, conduct an end-to-end test using your production setup:
* Trigger a live payment
* Confirm the **webhook is received and processed** correctly
* Verify the **payment status is updated** in your system
Also test edge cases such as **declined payments, canceled flows, and network timeouts**.
## 6. Customize Your Checkout Experience
Make the checkout experience look and feel like your brand. Simply configure your branding once from your Dashboard or Atoa Business App
**Theme Color:** Choose your brandβs hex color in the dashboard, and the checkout will automatically reflect your theme. [Update your theme color here](https://dashboard.paywithatoa.co.uk/add-ons/custom-branding).
**Design Guidelines:** Refer to our official branding guide to correctly showcase Pay by Bank or Pay by Card in your checkout flow. [View Figma file](https://www.figma.com/file/DODCiX4Mbj8tfdN4NelqgL/Integration-Design-Guide?type=design\&node-id=1%3A397\&mode=design\&t=xd4d8bxbXeek5JyM-1).
## 7. Pass Customer Details for Faster Checkout
To enable a frictionless experience, Atoa uses the customerβs email and mobile number to pre-select their previously used bank within our network. We recommend passing customer details in the Mobile and Web SDK to support one-click checkout for returning users.
# POS Integrations | Overview
Source: https://docs.paywithatoa.co.uk/integration-overview
Integrate Atoa with your POS or platform: SDKs, plugins (WooCommerce, Magento) and the API for Pay by Bank and card payments.
Atoa offers seamless integration with a wide range of POS and e-commerce platforms. This guide is designed to provide clear and concise steps to help you integrate Atoa smoothly with your software or website.
## Integrating Atoa with Your POS System
You can integrate with Atoa in a few straightforward steps. However, the integration process may vary based on user flow and specific use cases. Please reach out to our integration team for a more tailored approach.
Follow these 5 simple steps to integrate Atoa with your system.
**Sign up and test:** Start by signing up through Atoaβs Business app or dashboard.
**Access token:** Generate a test access token on the Dashboard (Profile icon > Settings > API Access).
**Payment process API:** Use our payment process API to create a payment link or QR code. For POS providers, this QR code can be displayed on the till screen or kiosk and printed on bill receipts. More details are available in the [Generate Payment API](./api-reference/payment/process-payment) documentation.
**Webhook:** Subscribe to our webhook service for real-time payment notifications. This feature is essential for updating payment status, including pending, successful, and failed transactions. Check our [Webhook guide](./api-reference/webhook/create-webhook-event) for further information.
**Print QR code**:Refer to our design system guide to print the QR code on your receipts and kiosk or till setup.
**Test and Go Live:** After making a test payment, your system is ready to go live with Atoa.
# Getting Started with Atoa Payments
Source: https://docs.paywithatoa.co.uk/introduction
Get started with Atoa payments: create a payment, show a QR code or link, and let customers pay from their bank app in seconds.
**Integrate Atoa as the payment method in just four easy steps.**.
## Step 1 - Sign up for developer access
You can generate an API Access Token via the **Atoa Dashboard** or the **Atoa Business App**. Follow the steps below:
1. **Sign up** via [Atoa dashboard](https://dashboard.paywithatoa.co.uk/) or Install the Atoa Business app from [Play store](https://play.google.com/store/apps/details?id=com.atoa.merchant) or [App store](https://apps.apple.com/gb/app/atoa-business/id1620667507).
2. **Complete KYB** to verify your business and personal details.
3. To access API settings:
* **On the App**: Tap the **store icon** on the homepageβs top left β go to **Settings β API Access**.
* **On the Dashboard**: Visit your Dashboard **[API Access](https://dashboard.paywithatoa.co.uk/my-account/settings/api-access)** page directly
4. **Generate a Sandbox Key** for testing.
5. When you're ready to go live, create a **Production Key**.
6. For detailed guidance, refer to the video walkthrough for the Atoa Business App.
Follow Our sandbox guide to simulate successful, failed and cancelled payment scenarios
with Atoa.
If you need any help please contact our team at `hello@paywithatoa.co.uk`.
## Step 2 - Create Payment Request.
Generate a Payment Request.
API Endpoint:
```bash theme={null}
https://api.atoa.me/api/payments/process-payment
```
```bash theme={null}
curl --location --request POST 'https://api.atoa.me/api/payments/process-payment' \
--header 'Authorization: Bearer {Access Secret}' \
--header 'Content-Type: application/json' \
--data-raw '{
"customerId": "",
"orderId": "",
"amount": "",
"currency": "GBP",
"paymentType": "",
}'
```
You can include additional parameters in the body of the request, check the [API reference](../api-reference/payment/process-payment) for further information.
After you generate a payment request, you will receive a **qrCodeUrl** and a **paymentUrl**. You can use either of these to handle payments, depending on your needs: There are two ways to handle the payment flow depending on the use case.
**Option 1:**
Payment URL - Use the payment URL from response to let your customers complete their payment online.
**Option 2:**
QR Code URL - If you're using a Point Of Sale (POS) system, you can show this QR code on a screen, receipt, or any device at your store. Customers can scan this QR code to pay.
## Step 3 - Handle Payment Status
Once the user completes the payment, you must verify the payment status. Atoa offers two methods for this: **Polling** and **Webhook**.
We recommend webhooks over polling for a more reliable integration. Webhooks deliver status updates in real time, removing the need for repeated API calls.
### Option 1: Redirection and polling
When you create a payment request, you can specify a redirection URL. After the customer completes (or abandons) the payment, Atoa redirects them back to that URL with the payment status appended as query parameters:
```
https://?status=&paymenRequestId=&paymentIdempotencyId=&orderId=&atoaSignature=
```
You can set up your redirection URL when creating API keys in Step 1.
If the status is `PENDING`, the payment is still being processed by the bank. Poll the [Get Payment Status](/api-reference/payment/get-payment-status) endpoint until you receive a definitive status (`SUCCESS` or `FAILURE`). Use an [exponential back-off strategy](https://en.wikipedia.org/wiki/Exponential_backoff) to avoid excessive requests.
```bash theme={null}
curl --location --request GET 'https://api.atoa.me/api/payments/payment-status/?type=request'
```
Pass query param `&env=sandbox` for sandbox testing.
| Status | Description |
| --------- | -------------------------------------------------------------------------------------- |
| COMPLETED | Payment processed successfully. Funds have been transferred to the merchant's account. |
| PENDING | Payment is in progress and awaiting final confirmation from the bank. |
| FAILED | Transaction was unsuccessful. The customer will have to retry the payment. |
### Option 2: Subscribe to webhooks (Recommended)
Webhooks notify your server in real time whenever a payment is completed, fails, expires, or is refunded β no polling required. Atoa delivers a signed HTTP POST to your registered endpoint with the full event payload.
For everything you need to set up webhooks β registering endpoints, event types, payload references, signature verification, and retry mechanism β see the [Webhook guide](/api-reference/webhook/introduction).
All set! You are ready to start accepting payments. If you have any questions or feedback, reach out to us at `hello@paywithatoa.co.uk`.
# Magento Payment Integration
Source: https://docs.paywithatoa.co.uk/magento
Accept Pay by Bank and card payments in Magento with Atoa. Install and configure the Atoa payment gateway for your store.
## Installation Guide
Magento 2 is a powerful, self-hosted platform that empowers businesses globally to create and manage their e-commerce stores. It offers flexibility for businesses to customise their online stores according to their specific requirements.
Atoa Payments integrates seamlessly with Magento, allowing businesses to process payments securely and efficiently, enhancing the overall customer experience.
To ensure smooth integration, we advise testing the Atoa Payments plugin in a Magento test environment before deploying it on a live production site.
Atoa plugin is compatible with Adobe Commerce Cloud v2.4 and Magento v2.4.
## 1. Prerequisites
First, create an Atoa account and generate the Access secret. Check the [Getting Started](/introduction) guide for more information.
## 2. Install the Atoa Payments Magento Plugin
### Option 1 - Download the Plugin using Composer
### 1. Add the GitHub Repository to Composer
First, add the GitHub repository to your Magento project's `composer.json` file. Open a terminal and navigate to the root of your Magento to project, then run the following command:
```sh theme={null}
composer config repositories.atoa vcs
https://github.com/ATOAPaymentsLimited/AtoaMagento.git
```
### 2. Require the Module Using Composer:
Now, require the module in your project. Run the following command:
```sh theme={null}
composer require atoa/module-atoa-payment:dev-main
```
Ensure that you replace **atoa/module-atoa-payment** with the correct vendor and package name if it is different. **dev-main** refers to the main branch of the repository.
### 3. Enable the Module in Magento:
Once the module is installed, enable it using Magento's CLI commands:
```sh theme={null}
php bin/magento module:enable Atoa_AtoaPayment
php bin/magento setup:upgrade
php bin/magento setup:static-content:deploy
php bin/magento cache:clean
```
### Via Zip/Archive
* Download file, unzip file.
* Copy folder app into root folder.
* Run the above commands.
## 3. Configure the Atoa Payments Plugin
* Sign in to your Magento Admin Panel
* Choose Stores -> Configuration -> Sales -> Payment Methods -> Atoa Payment
* Copy and paste your Access token from your Atoa Payments Portal into the API key field
* Choose the type and colour of payments banner for your storefront
* Save your changes
If you have enabled [V2 webhook signing](/api-reference/webhook/v2-signing) in the Atoa Dashboard (**Settings β Webhooks**), paste your signing secret into the plugin's **Webhook Signing Key** field and save. This ensures the plugin can verify incoming webhook deliveries. Without this step, plugin webhooks will fail signature verification after V2 signing is enabled.
### π§ Sandbox Account
Contact us at [hello@paywithatoa.co.uk](mailto:hello@paywithatoa.co.uk) if you need a Sandbox account.
## 4. Activate the Atoa Payments Plugin
Enable the Atoa Payments module by selecting **Enabled -> Yes**. Atoa Payments will appear as an option on your checkout. Set the **Sort Order -> 1** so Atoa Payments features on top.
**Frequently Asked Questions**
To disable the plugin,
run the following commands via the terminal:
**php bin/magento module:disable Atoa\_AtoaPayment**
To delete the plugin, choose any one of two options:
**Option 1** - Delete using Composer
composer remove atoa/module-atoa-payment
**Option 2** - Delete the plugin file in the code directory of the module folder.
Chat with us by emailing us on [hello@paywithatoa.co.uk](mailto:hello@paywithatoa.co.uk) or contact your designated Payment Specialist, directly.
# Model Context Protocol (MCP)
Source: https://docs.paywithatoa.co.uk/mcp-server
Connect AI assistants to Atoa with the Model Context Protocol (MCP) server, let agents create payments, check status and manage data.
The Atoa **Model Context Protocol (MCP)** server provides a set of tools that AI assistants and applications can use to interact with the Atoa payment API. Process payments, manage customers, handle refunds, access bank feeds, and more β all through natural language or programmatic tool calls.
Connect any MCP-compatible client to Atoa's hosted server. No local installation required.
Run the MCP server locally via `npx`. Works offline and keeps credentials off HTTP.
***
## Prerequisites
Before you begin, make sure you have:
* An **Atoa SDK Token** β follow the [Getting Started](/introduction#step-1-sign-up-for-developer-access) guide to generate one
***
## HTTP Mode (Recommended)
Connect any MCP-compatible client directly to Atoa's hosted MCP server β no local installation required.
**Endpoint:** `https://mcp.atoa.me/mcp`
Pass your SDK token and target environment as request headers on every connection:
| Header | Required | Value |
| ----------------------------- | -------- | ---------------------------------------------------- |
| `Authorization` | Yes | `Bearer YOUR_AUTH_TOKEN` |
| `X-Atoa-Env` | Yes | `sandbox` or `production` |
| `X-Atoa-Payment-Redirect-Url` | No | URL the customer returns to after completing payment |
| `X-Atoa-Ais-Redirect-Url` | No | URL the user returns to after bank authorization |
Use your **Sandbox token** with `X-Atoa-Env: sandbox` and your **Production
token** with `X-Atoa-Env: production`. Mixing a token with the wrong
environment will return authentication errors.
### AI Assistant Configuration
Most AI clients support HTTP-mode MCP connections natively. Use these configs to point your assistant directly at the Atoa MCP server.
Run from your terminal:
```bash theme={null}
claude mcp add --transport http atoa https://mcp.atoa.me/mcp --header "Authorization: Bearer YOUR_AUTH_TOKEN" --header "X-Atoa-Env: sandbox"
```
Edit `.cursor/mcp.json` in your project root:
```json theme={null}
{
"mcpServers": {
"atoa": {
"type": "http",
"url": "https://mcp.atoa.me/mcp",
"headers": {
"Authorization": "Bearer YOUR_AUTH_TOKEN",
"X-Atoa-Env": "sandbox"
}
}
}
}
```
Edit `.vscode/mcp.json` in your project root:
```json theme={null}
{
"servers": {
"atoa": {
"type": "http",
"url": "https://mcp.atoa.me/mcp",
"headers": {
"Authorization": "Bearer YOUR_AUTH_TOKEN",
"X-Atoa-Env": "sandbox"
}
}
}
}
```
Switch `X-Atoa-Env` from `sandbox` to `production` (and swap your token) when you go live.
### Connecting Programmatically (MCP SDK)
For web applications and backend services, use the MCP SDK's `StreamableHTTPClientTransport`:
```bash theme={null}
npm install @modelcontextprotocol/sdk
```
```typescript theme={null}
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport(
new URL("https://mcp.atoa.me/mcp"),
{
requestInit: {
headers: {
Authorization: `Bearer YOUR_AUTH_TOKEN`,
"X-Atoa-Env": "sandbox", // or "production"
},
},
},
);
const client = new Client({ name: "my-app", version: "1.0.0" });
await client.connect(transport);
// List available tools
const { tools } = await client.listTools();
// Execute a tool
const result = await client.callTool({
name: "get_stores",
arguments: {},
});
```
### Rate Limits
The HTTP endpoint enforces rate limiting on a per-token basis. Requests that exceed the allowed rate will receive a **429 Too Many Requests** response. If this happens, back off and retry after a short delay using an exponential backoff strategy.
***
## NPX Mode (Local)
Run the Atoa MCP server locally via `npx`. Use this when you prefer not to send auth headers over HTTP, need to work offline, or your AI client does not support HTTP-mode MCP connections.
**Requires Node.js 18+ and npm 9+** (`node --version` to check).
### Quick Setup
The fastest way to get started is the interactive wizard:
```bash theme={null}
npx @atoapayments/mcp init
```
This walks you through a series of prompts and generates the configuration snippet you need to add to your AI client's config file.
### Manual Configuration
Add the following to your AI client's MCP configuration file:
Edit `claude_desktop_config.json`:
* **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
* **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
```json theme={null}
{
"mcpServers": {
"atoa": {
"command": "npx",
"args": ["-y", "@atoapayments/mcp"],
"env": {
"ATOA_AUTH_TOKEN": "YOUR_AUTH_TOKEN",
"ATOA_ENV": "sandbox"
}
}
}
}
```
Edit `.cursor/mcp.json` in your project root:
```json theme={null}
{
"mcpServers": {
"atoa": {
"command": "npx",
"args": ["-y", "@atoapayments/mcp"],
"env": {
"ATOA_AUTH_TOKEN": "YOUR_AUTH_TOKEN",
"ATOA_ENV": "sandbox"
}
}
}
}
```
Edit `.vscode/mcp.json` in your project root:
```json theme={null}
{
"servers": {
"atoa": {
"command": "npx",
"args": ["-y", "@atoapayments/mcp"],
"env": {
"ATOA_AUTH_TOKEN": "YOUR_AUTH_TOKEN",
"ATOA_ENV": "sandbox"
}
}
}
}
```
Edit `~/.gemini/settings.json`:
```json theme={null}
{
"mcpServers": {
"atoa": {
"command": "npx",
"args": ["-y", "@atoapayments/mcp"],
"env": {
"ATOA_AUTH_TOKEN": "YOUR_AUTH_TOKEN",
"ATOA_ENV": "sandbox"
}
}
}
}
```
Passing credentials via the `env` object keeps your token out of the process
argument list, which is more secure than using `--auth-token` CLI flags.
**Optional environment variables** you can add to the `env` block:
| Variable | Description |
| --------------------------- | ---------------------------------------------------- |
| `ATOA_PAYMENT_REDIRECT_URL` | URL the customer returns to after completing payment |
| `ATOA_AIS_REDIRECT_URL` | URL the user returns to after bank authorization |
Switch `ATOA_ENV` from `sandbox` to `production` (and swap your token) when you go live.
Once configured, restart your AI client and ask:
```
"Show me my Atoa stores"
```
If everything is set up correctly, the assistant will call `get_stores` and return your store list.
***
## Available Tools
Browse the **29 tools** below, organized into 8 categories. Click any category to expand.
List all active stores linked to your merchant account
Create a new payment request (returns a payment link and QR code)
Cancel a pending payment request
Check the current status of a payment
Retrieve transaction history with filters (date range, status, customer)
Register a new customer β requires either `email` or `phoneNumber`
List all customers with pagination
Get details for a specific customer
Update customer information
Remove a customer record
List saved payment methods (cards) for a customer
Get details of a specific saved card
Remove a saved card
Charge a previously saved card β returns `AUTHORIZED`, not `COMPLETED`
Settle a pre-authorized payment (required for `MANUAL_CAPTURE`)
Void a pre-authorization or unsettled card payment
List refunds for a given payment
Start a refund for a `COMPLETED` payment (full or partial)
Cancel an `INITIATED` refund β **production only**
List all registered webhook endpoints
Register a new webhook endpoint (supports OAuth 2.0 or Basic Auth)
Remove a webhook endpoint
Start the bank authorization flow β returns a URL the user must visit
List authorized bank accounts (requires completed authorization)
Get sort code, account number and bank name for an account
Get the current balance for an account
Retrieve paginated transactions for an account
Revoke access to one account, multiple accounts, or an entire session
List all supported banking institutions
Some tools are guarded by your merchant's capabilities. For example, card-on-file tools require card payments to be enabled, AIS tools require AIS bank feed access, and tipping requires tipping to be enabled. Contact `hello@paywithatoa.co.uk` to check which features are active on your account.
***
## Core Workflows
The simplest flow β create a payment link and check when it's completed.
```
"Create a payment of Β£25 for order #12345"
```
The assistant will:
1. Call `get_stores` to resolve your default store
2. Call `process_payment` with `amount: 25, currency: "GBP"`
3. Return a `paymentLink` and `qrCodeUrl` you share with the customer
4. The customer opens the link or scans the QR code to pay via their bank app
`amount` must be a positive number with no more than 2 decimal places (e.g.,
`25.99` β
β `25.999` β). Currency defaults to `"GBP"`.
**Poll for completion:**
```
"What's the status of payment abc-123-def?"
```
The assistant calls `get_payment_status` and returns one of:
| Status | Meaning |
| ------------ | ------------------------------------------------ |
| `PENDING` | Awaiting customer action |
| `AUTHORIZED` | Card pre-authorization held (card payments only) |
| `COMPLETED` | Payment settled β eligible for refund |
| `FAILED` | Payment attempt failed |
| `EXPIRED` | Customer didn't act in time (default: 30 min) |
| `CANCELLED` | Payment was cancelled |
This two-step flow saves a card during a regular payment, then charges it in future without the customer re-entering details.
**Step 1 β First payment (saves the card)**
```
"Create a Β£10 payment for customer cust_abc123, and save their card"
```
The assistant calls `process_payment` with `savePaymentMethod: true` and `atoaCustomerId: "cust_abc123"`. After the customer completes the payment, their card is stored.
`atoaCustomerId` is **required** when `savePaymentMethod` is `true`. The
customer must exist first β create them with `create_customer` if needed.
**Step 2 β List saved cards**
```
"List the saved cards for customer cust_abc123"
```
The assistant calls `list_payment_methods` and returns the saved cards with masked numbers.
**Step 3 β Charge the saved card**
```
"Charge Β£50 to card pm_xyz for customer cust_abc123, auto-settle it"
```
The assistant calls `charge_saved_card` with:
| Parameter | Value | Meaning |
| ------------- | ----------------------- | ---------------------------------------------------------- |
| `captureType` | `AUTO_CAPTURE` | Charge settles immediately |
| `captureType` | `MANUAL_CAPTURE` | Charge is held β you must call `capture_payment` to settle |
| `captureType` | `CAPTURE_BEFORE_EXPIRY` | Held authorization auto-settles before it expires |
`charge_saved_card` always returns status `AUTHORIZED`, not `COMPLETED`. For
`AUTO_CAPTURE`, settlement happens automatically. For `MANUAL_CAPTURE`, you
**must** call `capture_payment` to collect the funds β otherwise the
authorization expires and no money moves.
**Step 4 β Capture (MANUAL\_CAPTURE only)**
```
"Capture payment pay_123"
```
The assistant calls `capture_payment` with the `paymentRequestId` from step 3.
```
"Refund Β£15 from payment pay_abc-123"
```
The assistant calls `initiate_refund`. Key constraints:
* Payment must be in `COMPLETED` status β pending, expired, or cancelled payments cannot be refunded
* Partial refunds are supported β `amount` must be β€ the original payment amount
* The refund goes through as `INITIATED` β `PENDING` β `COMPLETED` (or `FAILED`)
* To cancel an `INITIATED` refund before it processes, use `cancel_refund` β **production only**
**Sandbox:** To simulate a failed refund, call `initiate_refund` with
`refundNotes: "FAILURE TEST"`. Use this to test your error-handling paths β
`cancel_refund` is not available in sandbox.
Webhooks notify your server when payment or refund statuses change.
```
"Register a webhook at https://mysite.com/webhooks/atoa for payment status updates, secured with basic auth username=atoa password=secret"
```
The assistant calls `create_webhook` with:
| Field | Description |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `url` | Your HTTPS endpoint |
| `event` | `PAYMENTS_STATUS`, `EXPIRED_STATUS`, or `REFUND_STATUS` |
| `authentication` | Optional β **OAuth 2.0** (`clientId`, `clientSecret`, `authUrl`) **or** Basic Auth (`username`, `password`) β not both |
You can create one webhook per event type. Register separate webhooks for
payment status and refund status if you need both.
## Troubleshooting
Your auth token is invalid, expired, or mis-formatted. Check:
1. Get a fresh token from the [Atoa Dashboard](https://dashboard.paywithatoa.co.uk/my-account/settings/api-access)
2. **HTTP mode:** confirm the `Authorization` header is `Bearer YOUR_TOKEN` (with a space after `Bearer`) and the `X-Atoa-Env` header matches your token type (`sandbox` or `production`)
3. **NPX mode:** confirm `ATOA_AUTH_TOKEN` and `ATOA_ENV` are set correctly in the `env` block of your config
4. Sandbox tokens and production tokens are **not interchangeable** β ensure you are using the right one for each environment
1. **Restart your AI client** after updating the config file β most clients load MCP config only on startup
2. Verify the config file path and key name match your client (see the config tabs above)
3. **NPX mode:** check Node.js version β `node --version` must be 18+
4. **NPX mode:** run the server manually to see errors: `npx @atoapayments/mcp`
This is normal. All card payments return `AUTHORIZED` first.
* **AUTO\_CAPTURE**: settlement happens automatically within minutes β no action needed
* **MANUAL\_CAPTURE**: you must call `capture_payment` to settle the funds
* **CAPTURE\_BEFORE\_EXPIRY**: Atoa auto-captures before the hold expires β no action needed unless you want early settlement
If you want immediate settlement with no extra step, use `captureType: "AUTO_CAPTURE"`.
The user **must complete the bank authorization** before this tool works. After calling `initiate_ais_auth`:
1. Share the `authorizationUrl` with the user
2. Wait for them to approve access in their banking app
3. Only then call `fetch_all_accounts` with the returned `accountAuthId`
In sandbox, the authorization URL may redirect immediately with mock data.
Refunds require the payment to be in `COMPLETED` status. Check the payment
status first with `get_payment_status`. You cannot refund a `PENDING`,
`EXPIRED`, `CANCELLED`, or `FAILED` payment.
This is expected β `cancel_refund` only works in production. To test refund
cancellation paths in sandbox, call `initiate_refund` with `refundNotes:
"FAILURE TEST"` to simulate a failure instead.
Some tools require specific features to be enabled on your merchant account. For example:
* **Card-on-file tools** (`charge_saved_card`, `capture_payment`, `cancel_card_payment`) require card payments
* **AIS / Bank Feed tools** require AIS bank feed access
* **Tips** on `process_payment` require tipping to be enabled
Contact `hello@paywithatoa.co.uk` to check which capabilities are active or to enable additional features.
The `get_stores` tool only returns **active** stores. If a store is disabled
in your [Atoa Dashboard](https://dashboard.paywithatoa.co.uk/), it won't
appear. Enable the store in the dashboard and retry.
If you experience timeouts connecting to `https://mcp.atoa.me`:
1. Check your internet connection
2. Verify no firewall rules are blocking outbound HTTPS (port 443)
3. Check [Atoa Status](https://atoa.instatus.com/) for service incidents
The HTTP endpoint enforces rate limiting per token. If you receive a `429` response:
1. Wait briefly before retrying the request
2. Space out sequential tool calls
3. If you consistently hit rate limits for a production use case, contact `hello@paywithatoa.co.uk`
***
## Need Help?
Contact our team at `hello@paywithatoa.co.uk` or use chat support on the [Dashboard](https://dashboard.paywithatoa.co.uk/).
# Atoa Developer Docs | Overview
Source: https://docs.paywithatoa.co.uk/overview
Atoa developer docs: integrate instant Pay by Bank and card payments using our SDKs, REST API, POS and e-commerce plugins, CLI and MCP server.
Atoa is a UK payments platform that lets businesses take instant account-to-account (Pay by Bank) and card payments in person and online. These developer docs cover everything you need to integrate: quickstart guides, SDKs for web, Flutter and React Native, POS and e-commerce plugins for WooCommerce and Magento, the full REST API reference, webhooks, the Atoa CLI, and the Model Context Protocol (MCP) server for AI agents. Use the sidebar to jump to your platform, or begin with Getting started and the Sandbox guide before going live.
A brief guide on how to use Atoa for payments.
How to integrate Atoa with your point-of-sale system.
Explore the full suite of API endpoints available.
Manage payments, customers, and webhooks from your terminal.
Connect AI assistants to Atoa using the Model Context Protocol.
# React Native SDK for Atoa Payments
Source: https://docs.paywithatoa.co.uk/react-native-sdk
Add Atoa payments to your React Native app. Install the SDK, launch checkout and handle responses, with setup steps and code samples.
## About the React Native SDK
Integrate Atoa into your React Native app in minutes using our dedicated SDK.
Easily add seamless Pay by Bank payments to new or existing React Native apps. The SDK is available on [npm](https://www.npmjs.com/package/@atoapayments/atoa-react-native-sdk), where developers are welcome to contribute and provide feedback.
## Prerequisites
Before you start, ensure the following setup is complete:
* Create an Atoa account: Sign up using our Business App or Web Dashboard.
* Generate API keys: Go to API Access inside the Atoa Business App and generate keys for Sandbox (testing) and Production (live).
* Backend readiness: You must be able to create a Payment Request on your backend server using Atoa APIs before calling the React Native SDK.
See the [Getting Started](/introduction#step-1-sign-up-for-developer-access) guide for more information.
## 1: Install the SDK
Run the following to add the Atoa SDK to your React Native project
```sh theme={null}
npm install @atoapayments/atoa-react-native-sdk
```
or
```sh theme={null}
yarn add @atoapayments/atoa-react-native-sdk
```
## 2. Install required dependencies
The SDK uses a few community libraries for its payment sheet UI. **Most React Native projects already have some of these** β only add the ones you're missing.
| Package | Used for | You likely have it if... |
| --------------------------------- | ------------------------------ | ----------------------------------------- |
| `react-native-gesture-handler` | Touch & swipe gestures | You use React Navigation or bottom sheets |
| `react-native-reanimated` | Smooth animations | You use any animation-heavy library |
| `react-native-svg` | Icons and graphics | You render SVGs anywhere in your app |
| `@gorhom/bottom-sheet` | Payment sheet modal | β |
| `@react-native-community/netinfo` | Network connectivity detection | β |
| `lottie-react-native` | Payment status animations | β |
Install all at once (skip any you already have):
```sh theme={null}
npm install @gorhom/bottom-sheet react-native-gesture-handler react-native-reanimated react-native-svg @react-native-community/netinfo lottie-react-native
```
Using Expo? Use `npx expo install` instead to ensure compatible versions:
```sh theme={null}
npx expo install @gorhom/bottom-sheet react-native-gesture-handler react-native-reanimated react-native-svg @react-native-community/netinfo lottie-react-native
```
> **Note:** `react-native-reanimated` requires a Babel plugin. If you haven't already, add `'react-native-reanimated/plugin'` to your `babel.config.js`. See the [Reanimated installation guide](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/) for details.
For usage, sample code to integrate can be found in [Example app](https://github.com/ATOAPaymentsLimited/AtoaReactNativeSDK/tree/main/demo_app).
## 3. Wrap Your App with AtoaProvider
Wrap your app root with `` to enable the Atoa payment modal. This is **required** for `AtoaSdk.pay()` to work.
```tsx theme={null}
// index.js
import { AppRegistry } from "react-native";
import { AtoaProvider } from "@atoapayments/atoa-react-native-sdk";
import App from "./App";
import { name as appName } from "./app.json";
const Root = () => (
);
AppRegistry.registerComponent(appName, () => Root);
```
Alternatively, if you already use `react-native-root-siblings` in your project, you can wrap with `` directly:
```tsx theme={null}
import { RootSiblingParent } from "react-native-root-siblings";
const Root = () => (
);
```
## 4: Generate a Payment Request (Backend)
You can generate a payment using the [payment-process](/api-reference/payment/process-payment) API, which must be called from your server while creating an order. Refer to the getting started guide to know more
```sh theme={null}
https://api.atoa.me/api/payments/process-payment
```
```sh theme={null}
curl --location --request POST 'https://api.atoa.me/api/payments/process-payment' \
--header 'Authorization: Bearer {access-secret}' \
--header 'Content-Type: application/json' \
--data-raw '{
"customerId": "",
"orderId": "",
"amount": "",
"currency": "GBP",
"paymentType": "",
"autoRedirect": "",
"consumerDetails": {
"phoneCountryCode": "",
"phoneNumber": ""
}
}'
```
Note: `redirectUrl` can be used to redirect the user back to your app after the payment is completed. Follow the information given in the handle redirection steps below
## 5. Launch the Payment Dialog
Do not display the payment widget before your backend has successfully created
a valid `paymentRequestId`.
After obtaining a valid paymentRequestId, you can now invoke the React Native SDK's Pay method to show the checkout experience:
```tsx theme={null}
const transactionDetails = await AtoaSdk.pay({
paymentId: "your-payment-request-id",
showHowPaymentWorks: false,
customerDetails: {
// pass customer details for pre-select bank
phoneCountryCode: "44",
phoneNumber: "8788899999",
email: "aaa@gmail.com",
},
env: "production",
// or 'sandbox'
onUserClose: ({
paymentRequestId,
redirectUrlParams,
signature,
signatureHash,
}) => {
// handle payment when user close the payment verification bottom sheet
console.log(
`User closed the payment for paymentRequestId: ${paymentRequestId}`,
);
},
onPaymentStatusChange: ({
status,
redirectUrlParams,
signature,
signatureHash,
}) => {
// handle payment status
console.log(`Payment Status Changed to ${status}`);
},
onError: (error) => {
// handle Atoa Mobile SDK error
console.error(`Error in Atoa Mobile SDK: ${error.message}`);
},
});
```
Pass `customerDetails` if available. It pre-selects the user's previous bank
for faster checkout.
#### SDK Parameters
| Parameters | Type | Required | Description |
| :-------------------- | :-------------- | :------- | :------------------------------------------------- |
| env | AtoaEnv | Yes | Environment: `'sandbox'` or `'production'` |
| paymentId | String | Yes | Unique payment request ID |
| showHowPaymentWorks | Boolean | Yes | Display onboarding sheet for users |
| customerDetails | CustomerDetails | No | Mobile + Email to fetch last-used bank |
| onError | Callback | Optional | Called on SDK errors |
| onPaymentStatusChange | Callback | Optional | Called when the payment status updates |
| onUserClose | CallBack | Optional | Called when user closes payment verification sheet |
Below are the payload structures you can expect inside each callback:
#### onError
Triggered when an error occurs during the payment process
```tsx theme={null}
{
AtoaException error // Error object which contains error message,
}
```
#### onPaymentStatusChange
Triggered when the payment status updates. Handle COMPLETED, PENDING and FAILED statuses.
```tsx theme={null}
{
status: string, // Current payment status
signature?: string, // Atoa signature for verification (optional)
signatureHash?: string, // Atoa signature hash for verification (optional)
redirectUrlParams?: Record, // Additional callback parameters (optional)
}
```
#### onUserClose
Triggered when the user closes the payment verification sheet
```tsx theme={null}
{
paymentRequestId: string, // The payment request ID
signature?: string, // Atoa signature for verification (optional)
signatureHash?: string, // Atoa signature hash for verification (optional)
redirectUrlParams?: Record, // Additional callback parameters (optional)
}
```
## 6. Handle Payment Response
You can handle the payment success, failure, pending and other statuses based on payment response
```tsx theme={null}
if (transactionDetails) {
if (isCompleted(result)) {
// handle success
} else if (isPending(result)) {
// handle pending
} else if (isFailed(result)) {
// handle failure
}
} else {
// Bottom sheet was dismissed
}
```
Sample response can be seen [here](/introduction#step-3-handle-payment-status).
## 7. Handle Redirection (Deep link)
While calling payment-process API to generate a payment, you can specify a redirectUrl in your request body. The redirectUrl, which should be passed as body parameters, redirects to your website and then opens your app via deep linking. This enables users to open your application after payment.
Following the completion of the payment by the customer's bank app, they will be redirected back to their browser. Atoa will transmit the idempotency id, amount and redirect URL with status as query parameters appended to your specified Redirected URL, forming a URL structure like:
```sh theme={null}
www.yourdeeplink.me?idempotency=ATOA783838728372&redirectUrl=https://pay.me&amount=2
```
Resources For Implementing deep-linking for your app
* [React Native Linking](https://reactnative.dev/docs/linking)
* [Android App Links](https://developer.android.com/training/app-links)
* [iOS Universal Links](https://developer.apple.com/documentation/xcode/allowing-apps-and-websites-to-link-to-your-content)
### Subscribe to webhook (Recommended)
Atoa uses webhooks to notify your application whenever an event happens in your account. Webhooks are particularly useful for events such as changes in payment status, such as completion, failure, or pending.
To get started, you must [Register your webhook endpoint](/api-reference/webhook/create-webhook-event) so Atoa knows where to deliver events.
After registration, your endpoint will start receiving detailed webhook payloads. These will inform you of payment status updates, such as `COMPLETED`, `PENDING`, or `FAILED`.
### Store Fields in Server
Store the following in your backend once the transaction is complete.
```sh theme={null}
"signatureHash": string
"paymentIdempotencyId": string
```
## 8. Check Bank App Installation (Android/iOS Requirements)
Our mobile SDK checks if the bank(using for making payments) app is installed or not. For that, you need to add 'queries' tag for android and 'LSApplicationQueriesSchemes' key for iOS
In Android, you need to add 'queries' tag in `AndroidManifest.xml`
```xml theme={null}
```
In iOS, you need to add 'LSApplicationQueriesSchemes' key in `Info.plist`
```xml theme={null}
LSApplicationQueriesSchemes
pulsesecure
launchbmb
lloyds-retail
hsbc-pwnwguti5z
uk.co.santander.santanderUK
fb894703657238109
bos-retail
halifax-retail
monzo
starlingbank
tsbmobile
comfirstdirectbankingonthego
launchFT
virginmoneyimport
ybssavings
transferwise
tg
BOIOneAPP
ie.aib.mobilebanking
bos-commercial
chase-international
revolut
lloyds-commercial
dbbukmobileapp
nationwide
ydl
```
## 9. Verify Payment Signature
This is a mandatory step to confirm the authenticity of the details returned to the Checkout form for successful payments.
You can verify the signature by doing [this](/introduction#step-4-verify-signature).
## 10: Test in Sandbox
Use the sandbox for testing different flows
```tsx theme={null}
const result = await AtoaSdk.pay({
paymentId: "your-payment-request-id",
env: "sandbox",
showHowPaymentWorks: false,
// other config properties
});
```
Refer to the [Sandbox Guide](/atoa-sandbox) to simulate different outcomes.
## 11: Brand & Customize (Optional)
**Theme Color:** Choose your brand's hex color in the dashboard. The widget will automatically reflect your theme color. [Update your theme color here](https://dashboard.paywithatoa.co.uk/add-ons/custom-branding)
**Design Guidelines:** Refer to our official branding guide to correctly showcase "Pay by Bank" in your checkout. [View Figma file](/design-system)
**Note:** You do **not** need to pass the theme color in the SDK. Simply configure these once from your Dashboard or Atoa Business App
# Remote Payments on a PAX Terminal
Source: https://docs.paywithatoa.co.uk/remote-payments
Trigger a card or Pay by Bank payment on your own PAX terminal through the Atoa API. Set up remote payments and process your first charge.
If card or terminal payments are not enabled on your account, please [contact
support](https://help.paywithatoa.co.uk).
## Introduction
Remote Payments let a merchant trigger a payment on their own **PAX terminal**
via the API. You call the Atoa API with
your SDK API key, choose one of your registered terminals, and the terminal
prompts the customer to pay β by **card** or **Pay by Bank**.
## Prerequisites
Before you start, make sure the following are in place:
* **An Atoa SDK API key.** Generate it from the Atoa Merchant Dashboard under API
Access. Call the Remote Payments endpoints from your **server**.
* **A registered PAX terminal with remote payments enabled.** Terminals are
**provisioned by Atoa**. On the terminal, open the **Remote Payments** page in
the PAX app's **Settings** view and make sure it shows **Connected** β that
confirms the terminal can receive remote payments.
## Authentication
All Remote Payments endpoints authenticate with your SDK API key as a bearer
token:
```
Authorization: Bearer
```
The merchant is resolved from the API key you authenticate with.
## End-to-end flow
Call [List Terminals](/api-reference/remote-payments/list-terminals)
(`POST /api/terminal/list`) to fetch the terminals registered
against your merchant account. Pick one that is enabled for remote payments
and has a push-notification device (`tokenMissing: false`). Send a
`storeIds` array in the request body to filter to terminals in specific
stores; omit it to list terminals across all your stores. Live `online` /
`lastActive` presence is resolved by default β send `availability: false` to
skip it.
Call [Process Remote
Payment](/api-reference/remote-payments/process-remote-payment)
(`POST /api/terminal/process-remote-payment`) with the terminal `id`, the
`amount`, and optionally the `paymentMethod` (one of `CARD` or
`PAY_BY_BANK` β if omitted or set to any other value, the terminal shows
both), `notes`, and `consumerDetails`.
On success the endpoint returns a `paymentRequestId`, and the target
terminal prompts the customer to complete the payment.
Use the `paymentRequestId` to follow the payment to a terminal state. There
are two ways to do this, and you can use both:
* **Poll the status endpoint** β call [Get Payment
Status](/api-reference/payment/get-payment-status)
(`GET /api/payments/v1/payment-status/:id`) with the `paymentRequestId`.
Poll at intervals (an [exponential
back-off](https://en.wikipedia.org/wiki/Exponential_backoff) is
recommended) until the status is terminal.
* **Receive the webhook** β subscribe to the [`POS_PAYMENT_STATUS`
webhook](/api-reference/webhook/pos-payment-status-webhook-response), which is
pushed to your endpoint when the terminal payment reaches a terminal
state. This requires a [V2 signing
secret](/api-reference/webhook/v2-signing) (a `whsec_` secret) β you must
generate one before you can subscribe to `POS_PAYMENT_STATUS`.
## Errors
For the full list of error responses on the create call β `400`, `401`, `404`,
`500` and when each occurs β see the error table on [Process
Remote Payment](/api-reference/remote-payments/process-remote-payment).
## Troubleshooting
Restart the terminal app so it reconnects, then re-check Settings β Remote payments.
The terminal is switched off or offline, or it's busy with another payment.
Make sure it's on and shows **Connected**, finish or clear any payment already
on screen, then send again.
The terminal only shows a new request when it isn't already handling a payment. If it's free,
it switches to the latest request you send. But if a payment is already in progress β including
an in-person payment on the terminal β the new request is **silently cancelled**, with no
status update and no webhook. Wait for the current payment to finish, or clear it on the
terminal, before sending again.
## Need help?
Email us at [hello@paywithatoa.co.uk](mailto:hello@paywithatoa.co.uk) or reach out via the dashboard's chat
support.
# Web Client SDK for Atoa Payments
Source: https://docs.paywithatoa.co.uk/web-client-sdk
Add Atoa Pay by Bank and card payments to your website with the Web Client SDK. Install, initialise and launch checkout, with examples.
The Atoa Web Client SDK lets you embed a seamless payment experience into your web checkout in minutes. It provides a simple interface for a payment dialog supporting both pay-by-bank and card payments (Visa, Mastercard, Apple Pay and Google Pay), handling the entire flow securely and efficiently β whether you're building a custom payment gateway or enhancing an existing one.
If you encounter any issues or need help debugging, please refer to our public [GitHub repository](https://github.com/ATOAPaymentsLimited/AtoaWebClientSDK) for detailed setup instructions and troubleshooting steps.
## Step 1: Install the SDK
Choose one of two ways to use the SDK:
### a. Install via NPM
```bash theme={null}
npm install @atoapayments/atoa-web-client-sdk
```
### b. Use via CDN (Script Tag)
```html theme={null}
```
## Step 2: Generate a Payment Request (Backend)
You must first create a payment request from your backend using the following [API](/api-reference/payment/process-payment):
```http theme={null}
POST https://api.atoa.me/api/payments/process-payment
```
To know more, follow the [Getting Started](/introduction#step-1-sign-up-for-developer-access) guide for backend integration, which includes payment request generation, webhook handling, and signature verification.
## Step 3: Initialize and Launch the SDK
> **Note:** To use the Atoa Web Client SDK, you must first whitelist your domain. Set your domain or subdomain (e.g. `https://pay.yourbrandname.co.uk`) via the Atoa Dashboard or mobile app: [Whitelist your domain here](https://dashboard.paywithatoa.co.uk/my-account/settings/api-access)
Here's how to launch the SDK on a button click after generating the paymentRequestId:
```typescript theme={null}
import { AtoaWebSdk } from "@atoapayments/atoa-web-client-sdk";
const sdk = new AtoaWebSdk({
environment: "PRODUCTION", // Use 'SANDBOX' for testing
paymentRequestId: "ad90e280-5e42-4917-9237-aa1cb2b829sd", // Required
customerDetails: {
phoneCountryCode: "44",
phoneNumber: "1234567890",
email: "test@mail.com",
}, // Optional β enables one-click payments for return users
onClose: (data) => {
// Triggered when the payment dialog is closed
},
onUserCancel: (paymentRequestId) => {
// Triggered when user cancels before completing the payment
},
onPaymentStatusChange: (data) => {
if (data.status === "COMPLETED") {
// Redirect to success page
} else if (data.status === "PENDING") {
// Show pending state until COMPLETED
}
},
onError: (error) => {
// Handle errors in SDK loading or runtime
},
});
// Launch the checkout flow
sdk.showPaymentDialog();
```
### Example using Script Tag
```html theme={null}
```
## Step 4: Handle Payment Status
The Web SDK notifies you of real-time payment updates via the onPaymentStatusChange callback. You can display your order summary page when the payment status is `COMPLETED`.
β οΈ Important: If the payment status is PENDING, display an intermediate state in your UI and continue showing it until a COMPLETED update is received.
To track the final payment outcome beyond the client, we recommend implementing [webhooks](/api-reference/webhook/create-webhook-event) (Recommended) or [polling](/api-reference/payment/get-payment-status) (as a fallback). For full implementation details, refer to the [Getting Started](/introduction#step-1-sign-up-for-developer-access) Guide.
## Step 5: Handle Events
Here's what each event callback does:
| Event | Fires when | Description |
| ----------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `onClose` | When the payment dialog is closed, either after payment or by user. | Redirect to thank-you page or refresh cart summary. |
| `onUserCancel` | When the user manually closes the payment dialog before completing payment. | Log the event, notify the user, or offer retry. |
| `onPaymentStatusChange` | When payment status updates (COMPLETED, PENDING, FAILED) | Handle COMPLETED and PENDING in your UI. You may show an intermediate confirmation screen. |
| `onError` | If the SDK fails to load, initialize, or encounters any internal error | Display a toast or fallback UI. Consider retry logic. |
Below are the payload structures you can expect inside each callback:
#### onPaymentStatusChange(data)
Triggered when the payment status updates. Handle `COMPLETED`, `PENDING` and `FAILED` statuses.
```typescript theme={null}
{
status: "COMPLETED" | "PENDING" | "FAILED",
paymentRequestId: string,
paymentIdempotencyId?: string,
atoaSignature?: string,
atoaSignatureHash?: string,
callbackParams?: Record
}
```
#### onError(error)
Fired when thereβs an issue with SDK initialization, config, or runtime.
```typescript theme={null}
{
name: "AtoaPayWebSDKError",
message: string,
details?: any, // Any additional metadata or context
componentName?: string // Example: "PaymentDetailsView"
}
```
#### onClose(data)
Called when the user closes the payment dialog after any action.
```typescript theme={null}
{
status: string, // e.g., "COMPLETED", "FAILED"
paymentRequestId: string,
paymentIdempotencyId?: string,
callbackParams?: Record,
atoaSignature?: string,
atoaSignatureHash?: string
}
```
#### onUserCancel(paymentRequestId)
Triggered when the user closes the widget without making a payment.
```typescript theme={null}
(paymentRequestId: string)
```
## Step 6: Cleanup the SDK
Use dispose() to clear the SDK from memory and DOM.
```javascript theme={null}
sdk.dispose();
```
* Disposes the payment widget.
* Removes listeners and internal state.
* Recommended after every completed or cancelled session.
## Step 7: Test in Sandbox
To use sandbox mode, change the environment to "SANDBOX":
```javascript theme={null}
const sdk = new AtoaWebSdk({
environment: "SANDBOX",
paymentRequestId: "ad90e280-5e42-4917-9237-aa1cb2b829sd",
// ...other config properties
});
```
Refer to our [Sandbox Guide](/atoa-sandbox) to simulate **`COMPLETED`**, **`FAILED`** and **`PENDING`** payments before going live.
## Step 8: Brand & Customize (Optional)
* **Theme Color:** Choose your brand's hex color in the dashboard. The widget will automatically reflect your theme color. [Update your theme color here](https://dashboard.paywithatoa.co.uk/add-ons/custom-branding)
* **Design Guidelines:** Refer to our official branding guide to correctly showcase "Pay by Bank" in your checkout. [View Figma file](/design-system)
* **Custom Domain:** Set your domain or subdomain (e.g., pay.yourbrandname.co.uk) from the Atoa Dashboard or mobile app. [Whitelist your domain here](https://dashboard.paywithatoa.co.uk/my-account/settings/api-access)
> **Note:** You do **not** need to pass the domain or theme color in the SDK. Simply configure these once from your Dashboard or Atoa Business App
### Apple Pay Setup
To use Apple Pay with the Web SDK, please [contact Support](https://help.paywithatoa.co.uk). We'll verify your domain ownership with Apple and get everything set up for you. This won't impact your payments.
## π§ Tips & Best Practices
β
To enable **faster checkout**, Atoa uses the customer's email and mobile number to pre-select the previously used bank within our network. We recommend passing customer details in the Web SDK to support **one-click checkout** for returning users.
β
Always validate the payment status using webhooks or polling.
β
Do not display the payment widget before your backend has successfully created a valid **paymentRequestId**.
β
Before going **LIVE**, ensure to use the production access secret generated from your Business App or Web Dashboard.
β
Tipping is disabled when using the Web SDK. If tipping is part of your experience, we recommend implementing it on your end.
## π© Need Help?
If you encounter any issues or need help debugging, please refer to our public [GitHub repository](https://github.com/ATOAPaymentsLimited/AtoaWebClientSDK) for detailed setup instructions and troubleshooting steps.
We're here to help β email us at [hello@paywithatoa.co.uk](mailto:hello@paywithatoa.co.uk) or talk to us from chat support from the dashboard.
# WooCommerce Payment Integration
Source: https://docs.paywithatoa.co.uk/woo-commerce
Accept Pay by Bank and card payments in WooCommerce with Atoa. Install the plugin, add your access secret and enable the gateway.
Woocommerce allows you to build and manage own online stores on top of WordPress. Atoa Pay is a payment gateway that allows customers to pay instantly for goods and services through their bank app using Open Banking. It works with all major UK banks.
## Steps to install and integrate Atoa plugin with woo-commerce
### 1. Installing the Atoa Business App
The Atoa Business app serves as the central hub for managing all your payments, APIs, integrations, and access tokens.
To get started, follow the steps below:
* **Download** the Atoa Business app from your device's Play Store or Appstore.
* Sign up with your business details to create an account. If you are new
to Atoa, you must complete the onboarding and Know Your Business (KYB) process
before you can begin using the app.
* Once signed in, access the homepage and locate the store icon on the top-left-hand side.
* Navigate to the settings tab within the app.
* Click on "API Access" to generate either the Production or Sandbox key based on your requirements.
* Simply paste your Homepage URL into the redirect URL field.
The Sandbox environment is exclusively intended for test payments with Atoa.
Refer to the provided Sandbox guide for more detailed information.
### 2 .Downloading and Installing the Atoa WordPress Plugin
Integrate the Atoa WordPress plugin into your e-commerce store by installing the [Atoa - WooCommerce](https://wordpress.org/plugins/atoa-pay/) plugin from the marketplace.
### 3. Setting up Atoa Pay on Your WordPress Account
**To set up Atoa Pay:**
* Access Settings or for WooCommerce users, navigate to WooCommerce > Payments > Atoa Pay.
* Click "Manage" and paste the API access key for Production or Sandbox.
Thats it! Atoa is now ready to accept payments. For more help, visit docs.atoa.me or email us at **`hello@paywithatoa.co.uk`**.
If you have enabled [V2 webhook signing](/api-reference/webhook/v2-signing) in the Atoa Dashboard (**Settings β Webhooks**), paste your signing secret into the plugin's **Webhook Signing Key** field and save. This ensures the plugin can verify incoming webhook deliveries. Without this step, plugin webhooks will fail signature verification after V2 signing is enabled.
**Congratulations! You have now successfully installed the Atoa WordPress plugin, and it is ready for use on your e-commerce store.**
**Atoa Sandbox environment guide**
Refer to atoa [**sandbox guide**](/atoa-sandbox) to simulate different
scenarios, such as successful payments, failed payments, and pending payments.
## Frequently asked questions (FAQs)
-
The customer adds items to their cart and proceeds to checkout.
-
At checkout, the customer see a list of payment methods, including
Atoa.
- The customer selects Atoa and the checkout amount is shown.
-
The customer is then presented with a list of their bank apps.
-
The customer selects their bank app and is securely redirected to their
bank app to complete the payment.
-
When a customer pays with Atoa Pay, the funds are sent directly from their
bank account to your merchant account. This means that you will receive the
money in a matter of seconds, not days or weeks.
-
You can easily check all of your payments in the
Transactions section of your Atoa Business app. This section
shows the payment amount, the date and time the customer made it, and
the customerβs bank account information.
- Atoa charges a flat rate of 0.6% per transaction. There are no monthly charges or setup fees. You can set up a direct debit to pay your transaction fees, or you can pay manually. To check your bill, go to My Account > Billing in the Atoa Business app. You can also manage your direct debit, usage, plan, and billing history in this section.
- Atoa sends bills on the last week of every month. The amount of your bill reflects your pricing plan and usage for the previous month. The invoice amount will be charged to your bank account on the 1st of each month.