Two shapes: the customer is present and pays a link, or they’re not and you charge under a COLLECT contract they approved once. No card or method parameters in either — Atoa settles with the method on file.

req = atoa.payment.collect(
    amount={"amount": 45.00},    # decimal major units; GBP by 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
  • 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.

customer = atoa.customer.create(
    full_name="Ada Lovelace",
    email="[email protected]",      # 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)

Atoa enforces the limits on every charge. Check headroom before charging instead of probing with a failure:

usage = atoa.contract.get(contract.contract_id).usage or []
month = next((u for u in usage if u.period == "MONTH"), None)
print(month.remaining)           # headroom this window; over-cap charges fail LIMIT_EXCEEDED

SCA on a charge

An off-session charge doesn’t settle on its own. It returns an approval action — nextAction (TS) / next_action (Python) — and the customer approves on Atoa’s page with a one-time code or a passkey. Your app never sees the credential.

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

To keep the approver in your own web UI, embed the approval with the Approvals SDK instead of sharing the URL — pass nextAction.clientSecret to confirmApproval. Declined reads back FAILED with failureReason: APPROVAL_DECLINED; lapsed, APPROVAL_EXPIRED — returned on the Payment, not thrown. In sandbox, open the approvalUrl and force whichever decision you want to test.

Refunds

Refund a COMPLETED collect, in full or in part. A refund moves INITIATED → COMPLETED (or FAILED).

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

A pending refund can be cancelled with cancelRefund(refundId). Sandbox: set reason to "FAILURE TEST" to force a FAILED refund and exercise that branch.