NGN payouts
Send NGN to any Nigerian bank account, settled over the Nigerian interbank rails (NIP). Recipient names are auto-resolved from the account number, so you only send account_number + bank_code.
New to payouts? Start with the Payouts overview for the common fields, customer attribution, and management endpoints shared across every currency.
Want the exact fee before you send? Preview it with POST /v1/payouts/quote, an advisory, read-only fee + cost breakdown for your account.
The payout object
{
"object": "payout",
"id": "ckxxxxxxxxxxxxxxxxxx",
"reference": "po_xxxxxxxxxxxx",
"status": "paid",
"currency": "NGN",
"amount_minor": "500000",
"fee_minor": "75",
"tax_minor": "0",
"total_debit_minor": "500075",
"recipient_name": "ADAEZE BLESSING NWAFOR",
"recipient_account": "0690000032",
"recipient_bank_code": "044",
"wallet_id": "ckwallet_xxx",
"nip_reference": "100004250505000123456789",
"merchant_reference": "ORDER_001",
"narration": "Payroll April 2026",
"customer_id": null,
"customer_reference": null,
"batch_id": null,
"failure_code": null,
"failure_message": null,
"created_at": "2026-05-05T12:34:50.123Z",
"queued_at": "2026-05-05T12:34:50.456Z",
"processing_at": "2026-05-05T12:34:51.789Z",
"completed_at": "2026-05-05T12:34:56.789Z"
}Status values
| Status | Description |
|---|---|
draft | Created via dual-control flow; awaiting team approval |
queued | Approved/auto-approved; awaiting dispatch. Also used while we confirm a payout whose send to the provider had an unclear result (see below) |
processing | Dispatched to provider; awaiting confirmation |
paid | Funds reached the recipient |
paid_manual | Manually marked paid by Technest support after evidence review |
failed | The payout did not go through. A single payout that had already been debited is refunded and moves on to reversed. A bulk payout row stays failed; any amount taken for it has already been put back in your wallet |
failed_manual | Manually marked failed by Technest support |
reversed | Your wallet was refunded (principal + fee + tax) because the provider failed or returned the payout, or the receiving bank sent a paid payout back |
cancelled | Cancelled before dispatch |
awaiting_admin_review | Held by an internal fraud rule; awaiting Technest review |
When a provider’s result is unclear
Sometimes we send a payout to a provider and cannot tell whether it went through, for example when the provider times out or answers with a temporary error after receiving the request. If we can’t confirm whether the provider received the payout, it stays queued or processing while we confirm. It is never refunded until we know the money did not move. It then moves to paid, or to reversed with a refund.
While we confirm, you receive no payout_failed event for it, and it can’t be cancelled (cancel returns 400 invalid_status). Once a held queued payout has been waiting at least 30 minutes, you can re-query it: we ask the provider, and it can only move to paid. Any other answer leaves it queued and nothing is refunded. A re-query sooner than 30 minutes returns 400 invalid_status. Don’t send the same payment again under a new reference while it is in this state, or the recipient could be paid twice. Wait for payout_paid or payout_reversed.
Maker-checker rules
Single payouts that exceed your account’s per-transaction dual-control threshold land in draft status awaiting team approval (approve via the dashboard or your internal tooling that calls our dashboard API).
Live-merchant Owner-only self-approve: on live-approved merchants, only the team member assigned the Owner role can approve their own draft payouts. All other roles drop to maker-checker on live and need a different teammate to approve, even when they hold both payout_create and payout_approve. Sandbox keeps dual-perm self-approve for development velocity. The Owner role is capped at 3 holders per merchant.
Email-OTP gate (dashboard only)
The dashboard’s New Payout and Approve flows can require an email-OTP before submit and/or approve, configured per (merchant, env, currency).
API integrations bypass the OTP gate entirely: POST /v1/payouts authenticates via Bearer token + IP allowlist + Idempotency-Key, which is the right machine-to-machine shape. The OTP is a dashboard-only control for human-clicked actions.
POSTCreate an NGN payout
Send NGN to a Nigerian bank account. The Idempotency-Key header is required.
Minor units (kobo).
NGN
Inline recipient (below). Required unless beneficiary_id is supplied.
A saved NGN beneficiary. Alternative to inline recipient.
Auto-resolved from currency + env if omitted.
Your own ref. Unique per merchant in a 30-day window.
Shown on the recipient’s bank statement.
Sender/customer attribution, see Customer attribution.
Opaque attribution string (max 64 chars).
Bypass the beneficiary cool-down for an intentional retry.
NGN does not use sender_customer_id (that is for international-currency payouts). It can still require a sender block: if your account is configured to require sender information, that requirement applies to NGN payouts too. See Sender information.
Inline recipient
{
"account_number": "0690000032",
"bank_code": "044"
}| Field | Required | Notes |
|---|---|---|
account_number | yes | 10-digit NUBAN. |
bank_code | yes | Any code from GET /v1/banks?currency=NGN: validated against the bank list, not a length rule. Real NGN codes come in several widths: CBN 3-digit (058 for GTBank), NIBSS/NIP 5-digit (50515 for Moniepoint) and 6-digit (000013 for GTBank). Send whichever the bank publishes; Swappr canonicalizes it internally. |
bank_name | no | Optional hint. If omitted, Swappr resolves the canonical name and stores it for dashboards + receipts. |
name | no | Optional. Auto-resolved via NUBAN against the bank-of-record. |
Swappr canonicalizes whichever code form you send to the 3-digit CBN form internally before dispatch. A bank_code that no bank in the list carries returns 422 unknown_bank_code with the input echoed; a non-numeric bank_code returns 400 invalid_field.
Tip: call GET /v1/banks?currency=NGN once at integration time, cache the response (it returns both cbn_code and nip_code per bank), and look up by name when building payloads. Daily refresh is plenty.
Errors
| Code | HTTP | Cause |
|---|---|---|
missing_field | 400 | Required body field absent |
invalid_field | 400 | Wrong type or format of a body field (e.g. a non-numeric bank_code) |
unknown_bank_code | 422 | bank_code not found in the bank list |
wallet_not_found | 422 | wallet_id doesn’t match merchant + currency + env |
recipient_unresolvable | 422 | The account name could not be resolved from the account number + bank code |
beneficiary_cooldown | 429 | Same recipient paid recently, pass allow_duplicate: true if intentional |
limit_violation | 422 | Per-tx / daily / per-beneficiary cap exceeded; see error.detail.kind |
fraud_rule_blocked | 422 | Internal fraud rule blocked the payout; see error.detail.hits |
beneficiary_blacklisted | 403 | Recipient is on your merchant blacklist, un-blacklist them on the Beneficiaries page, or send to a different recipient |
beneficiary_globally_blocked | 403 | Recipient is on the platform-wide blacklist and cannot be overridden, contact support |
wallet_frozen | 403 | The source wallet is frozen; no debits are permitted |
customer_not_found | 404 | customer_id unknown / not yours |
idempotency_key_conflict | 409 | Same Idempotency-Key used with a different body |
idempotency_conflict | 409 | Same Idempotency-Key as a prior payout that differs in amount or recipient account: narrower than idempotency_key_conflict, which compares the whole request body |
merchant_reference_duplicate | 409 | merchant_reference was already used on a recent payout; each reference is single-use within the cool-down window |
insufficient_funds | 402 | Wallet balance is below the payout amount plus fees |
routing_failed | 422 | No disbursement route is available for this payout, or the destination bank could not be identified |
provider_error | 502 | Downstream rail unreachable; safe to retry with the same key |
curl https://api.swappr.me/api/v1/payouts \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount_minor": "500000",
"currency": "NGN",
"recipient": {
"account_number": "0690000032",
"bank_code": "044"
},
"merchant_reference": "ORDER_001",
"narration": "Payroll April 2026",
"customer_id": "cust_cmoji8..."
}'curl https://api.swappr.me/api/v1/payouts \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount_minor": "500000",
"currency": "NGN",
"beneficiary_id": "ben_cmo8x2p9q0...",
"merchant_reference": "ORDER_002"
}'Response
201 Created with the payout object. Status is typically paid in sandbox, or queued / processing in live (transitions to paid when the provider confirms, listen for the payout.paid webhook).
Webhook events
NGN payouts emit payout.processing, then payout.paid or payout.failed (and payout.reversed if a paid payout is later reversed). If you set customer_id / customer_reference, they’re included in the event payload. See Webhooks.
Fund from another currency (cross-currency)
You can fund an NGN payout from a different wallet currency (e.g. pay ₦ from your CAD wallet) in a single call by adding funding_currency to the request. See Cross-currency payouts for the full method, both amount options (destination / source), the fx response block, fee_inclusive, rate-freshness behaviour, and the error table.
Manage payouts
These endpoints are currency-agnostic, they work the same for NGN and every FX currency.
GETList payouts
Cursor-paginated. Filter by status, currency, created_after, created_before, limit (1–100, default 50), starting_after.
{
"object": "list",
"has_more": true,
"data": [{ "object": "payout", "id": "...", "...": "..." }]
}GETRetrieve a payout
Accepts the cuid (ckxxx) OR the po_xxx reference.
curl https://api.swappr.me/api/v1/payouts/po_da06226542dc44a9 \
-H "Authorization: Bearer sk_test_..."POSTRe-query payout status
Forces a status refresh by calling the provider directly. Use it when a payout has been stuck in processing unusually long. If the provider reports a failure, the wallet is refunded (principal + fee + tax) and the payout moves to reversed.
You can also re-query a held payout (queued after an unclear send) once it has been waiting at least 30 minutes. It can only move to paid, and then you receive payout_paid. Any other answer leaves it queued, the response shows "status": "queued", and nothing is refunded. Other queued payouts, and a held payout less than 30 minutes old, return 400 invalid_status.
POSTCancel a payout
Cancel a payout in draft (no wallet movement) or queued (the wallet is refunded: principal + fee + tax) status. The payout ends cancelled. Body: { "reason": "..." } (required, 3 to 500 chars). Cannot cancel processing or terminal-status payouts, or a queued payout that has already started sending; those return 400 invalid_status.