Skip to main content
Every gated payment — a payout or an off-session 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 companion to the nextAction you get back from payment.send / payment.collect — an alternative to delivering 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); this SDK is just a nicer surface for the same approval, not a separate one.

Install

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.

Options

confirmApproval(options)ApprovalHandle.

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.
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. 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 }: 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 — a backdrop tap, a swipe-away, or the component unmounting. 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.