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.
methodIdstringRequired
Method id (GET /api/v1/me/payout/methods). E.g. po_mxn_spei_pagsmile.
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
- 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 { methodId, account, amount }. 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.
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.po_mxn_spei_pagsmile) 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": "po_mxn_spei_pagsmile", "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 (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 ["*"]). 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.
