Send a payout
A payout moves USD from your balance to a recipient’s bank account in their currency. One call prices and sends it.
- Optionally show a price with
GET /payments/organizations/{orgId}/rates. - Send with
POST …/payouts. - Read the outcome from
GET …/orders/{payoutId}or thepayout.*webhooks.
To lock a rate before money moves, you can
quote, then accept instead of step 2. That path is closed
once your organization has caps or approvals; use POST /payouts and
approvals for review.
-
Show a price
Section titled “Show a price”GET /payments/organizations/{orgId}/rates curl -s "$AVVIO_BASE_URL/payments/organizations/$AVVIO_ORG_ID/rates?from=USD&to=MXN&amount=200.00" \-H "x-api-key: $AVVIO_API_KEY"GET /payments/organizations/{orgId}/rates const quote = await avvio.quote({amount: '200.00',to: 'MXN',});// → { sourceAmount, destinationAmount, fee, rate, limits, indicative: true }GET /payments/organizations/{orgId}/rates import osimport requestsres = requests.get(f"{os.environ['AVVIO_BASE_URL']}/payments/organizations/{os.environ['AVVIO_ORG_ID']}/rates",headers={"x-api-key": os.environ["AVVIO_API_KEY"],},params={"from": "USD","to": "MXN","amount": "200.00"},)print(res.status_code, res.json())GET /payments/organizations/{orgId}/rates avvio-payments quote --amount 200.00 --to MXNThe price needs no recipient and creates nothing. Keep
destinationAmount.amountas the next step’sexpectDestination. -
Send the payout
Section titled “Send the payout”POST /payments/organizations/{orgId}/payoutsx-api-key: avvio_live_…Idempotency-Key: c94b21fa-36e2-411a-9f5b-91f81d11234aContent-Type: application/json{"amount": "200.00","destinationAccountId": "sbx_acct_MXN_4471_ae66cbc5","expectDestination": "3384.65","maxDriftBps": 200,"reference": "PAYROLL-2026-0042","endUser": {"id": "customer_42","name": "Ana López"}}{"payoutId": "sbx_pay_7c1c0f2e-6d9b-4a3e-9d1a-2f4b8c9e0a11","status": "pending","sourceAmount": { "currency": "USD", "amount": "200.00" },"destinationAmount": { "currency": "MXN", "amount": "3384.65" },"destinationAccountId": "sbx_acct_MXN_4471_ae66cbc5","fee": { "currency": "USD", "amount": "1.02" },"rate": "16.923250","reference": "PAYROLL-2026-0042","endUser": { "id": "customer_42", "name": "Ana López" },"createdAt": "2026-08-20T14:03:11.000Z","updatedAt": "2026-08-20T14:03:11.000Z","completedAt": null}An accepted payout answers
200withstatus: "pending". The fee is deducted from the send, sodestinationAmount = sourceAmount × rate, whererateis the effective rate after the fee, not the mid-rate from/rates.feeisnullwhen the network has not disclosed one.Rate protection
Section titled “Rate protection”expectDestinationis the destination amount you showed the user.maxDriftBpsis how far the rate may slip, in basis points (default200, or 2%). Drift is checked only when you sendexpectDestination. A payout that drifts further is refused with400 RATE_DRIFT_EXCEEDEDand nothing is debited.Purpose of payment
Section titled “Purpose of payment”INR, GHS, CNY and BRL payouts need
purposeOfPayment: onPOST /payouts, or, on the two-step path, onPOST …/quotes/offramp(the accept call takes nopurposeOfPayment). Pick a value from the currency’s list; we never default one:GET /payments/organizations/{orgId}/payment-reasons?currency=MXNThe response is
{ payment_reasons: [...], default? }. A missing or unlisted value is refused with400 VALIDATION_ERRORbefore anything is priced. So ispaymentReasononPOST /payouts, where it does not exist; onPOST …/quotes/acceptit is an optional free-text field, andpurposeOfPaymentthere is refused as unknown.Approvals (
Section titled “Approvals (202)”202)Above your organization’s approval threshold,
POST /payoutsanswers202and nothing is priced or sent:{ "status": "pending_approval", "approvalId": "cmf3k2xf90008q8b7q4r6s8tu", "requiredApprovals": 2, "expiresAt": "2026-09-04T10:00:00.000Z" }Wait for
payout_approval.executed, which carries thepayoutId. A replay of the same key returns the same202(Approvals).Caps and refusals
Section titled “Caps and refusals”Refusal When 422 PAYOUT_LIMIT_ EXCEEDED Over one of your USD caps: per payout, per day, or per end user per day 422 PAYOUT_REFUSEDScreening refused it 400 INSUFFICIENT_BALANCE Larger than your USD balance; no quote is used up The per-end-user cap counts against
endUser.idand makes it required. Caps and screening apply toPOST /payouts, batches and payout links. Quote-then-accept is refused outright with403 USE_POST_PAYOUTSonce your organization has any cap or approval threshold. Errors lists the rest. -
Check the outcome
Section titled “Check the outcome”GET /payments/organizations/{orgId}/orders/{payoutId} curl -s "$AVVIO_BASE_URL/payments/organizations/$AVVIO_ORG_ID/orders/$payoutId" \-H "x-api-key: $AVVIO_API_KEY"GET /payments/organizations/{orgId}/orders/{payoutId} const order = await avvio.getPayout(payout.payoutId);// pending → processing → completed, and sometimes back to failedGET /payments/organizations/{orgId}/orders/{payoutId} import osimport requestspayoutId = "<payoutId>"res = requests.get(f"{os.environ['AVVIO_BASE_URL']}/payments/organizations/{os.environ['AVVIO_ORG_ID']}/orders/{payoutId}",headers={"x-api-key": os.environ["AVVIO_API_KEY"],},)print(res.status_code, res.json())GET /payments/organizations/{orgId}/orders/{payoutId} avvio-payments status $payoutIdThe read is authoritative even when a webhook went missing. Track status & failures covers every status.
Quote, then accept
Section titled “Quote, then accept”A quote locks the rate until its expires_at. Nothing is debited until you
accept. Creating a quote takes no Idempotency-Key; accepting it does.
POST /payments/organizations/{orgId}/quotes/offrampx-api-key: avvio_live_…Content-Type: application/json
{ "amount": "200.00", "destinationAccountId": "sbx_acct_MXN_4471_ae66cbc5", "amountLeg": "source"}The quote may also carry purposeOfPayment (validated here, for the currencies
that need one) and sourceCurrency, the balance currency to debit. This
returns a snapshot id, a best_quote_id and expires_at (snake case). Accepting
moves the money:
POST /payments/organizations/{orgId}/quotes/acceptx-api-key: avvio_live_…Idempotency-Key: <uuid>Content-Type: application/json
{ "snapshotId": "sbx_quote_eyJkIjoic2J4X2FjY3RfTVhOXzQ0NzFfYWU2NmNiYzUiLCJzIjoiMjAwLjAwIn0", "quoteId": "sbx_quote_eyJkIjoic2J4X2FjY3RfTVhOXzQ0NzFfYWU2NmNiYzUiLCJzIjoiMjAwLjAwIn0", "type": "OFFRAMP", "reference": "INV-1092"}The accept body also takes an optional paymentReason. Its reference allows
only letters, digits, spaces, : and -.
amount is a decimal string, never a JSON number: up to six decimals on a
quote, two on a one-call payout. Quote-accept and one-call payouts refuse
near-duplicates unless you send X-Allow-Duplicate (Idempotency).
Fixing the amount the recipient receives
Section titled “Fixing the amount the recipient receives”Payouts are priced from the amount you send (amountLeg: "source"). To fix
what arrives instead, set amountLeg: "destination":
{ "amount": "5000.00", "amountLeg": "destination", "destinationAccountId": "sbx_acct_MXN_4471_ae66cbc5"}Check capabilities.exactOutput on GET /recipients/{orgId}/corridors first.
When it is false, this returns 400 EXACT_OUTPUT_UNSUPPORTED.
fees.payout on GET …/policy is the schedule your routing states before a
quote. Where published, the fee is fixedUsd + sourceAmount × bps / 10000,
rounded to the cent and deducted before conversion.
| Field | Holds |
|---|---|
bps |
Basis points of the send, for every currency not in byCurrency |
fixedUsd |
The fixed part, in USD |
byCurrency |
Overrides by destination currency |
note |
Prose; don’t branch on it |
A null number means “not published before a quote”, never zero. Some routings
price the whole fee inside each quote, and one adds the network’s charge on top
of bps. The binding figure is always fee on the quote and payout, which
matches its events and /balance_transactions row.
Was this page helpful?