Send a payout
/api/v1/me/payout/sendSend money to your beneficiaries from your payout balance. Separate environment from pay-in: you fund it, optionally swap to local currency, and send the payout.
- Fund —
POST /api/v1/me/payout/transfermoves USD from your pay-in balance to payout (atomic). - Swap (optional) —
POST /api/v1/me/payout/swapconverts USD→local currency at the current rate. - Send —
POST /api/v1/me/payout/senddebits your balance in the method's currency and creates the payout.
GET /api/v1/me/payout/balance and the rails at GET /api/v1/me/payout/methods.payout_dispatch_failed(422) — the provider rejected the beneficiary data (e.g. a wrong CLABE/CCI, a bank_code that doesn't match the account, a PIX key type mismatch).details.failureReasoncarries the exact reason and the debit was already reversed to your balance. Fix therecipientand retry with the sameIdempotency-Key— the error is not cached, so a corrected retry re-executes.payout_method_unavailable(422) — themethodIddoesn't exist. List the methods at GET /api/v1/me/payout/methods.currency_not_funded(422) — you have no balance in the method's currency. Do a USD→that-currency swap first.balance_insufficient(422) — the amount exceeds your balance in that currency. The response carriesdetails.availableUsdanddetails.shortfallUsd.amount_invalid(422) ·invalid_request(400) ·rate_limited(429).
Idempotency-Key — a retried POST never sends twice. A malformed beneficiary field never leaves you short: the debit is reversed automatically on any rejection.recipient) — discover the fields per method. Each method in GET /api/v1/me/payout/methods now carries recipientFields — the EXACT fields to send for that rail (key, label, required, type, and options for bank selectors). Read it and you never guess. The shapes by rail:- All rails:
name,documentId(beneficiary ID, sent WITHOUT dots/dashes). Optional:email. - SPEI (MX):
account= CLABE (exactly 18 digits) ·bankCodeauto-derived from the CLABE (don't send it) ·documentTypeRFC|CURP. - PIX (BR):
account= PIX key (CPF, CNPJ, email, phone or EVP) ·accountType= the key type ·documentId= CPF (11) or CNPJ (14). - Bank — Colombia / Chile / Peru:
bankCodeis a SELECTOR — send one of the exact codes in the method'srecipientFields[bankCode].options(e.g. Colombia Bancolombia1007, Peru BBVA0001). Peruaccount= CCI (20 digits), not the account number.accountType= SAVINGS|CHECKING. - Bank — Argentina:
account= CBU/CVU (22 digits); the bank is auto-derived (nobankCodeneeded). - Wallets (Nequi/Yape/Plin/…):
account= the beneficiary's phone (Colombia 10 digits, Peru 9). PayPal:account= the account email.
/api/v1/me/payout/sendsecret keyCreates a payout. The shop is identified via the Bearer token. The `amount` is in the method's currency (e.g. MXN for po_mxn_spei), and is debited from your payout balance in that currency. Returns the payout with its `status` (processing → completed/failed). When a REAL payout provider is configured (Pagsmile), the disbursement is dispatched to it and stays `processing`; its webhook finalizes it (PAID → completed; REJECTED/REFUNDED → failed and the debit is reversed to your balance). Without a real provider configured, a simulated driver completes it instantly.
methodIdstringrequiredMethod id (GET /api/v1/me/payout/methods). E.g. po_mxn_spei.amountnumberrequiredAmount in the method's currency. >0, ≤ your balance in that currency.recipientobjectDestination data (account, name, …).
curl -X POST "https://sandbox.key2pay.ai/api/v1/me/payout/send" \
-H "Authorization: Bearer <accessToken>" \
-H "Idempotency-Key: po-001" \
-H "Content-Type: application/json" \
-d '{ "methodId": "po_mxn_spei", "amount": 1850, "recipient": { "account": "012180012345678901", "name": "Juan Perez" } }'{
"payout": {
"id": "po-202606-ab12cd34",
"methodId": "po_mxn_spei",
"methodName": "SPEI",
"currency": "MXN",
"amount": 1850,
"status": "completed",
"provider": "simulated",
"providerRef": "SIM-po-202606-ab12cd34",
"environment": "sandbox",
"createdAt": "2026-06-20T18:00:00.000Z",
"completedAt": "2026-06-20T18:00:00.100Z"
},
"methodName": "SPEI",
"logoUrl": "https://api.key2pay.ai/api/payment-method-logo/payout_spei?v=2026-07-04T00:00:00.000Z",
"iconUrl": "https://api.key2pay.ai/api/payment-method-logo/payout_spei?v=2026-07-04T00:00:00.000Z"
}Testing payouts in sandbox
Sandbox payouts do NOT auto-settle and the Sandbox-Simulate header is pay-in only — it does NOT affect payouts. The POST /api/v1/me/payout/send/{id}/simulate endpoint is the completion signal: you drive each payout to its terminal status yourself. There are two ways to test:
- Discover —
GET /api/v1/me/payout/currenciesto see which currencies you can fund + pay out in (each with its rails + `funded` flag). In sandbox this includes the synthetic test currencies. - Fund —
POST /api/v1/me/payout/fund-sandboxwith{ currency, amount }credits your pay-outavailablebalance in that currency with test money (no pay-in / transfer / swap needed). Sandbox only. - Send —
POST /api/v1/me/payout/sendwith a real rail (e.g.po_mxn_spei) debits that balance and creates the payout inprocessing. - Complete / fail —
POST /api/v1/me/payout/send/{id}/simulatewith{ action }drives it tocompleted(or fails it and reverses the debit). This runs the same money logic a real provider webhook would.
# 1) Fund the sandbox pay-out balance (test money — sandbox only):
curl -X POST https://sandbox.key2pay.ai/api/v1/me/payout/fund-sandbox \
-H "Authorization: Bearer <accessToken>" \
-H "Content-Type: application/json" \
-d '{ "currency": "MXN", "amount": 5000 }'
# → { "currency": "MXN", "amount": 5000,
# "balances": [ { "currency": "USD", "available": 0, "pending": 0, "reserved": 0 },
# { "currency": "MXN", "available": 5000, "pending": 0, "reserved": 0 } ] }
# 2) Send a payout from that balance (lands as "processing"):
curl -X POST https://sandbox.key2pay.ai/api/v1/me/payout/send \
-H "Authorization: Bearer <accessToken>" \
-H "Content-Type: application/json" \
-d '{ "methodId": "po_mxn_spei", "amount": 1850,
"recipient": { "account": "012180012345678901", "name": "Juan Perez" } }'
# → { "payout": { "id": "po-202606-ab12cd34", "status": "processing", … } }
# 3) Complete it (the completion signal — sandbox payouts don't auto-settle):
curl -X POST https://sandbox.key2pay.ai/api/v1/me/payout/send/po-202606-ab12cd34/simulate \
-H "Authorization: Bearer <accessToken>" \
-H "Content-Type: application/json" \
-d '{ "action": "completed" }'
# → { "payout": { "id": "po-202606-ab12cd34", "status": "completed",
# "completedAt": "2026-06-27T18:00:00.000Z", … } }
# Drive a failure instead (reverses the debit back to your balance):
curl -X POST https://sandbox.key2pay.ai/api/v1/me/payout/send/po-202606-ab12cd34/simulate \
-H "Authorization: Bearer <accessToken>" \
-d '{ "action": "failed" }'GET /api/v1/me/payout/methods prepends synthetic test rails (sbx_po_mx_spei, sbx_po_br_pix, sbx_po_co_bank, sbx_po_pe_bank) — 4 countries. A payout created with an sbx_po_* id is SYNTHETIC: it lands in processing with NO balance debit and no real provider, so you don't even need to fund first. Then move it to any status with POST /api/v1/me/payout/send/{id}/simulate.Simulate actions: completed / paid → completed · failed / rejected / refunded / returned → failed (the debit is reversed on a real-debited payout) · processing → no-op. Both endpoints are sandbox-only — they return fund_sandbox_only / simulate_sandbox_only (400) in production.
payout.completed (beneficiary paid) or payout.failed (debit reversed, payload includes failureReason). These fire on the real provider webhook in production and when you drive a payout via simulate in sandbox, so you can test the full notification end-to-end. Subscribe on POST /webhooks with ["payout.completed","payout.failed"] (or ["*"]). You can also always poll GET /api/v1/me/payout/send/{id}.Other payout endpoints
GET /api/v1/me/payout/balance (balance per currency + transferable from pay-in) · GET /api/v1/me/payout/methods (available rails) · GET /api/v1/me/payout/currencies (distinct currencies you can pay out in, with funded + rails) · POST /api/v1/me/payout/transfer (fund from pay-in) · GET·POST /api/v1/me/payout/swap (quote/execute swap) · GET /api/v1/me/payout/send (list) · GET /api/v1/me/payout/send/{id} (get one). All in the OpenAPI viewer.