Payouts
Send funds from a wallet to a saved recipient, with currency conversion built in.
A payout debits one of your wallets and pays a recipient. If the wallet currency differs from the recipient's, MigraPay converts at the rate you can preview with GET /payouts/estimate.
Create a payout
POST /payouts requires the payment.create scope.
Prop
Type
Send an Idempotency-Key
Add an Idempotency-Key header to every payout. If the request times out, retry with the same key and body: you get the original payout back instead of paying twice. See Idempotency.
curl -X POST https://business-api.migrapay.com/v1/api/payouts \
-H "Authorization: Bearer $MIGRAPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"fromCurrency": "CAD",
"amount": 250,
"recipientId": "8d1e5b2a-6c4f-4a9e-b3d7-000000000001",
"narration": "INV-2048"
}'{
"status": "success",
"message": "Payment created successfully.",
"data": {
"transaction": {
"id": "a41c7e92-3b5d-4f08-8c6e-000000000001",
"reference": "20260812160811-K7Q2M9X4TD",
"businessId": "b7e4d2c1-5a3f-4c8e-8d2b-000000000001",
"walletId": "3f6c2a10-8b1d-4e5f-9a7c-000000000001",
"transactionType": "debit",
"businessTransactionType": "transfer",
"channel": "bank",
"status": "processing",
"statusLog": [
{ "status": "pending", "createdAt": "2026-08-12T16:08:11.000Z" },
{ "status": "processing", "createdAt": "2026-08-12T16:08:12.000Z" }
],
"amount": 250,
"netAmount": 252.5,
"fee": 2.5,
"transactionFees": [{ "feeType": "userFee", "fee": 2.5, "currency": "CAD" }],
"currency": "CAD",
"balance": 12332,
"paymentInitiated": true,
"narration": "INV-2048",
"customerEmail": null,
"customerName": null,
"metadata": {
"toCurrency": "NGN",
"toAmount": 271050,
"rate": 1084.2,
"formattedRate": "CAD 1.00 = NGN 1,084.20"
},
"paymentRequestId": null,
"recipientId": "8d1e5b2a-6c4f-4a9e-b3d7-000000000001",
"conversionPairId": null,
"approvalRequestId": null,
"createdBy": "e5b8c3d1-7f2a-4b6e-a9d4-000000000001",
"createdAt": "2026-08-12T16:08:11.000Z",
"updatedAt": "2026-08-12T16:08:12.000Z"
},
"approvalRequired": false
}
}The payout is a transaction: amount is what you asked to send, fee the fees, and netAmount the total debited from your wallet. What the recipient gets is in metadata.toAmount (metadata.toCurrency), rounded to that currency's precision.
Approvals
If your business has approval rules that match this payout, nothing is debited yet. The response has "approvalRequired": true, an approvalRequestId, and the transaction status pending_approval. Once an approver signs off in the dashboard the payout proceeds and your webhooks report the outcome.
Payout lifecycle
- pending_approvalWaiting for an approver. Only when approval rules apply.
- processingWallet debited; funds are on their way.
- successfulDelivered. payment.settled fires.
- failedNot delivered. Your wallet is credited back; payment.failed fires.
- reversedReturned after sending. Wallet credited; payment.reversed fires.
Some routes settle instantly, so a payout can already be successful in the create response. Use webhooks rather than polling to follow the rest.
Delivery time depends on the payout route and the recipient's bank or network. Use webhooks to track status changes.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | fromCurrency, amount or recipientId is missing. |
| 400 | INVALID_AMOUNT | amount is not a positive number. |
| 400 | AMOUNT_TOO_SMALL | The converted amount rounds to zero. |
| 400 | AMOUNT_BELOW_MINIMUM | Below the corridor minimum. data has min and currency. |
| 400 | AMOUNT_ABOVE_MAXIMUM | Above the corridor maximum. data has max and currency. |
| 404 | RECIPIENT_NOT_FOUND | No recipient with that id in this environment. |
| 404 | WALLET_NOT_FOUND | You have no wallet in fromCurrency. |
| 409 | IDEMPOTENCY_KEY_REUSED | The Idempotency-Key was already used with a different body. |
| 409 | IDEMPOTENCY_KEY_IN_USE | A request with this Idempotency-Key is still running. |
| 422 | INSUFFICIENT_BALANCE | The wallet cannot cover amount plus fees. |
| 422 | BUSINESS_NOT_ACTIVE | Your business is not yet approved for payouts. |
| 422 | business_restricted | Payouts are paused on your account. |
| 422 | limit_exceeded | Over a limit set on your account. data has limit, currency, value and projected. |
| 422 | RECIPIENT_INACTIVE | The recipient has been deactivated. |
| 422 | RECIPIENT_FLAGGED | The recipient cannot be paid. |
| 422 | UNSUPPORTED_CORRIDOR | Payouts to this currency, country or rail are not available. |
| 422 | RAIL_DISABLED | The recipient's payout method is switched off for this currency. |
| 422 | BANK_NOT_SUPPORTED | This bank or network cannot be paid right now. |
| 502 | PROVIDER_DECLINED | The payout was declined before it was sent. Your wallet has been credited back. |
| 502 | PAYOUT_FAILED | The receiving network declined the payout. Your wallet has been credited back. |
| 502, 503 | PROVIDER_ERROR | The payout could not be sent. Your wallet has been credited back. data.retryable says whether trying again later may help. |
Retrieve a payout
GET /payouts/{id} accepts the payout's id or its reference. Requires payment.view.
curl https://business-api.migrapay.com/v1/api/payouts/20260812160811-K7Q2M9X4TD \
-H "Authorization: Bearer $MIGRAPAY_API_KEY"{
"status": "success",
"message": "Payment retrieved successfully.",
"data": {
"transaction": { "id": "a41c7e92-3b5d-4f08-8c6e-000000000001", "status": "successful", "…": "…" },
"history": [
{ "status": "pending", "createdAt": "2026-08-12T16:08:11.000Z" },
{ "status": "processing", "createdAt": "2026-08-12T16:08:12.000Z" },
{ "status": "successful", "createdAt": "2026-08-12T16:09:40.000Z" }
]
}
}history is the payout's status timeline. Unknown ids return 404 TRANSACTION_NOT_FOUND.
Interac security question
CAD payouts sent by Interac e-Transfer carry a security question and answer. Recipients whose bank account has Interac autodeposit receive the money without it; everyone else is asked the question before they can accept the transfer. GET /payouts/{id} returns the pair as interac so you can share it with the recipient, for example in your own payout notification:
{
"status": "success",
"message": "Payment retrieved successfully.",
"data": {
"transaction": { "id": "1ac49cce-8df6-480b-9ce2-0733e081535c", "channel": "interac", "currency": "CAD", "…": "…" },
"history": [{ "status": "pending", "createdAt": "2026-10-16T12:33:25.000Z" }],
"interac": {
"securityQuestion": "What is the security code?",
"securityAnswer": "3JMNBX5"
}
}
}If the recipient was saved with its own securityQuestion and securityAnswer, that pair is used; otherwise one is generated for each payout. The pair is fixed once the payout is sent and stays the same if it is retried. interac is absent for every other payout, and only this endpoint returns it (never lists or webhooks). Treat the answer like a password: share it with the recipient only.
List payouts
GET /payouts returns your 100 most recent payouts, newest first, as data.payments. Requires payment.view. It takes no filters; to filter or page further back, use GET /transactions?activity=transfer.
curl https://business-api.migrapay.com/v1/api/payouts \
-H "Authorization: Bearer $MIGRAPAY_API_KEY"{
"status": "success",
"message": "Payments retrieved successfully.",
"data": { "payments": [{ "id": "a41c7e92-3b5d-4f08-8c6e-000000000001", "…": "…" }] }
}Older integrations
/payments is an alias of /payouts (same requests, same responses). POST /payouts also accepts the older field names currency or sourceCurrency for fromCurrency, sourceAmount for amount and beneficiaryId for recipientId. New integrations should use /payouts and the names above.