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

StatusDescription
draftCreated via dual-control flow; awaiting team approval
queuedApproved/auto-approved; awaiting dispatch. Also used while we confirm a payout whose send to the provider had an unclear result (see below)
processingDispatched to provider; awaiting confirmation
paidFunds reached the recipient
paid_manualManually marked paid by Technest support after evidence review
failedThe 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_manualManually marked failed by Technest support
reversedYour wallet was refunded (principal + fee + tax) because the provider failed or returned the payout, or the receiving bank sent a paid payout back
cancelledCancelled before dispatch
awaiting_admin_reviewHeld 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

POST/v1/payouts

Send NGN to a Nigerian bank account. The Idempotency-Key header is required.

Body parameters
amount_minorstring (BigInt)Required

Minor units (kobo).

currencystringRequired

NGN

recipientobjectConditional

Inline recipient (below). Required unless beneficiary_id is supplied.

beneficiary_idstringConditional

A saved NGN beneficiary. Alternative to inline recipient.

wallet_idstring

Auto-resolved from currency + env if omitted.

merchant_referencestring

Your own ref. Unique per merchant in a 30-day window.

narrationstring

Shown on the recipient’s bank statement.

customer_idstring

Sender/customer attribution, see Customer attribution.

customer_referencestring

Opaque attribution string (max 64 chars).

allow_duplicateboolean

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"
}
FieldRequiredNotes
account_numberyes10-digit NUBAN.
bank_codeyesAny 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_namenoOptional hint. If omitted, Swappr resolves the canonical name and stores it for dashboards + receipts.
namenoOptional. 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

CodeHTTPCause
missing_field400Required body field absent
invalid_field400Wrong type or format of a body field (e.g. a non-numeric bank_code)
unknown_bank_code422bank_code not found in the bank list
wallet_not_found422wallet_id doesn’t match merchant + currency + env
recipient_unresolvable422The account name could not be resolved from the account number + bank code
beneficiary_cooldown429Same recipient paid recently, pass allow_duplicate: true if intentional
limit_violation422Per-tx / daily / per-beneficiary cap exceeded; see error.detail.kind
fraud_rule_blocked422Internal fraud rule blocked the payout; see error.detail.hits
beneficiary_blacklisted403Recipient is on your merchant blacklist, un-blacklist them on the Beneficiaries page, or send to a different recipient
beneficiary_globally_blocked403Recipient is on the platform-wide blacklist and cannot be overridden, contact support
wallet_frozen403The source wallet is frozen; no debits are permitted
customer_not_found404customer_id unknown / not yours
idempotency_key_conflict409Same Idempotency-Key used with a different body
idempotency_conflict409Same 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_duplicate409merchant_reference was already used on a recent payout; each reference is single-use within the cool-down window
insufficient_funds402Wallet balance is below the payout amount plus fees
routing_failed422No disbursement route is available for this payout, or the destination bank could not be identified
provider_error502Downstream rail unreachable; safe to retry with the same key
Request
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..."
  }'
Request, by beneficiary
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

GET/v1/payouts

Cursor-paginated. Filter by status, currency, created_after, created_before, limit (1–100, default 50), starting_after.

Response
{
  "object": "list",
  "has_more": true,
  "data": [{ "object": "payout", "id": "...", "...": "..." }]
}
200 OK

GETRetrieve a payout

GET/v1/payouts/{id}

Accepts the cuid (ckxxx) OR the po_xxx reference.

Request
curl https://api.swappr.me/api/v1/payouts/po_da06226542dc44a9 \
  -H "Authorization: Bearer sk_test_..."

POSTRe-query payout status

POST/v1/payouts/{id}/requery

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

POST/v1/payouts/{id}/cancel

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.