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.