# 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. Atoa Bank Feed 'Authorise your bank' screen: pick your bank to grant account access, with a 90-day auto-renewing consent option. 🧠 **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. A chat thread drawn as grey placeholder bubbles, with the Atoa checkout rendered inside one of them and outlined as your checkout div. A chat thread drawn as grey placeholder bubbles, with the Atoa checkout rendered inside one of them and outlined as your checkout div. ## 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. Four checkout screens in a grid: the bank picker, an approval with Face ID, contract setup with saved cards, and the add-a-card form. Four checkout screens in a grid: the bank picker, an approval with Face ID, contract setup with saved cards, and the add-a-card form. | `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'`. | Four wireframe hosts: a chat thread, an in-app assistant panel, a mobile sheet, and a plain inline card, each with the checkout slot marked. Four wireframe hosts: a chat thread, an in-app assistant panel, a mobile sheet, and a plain inline card, each with the checkout slot marked. ### 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. An agent panel feeding the Atoa payment rail, which collects money in from a customer and sends money out to a payee. An agent panel feeding the Atoa payment rail, which collects money in from a customer and sends money out to a payee. ## 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. Your agent calls Atoa, which enforces consent and caps, then collects money in from a customer or sends money out to a payee. Your agent calls Atoa, which enforces consent and caps, then collects money in from a customer or sends money out to a payee. ## Collect or send Decision tree: which way is the money going? Receiving splits on whether the customer is here now β€” yes is a pay-link (payment.collect, no contract), no is an off-session charge (payment.collect under a COLLECT contract). Sending is payment.send under a SEND contract the owner authorises once. Decision tree: which way is the money going? Receiving splits on whether the customer is here now β€” yes is a pay-link (payment.collect, no contract), no is an off-session charge (payment.collect under a COLLECT contract). Sending is payment.send under a SEND contract the owner authorises once. * **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. Logos of major UK banks supported by Atoa, including Barclays, Lloyds, HSBC, NatWest, Santander, Monzo, TSB, first direct, Wise and Virgin Money. 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 [Run in Postman](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: Atoa QR code shown on a self-service kiosk screen and printed on a customer receipt to scan and pay by bank. **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. Online checkout showing an Atoa 'Pay by bank app' payment option, with three checkout button variants. # 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. Atoa SDK Flow ## 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.