Collect — money in
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.
Pay-link — customer present
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 backEXPIRED.- Cancel an unpaid link with
payment.cancel(paymentRequestId)→CANCELLED, reasonMERCHANT_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.