Send — money out
Every send happens under a SEND contract: the account owner authorises it once at their bank, and Atoa enforces its per-payment and period caps on every draw.
Set up the contract
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
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.
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)
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
The business owner approves 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.
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
To keep the owner in your own web UI, embed the approval with the
Approvals SDK instead of sharing the URL — pass nextAction.clientSecret to
confirmApproval. Declined payments read back FAILED with failureReason: APPROVAL_DECLINED; lapsed,
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.
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)
Update or revoke
contract.update returns a fresh authorizationUrl — the existing limits stay enforced until the owner
re-approves. contract.revoke is terminal; further sends are rejected.
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)
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.