Payouts

Send money to recipients across NGN bank accounts, CAD Interac email IDs, and GBP / USD / EUR bank rails.

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",
  "provider": "<provider_slug>",
  "provider_ref": "<provider_ref>",
  "nip_reference": "100004250505000123456789",
  "merchant_reference": "ORDER_001",
  "narration": "Payroll April 2026",
  "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
processingDispatched to provider; awaiting confirmation
paidFunds reached the recipient
paid_manualManually marked paid by Technest support after evidence review
failedProvider returned failed; if applicable, wallet was auto-reversed
failed_manualManually marked failed by Technest support
reversedPaid payout reversed (refund issued)
cancelledCancelled before dispatch
awaiting_admin_reviewHeld by an internal fraud rule; awaiting Technest review

Maker-checker rules

Single payouts that exceed your account’s per-transaction dual-control threshold land in draft status awaiting team approval (approve via dashboard or via 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 (Admin, Approver, etc.) drop to maker-checker on live and need a different teammate to approve, even when they hold both payout_create and payout_approve permissions. Sandbox keeps dual-perm self-approve for development velocity.

The Owner role is capped at 3 holders per merchant. Plan your team accordingly — for example, designate 1 Owner for routine approvals and 2 backups.

Email-OTP gate (dashboard only)

The dashboard’s New Payout and Approve flows can be configured to require an email-OTP before submit AND/OR approve. The threshold is configured per (merchant, env, currency) via MerchantLimits.otpRequiredAtAmountMinor in the admin console. Default in live env: OTP required at any amount. Sandbox: bypassed unless requirePayoutOtpInSandbox flag is set.

API integrations bypass the OTP gate entirelyPOST /v1/payouts and POST /v1/batches authenticate via Bearer token + IP allowlist + Idempotency-Key, which is the right MFA shape for machine-to-machine. The OTP is a dashboard-only control for human-clicked actions.


Create a payout

POST /v1/payouts

Create a single payout. The recipient block format is currency-specific.

Request

FieldTypeRequiredNotes
amount_minorstring (BigInt)yesMinor units (kobo for NGN, cents for USD, etc.)
currencystringyesNGN | GBP | USD | EUR | CAD
wallet_idstringnoAuto-resolved from currency + env if omitted
merchant_referencestringnoYour own ref. Unique per merchant in 30-day window.
narrationstringnoDescription shown on the recipient’s bank statement (NGN).
recipientobjectyesCurrency-specific. See below.
senderobjectconditionalRequired if your account requires sender info (IMTO compliance). See sender block below.
sender_customer_idstringconditionalAlternative to inlining the sender block. If your account has end-user customer records enabled, pass the customer’s id here and Swappr resolves the sender identity from their stored KYC. Mutually exclusive with sender — pass one or the other, not both.
allow_duplicatebooleannoBypass the beneficiary cool-down for an intentional retry.

Recipient by currency

NGN

{
  "account_number": "0690000032",
  "bank_code": "044"
}

Name is auto-resolved via NUBAN. Bank code is the 3-digit CBN code (see Banks).

CAD-Interac

{
  "email": "recipient@example.com",
  "first_name": "Adaeze",
  "last_name": "Okonkwo"
}

GBP

{
  "account_number": "12345678",
  "sort_code": "200000",
  "name": "John Smith"
}

EUR

{
  "iban": "DE89370400440532013000",
  "bic_code": "COBADEFFXXX",
  "name": "Hans Müller"
}

USD (ACH — domestic US)

{
  "account_number": "1234567890",
  "routing_number": "021000089",
  "name": "Jane Doe",
  "method": "ach"
}

USD (SWIFT/wire — international)

{
  "account_number": "1234567890",
  "swift_code": "CHASUS33XXX",
  "name": "Jane Doe",
  "bank_name": "JPMorgan Chase Bank",
  "method": "wire"
}

method defaults to ach for USD when omitted. Pass wire explicitly + supply swift_code + bank_name for international USD wires.

Sender block (compliance — only when required)

Optional for most merchants. Required when your merchant account is configured to capture sender identity on every transaction (e.g. for cross-border AML compliance under CBN IMTO regulations). Your dashboard shows whether your account requires it. If you’re unsure whether your business model needs this, contact support@the-technest.com.

{
  "first_name": "Jane",
  "last_name": "Carter",
  "id_type": "Passport",
  "id_number": "A9876543",
  "id_expiry": "15082031",
  "date_of_birth": "12041992"
}
FieldNotes
id_typeStrict enum: Passport | Driver's License | National-ID | Residence Card
id_numberFree-text
id_expiryDDMMYYYY
date_of_birthDDMMYYYY

Example

curl https://api.swappr.me/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"
  }'

Response

201 Created with the Payout object. Status will typically be paid (sandbox) or queued/processing (live, transitions to paid when the provider confirms).

Errors

CodeHTTPCause
missing_field400Required body field absent
invalid_field422Wrong type / format (e.g. id_expiry not DDMMYYYY)
wallet_not_found422wallet_id doesn’t match merchant + currency + env
unsupported_currency422Currency not active for this merchant
recipient_unresolvable422NUBAN cascade exhausted; account invalid at every provider
name_mismatch422(FX rails only) Merchant-supplied name differs from bank-of-record
beneficiary_cooldown422Same recipient paid in the last 5 minutes — 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
merchant_blocked422Recipient on your merchant blacklist
globally_blocked422Recipient on platform-wide blacklist (cannot override)
sender_info_required422Account requires sender info; see sender block
idempotency_conflict409Same Idempotency-Key was used with a different body
provider_error502Downstream rail unreachable; safe to retry with same idempotency key

List payouts

GET /v1/payouts

Cursor-paginated list. Filter by status, currency, date range.

Query params

ParamNotes
limit1-100, default 50
starting_afterCursor (last payout id from previous page)
statusFilter to one status
currencyFilter to one currency
created_afterISO 8601
created_beforeISO 8601

Response

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

Retrieve a payout

GET /v1/payouts/{id}

Accepts the cuid (ckxxx) OR the po_xxx reference.

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

Returns the full Payout object including timeline timestamps + provider info + beneficiary link if saved + batch context if part of a bulk batch.


Re-query payout status

POST /v1/payouts/{id}/requery

Forces a status refresh by calling the provider directly. Use this when a payout has been stuck in processing for an unusually long time.

If the provider returns failed, the wallet is auto-reversed (principal + fee + tax credited back) — the response shows the updated payout with status: "failed" and metadata flagging the auto-reverse with reason tag MRQS (merchant re-query status).

curl https://api.swappr.me/v1/payouts/po_xxx/requery \
  -X POST \
  -H "Authorization: Bearer sk_test_..."

Errors

CodeMeaning
payout_not_foundWrong id, wrong env, or not yours
invalid_statusPayout already in a terminal state — nothing to re-query
no_providerPayout was never dispatched (still draft)
requery_unsupportedAdapter for this provider doesn’t expose re-query (contact support)
provider_errorProvider returned an error; transient, retry safe

Cancel a payout

POST /v1/payouts/{id}/cancel

Cancel a payout in draft (no wallet movement) or queued (wallet auto-reversed) status. Cannot cancel processing or terminal-status payouts.

Request

{
  "reason": "Customer requested cancellation"
}

reason is required, min 3 chars, max 500.

Response

200 OK with the updated Payout object (status will be cancelled).

Errors

CodeMeaning
payout_not_foundWrong id or not yours
invalid_statusCannot cancel — payout is processing/paid/failed/etc
missing_fieldreason not provided