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"
  }'
201 Created
{
  "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

  1. pending_approvalWaiting for an approver. Only when approval rules apply.
  2. processingWallet debited; funds are on their way.
  3. successfulDelivered. payment.settled fires.
  4. failedNot delivered. Your wallet is credited back; payment.failed fires.
  5. 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

StatusCodeWhen
400VALIDATION_ERRORfromCurrency, amount or recipientId is missing.
400INVALID_AMOUNTamount is not a positive number.
400AMOUNT_TOO_SMALLThe converted amount rounds to zero.
400AMOUNT_BELOW_MINIMUMBelow the corridor minimum. data has min and currency.
400AMOUNT_ABOVE_MAXIMUMAbove the corridor maximum. data has max and currency.
404RECIPIENT_NOT_FOUNDNo recipient with that id in this environment.
404WALLET_NOT_FOUNDYou have no wallet in fromCurrency.
409IDEMPOTENCY_KEY_REUSEDThe Idempotency-Key was already used with a different body.
409IDEMPOTENCY_KEY_IN_USEA request with this Idempotency-Key is still running.
422INSUFFICIENT_BALANCEThe wallet cannot cover amount plus fees.
422BUSINESS_NOT_ACTIVEYour business is not yet approved for payouts.
422business_restrictedPayouts are paused on your account.
422limit_exceededOver a limit set on your account. data has limit, currency, value and projected.
422RECIPIENT_INACTIVEThe recipient has been deactivated.
422RECIPIENT_FLAGGEDThe recipient cannot be paid.
422UNSUPPORTED_CORRIDORPayouts to this currency, country or rail are not available.
422RAIL_DISABLEDThe recipient's payout method is switched off for this currency.
422BANK_NOT_SUPPORTEDThis bank or network cannot be paid right now.
502PROVIDER_DECLINEDThe payout was declined before it was sent. Your wallet has been credited back.
502PAYOUT_FAILEDThe receiving network declined the payout. Your wallet has been credited back.
502, 503PROVIDER_ERRORThe 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"
200 OK
{
  "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:

200 OK (CAD Interac payout)
{
  "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"
200 OK
{
  "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.