Send a payout
Send 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.
/api/v1/me/payout/sendSelect a stage to see what happens.
/me/payout/send- Authorization
- Bearer ••••••••
- Accept
- application/json
Prepare the request
Use credentials for the selected environment and complete the required parameters.
Illustrative flow · no data is sent
How this endpoint works
Creates one payout under a required Idempotency-Key. The Bearer token identifies the shop; amount is in the method currency. Returns processing, completed or failed. Signed provider notifications and status reconciliation update the payout. A partial return sets partially_refunded and exposes cumulative refundedAmount; only that returned principal is credited. A full return credits the remaining principal and original fee once. Only explicit sbx_po_* methods are synthetic and do not debit funds.
Request parameters
Expand a field to see its format and requirements.
payoutMethodIdinteger
Recommended: stable numeric ID from GET /api/v1/me/payout/methods. Send this or legacy methodId; both must agree if provided.
methodIdstring | integer
Compatibility alias: the old catalog id or the numeric payoutMethodId. Existing integrations continue working.
amountnumberRequired
Amount in the method's currency. >0, ≤ your balance in that currency.
recipientobject
Destination data (account, name, …).
Integration details
Currency, behavior and additional notes for this operation.
Context and key considerations
payoutMethodId from the authenticated methods catalog. Pay-in IDs are different. The flow:- 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) — a confirmed rejection reversed the debit. Savedetails.payoutIdanddetails.failureReason. The key remains bound to that failed payout. After confirming its final status, use a new key for a new, corrected payout.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 is required (1–200 characters). Store one key per intended payment and reuse the exact body for retries. A timeout or server error does not prove failure: retain the key and query GET /api/v1/me/payout/request with that header. A 404 can mean the original request is still in flight; retry with the same key. Never generate a new key to resolve an unknown result. A changed committed body returns 409. Keys are isolated by merchant, authenticated shop and environment.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:- Every method has its own contract: use its current
recipientFields, including required fields, exact select values and help. Name, ID, phone and email requirements differ between bank and wallet methods. RespectminRequestLocal,maxRequestLocalandamountStep; the balance must cover amount plus your fees. - 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: use the account, bank, ID and contact fields returned for your assigned method; alias/CBU/CVU requirements depend on the route.
- BreB (Colombia):
accountis the registered key (up to 64 characters), with name and document fields. No bank or bank-account type is required. - Wallets (Nequi/Yape/Plin/Vita/…): use the method's phone or email format; these are not interchangeable. PayPal uses the beneficiary email. Cash collection requires the beneficiary identity and phone, followed by the collection-instructions endpoint.
Confirm QR payouts
For a method whose subtype is qrcode, POST /api/v1/me/payout/qr-preview with { payoutMethodId, account, amount } (the legacy methodId remains accepted). Display the returned preview.name, bank, document, currency and confirmedAmount for confirmation. Use that confirmed amount and send recipient.account unchanged plus recipient.qrToken = preview.token to the send endpoint. The token expires after 60 seconds and is single-use. If the send result is unknown, recover it by the original Idempotency-Key before retrieving another QR.
Download the receipt
Once status is completed, GET /api/v1/me/payout/send/{id}/proof with the same shop authorization. Save the binary application/pdf response as a file. A pending, failed or partially returned payout has no completion receipt (422). Sandbox PDFs are marked as test documents without real money. For cash collection, GET /api/v1/me/payout/send/{id}/cash-instructions; the collection object distinguishes awaiting PIN, ready, collected and expired states. Share collection details only with the beneficiary.
payoutMethodId; the public id, methodId and webhook method fields contain that same neutral ID as a string. Previously issued aliases remain accepted in requests but are no longer returned. Unregistered historical methods return null. Keep the original request body unchanged on retries, including the identifier field. Subscribe to payout.partially_refunded as well as completed/failed. Its refundedAmount is cumulative in the payout currency; reconcile the difference from the previously stored total, not the full amount on each notification. Webhook deliveries can be retried; deduplicate their event IDs.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.20042) 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 "Idempotency-Key: sandbox-payout-order-001" \
-H "Authorization: Bearer <accessToken>" \
-H "Content-Type: application/json" \
-d '{ "methodId": "20042", "amount": 1850,
"recipient": { "account": "012180012345678901", "name": "Juan Perez", "documentType": "CURP", "documentId": "PEPJ900101HDFRRN09" } }'
# → { "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 (90001, 90002, 90003, 90004) — 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 ["*"]). Subscribe to payout.partially_refunded for partial returns; refundedAmount is cumulative. Recover missing or delayed notifications by polling GET /api/v1/me/payout/send/{id}. A missing notification is never a reason to send another payout.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.
