Payouts
The Payouts API allows you to convert crypto assets (BTC, USDT, USDC) into local fiat currencies (e.g., NGN, KES). The flow involves creating a quote, initializing the payout with beneficiary details, depositing funds, and finalizing the transaction.
Create quote → Initialize (with beneficiary) → Finalize. Use account-lookup, banks/:countryCode, and supported-countries/:country upstream to build a valid Initialize request.
Create Payouts Quote
Creates a quote for converting cryptocurrency to fiat. You can specify either amount (crypto amount) or settlement_amount (target fiat amount).
Create Payouts Quote Request
Crypto amount to convert, expressed as a decimal string in the source asset (e.g. '10' = 10 USDC). Takes priority over settlement_amount when both are provided.
Target fiat amount you want the beneficiary to receive. Ignored when amount is also provided.
Two-letter ISO 3166-1 alpha-2 country code for the destination (e.g. 'NG', 'KE', 'GH'). Must be one of the codes returned by GET /api/payouts/supported-countries.
Crypto asset you're debiting from your wallet: 'BTC', 'USDT', or 'USDC'.
Target fiat currency code the beneficiary will receive (e.g. 'NGN', 'KES', 'GHS'). Must be a currency supported on the chosen country.
Source of funds (lowercase). 'offchain' debits your Bitnob wallet balance directly; 'onchain' expects a blockchain deposit to cover the quote.
Blockchain network used when source is 'onchain' (e.g. 'BITCOIN', 'trc20', 'erc20'). Omit when source is 'offchain'.
Client-generated unique identifier for this quote. Reused on initialize and finalize for idempotency and reconciliation.
Provide either amount (crypto side) or settlement_amount (fiat side) — not both. When both are sent, amount takes priority and settlement_amount is ignored.
Create Payouts Quote Response
A globally unique identifier for the payout. Used to reference this payout in future requests.
An internal reference string for the quote, passed into subsequent payout endpoints.
The unique identifier of the company that created the quote.
The current state of the quote (e.g., 'QUOTE'). Changes as the payout progresses through its lifecycle.
The source of funds used for the payout (e.g., 'OFFCHAIN' for wallet balance, 'ONCHAIN' for blockchain deposit).
The crypto asset being converted (e.g., 'USDT', 'BTC', 'USDC').
The blockchain network for the transaction (e.g., 'CHAIN_TYPE_ETHEREUM', 'CHAIN_TYPE_BITCOIN').
The target fiat currency code (e.g., 'GHS', 'NGN').
The original crypto amount provided in the quote request.
The exact amount of fiat the recipient will receive after conversion.
The equivalent amount in satoshis for the requested crypto amount.
The equivalent BTC amount for the requested crypto amount.
The fee charged for this payout in the from_asset currency.
Contains the conversion rate details: 'rate' (crypto to fiat), 'btc_rate' (BTC to fiat), and 'currency'.
Your unique reference string for this quote.
Two-letter ISO country code for the destination.
Timestamp indicating when this quote expires. After this time, the quote must be re-generated.
Timestamp when the quote was created.
Initialize Payout
Locks the quote, attaches a beneficiary, and moves the payout to INITIATED. The beneficiary object varies by destination_type and country — call GET /api/payouts/supported-countries/:country (and, for bank rails, GET /api/payouts/banks/:countryCode + Account Lookup) to build a valid shape.
Initialize Payout - Path Parameters
The ID of the quote returned by Create Payouts Quote (e.g. 'QT_100003'). Must not be expired.
Initialize Payout - Request Body
The quote you're initializing. Must match the :quoteId path parameter.
Client-generated unique identifier for this payout. Reused on Finalize for idempotency and reconciliation.
Short reason code for the payment (e.g. 'salary', 'vendor_payment', 'family_support'). Some corridors require a value from a fixed list — check Get Country Details.
HTTPS URL that will receive payout lifecycle webhooks (completed, failed).
Recipient details. Shape depends on destination_type (bank, mobile_money, swift). For SWIFT, populate the bank fields, a nested beneficiary address, and a nested sender identity.
Rail selector: 'bank', 'mobile_money', or 'swift'.
Two-letter ISO country code of the destination account.
Name on the destination account. Use the value returned by Account Lookup when available.
Bank account number (or IBAN for SWIFT corridors).
Provider code from Get Banks. Required for 'bank' and 'mobile_money' destination types.
BIC/SWIFT code of the destination bank. Required when destination_type is 'swift'.
Display name of the destination bank. Required for SWIFT.
Physical address of the destination bank branch. All required for SWIFT.
Regulator-visible reason code (e.g. 'salary_payment', 'family_support'). Required for SWIFT.
Physical address of the recipient (country, city, post_code, address). Required for SWIFT.
Identity of the sender. type is 'business' or 'individual'. Business senders provide registration_number; individual senders provide date_of_birth and country_of_birth.
Initialize Payout - Response
Top-level flag indicating whether the request was processed successfully.
Human-readable status message.
The initialized payout record.
Unique identifier for this payout. Pass it as :id to GET /api/payouts/:id or as the path param on Finalize is keyed off quote_id (see Finalize).
The quote that was locked (echoed from the request).
Lifecycle state. 'INITIATED' after a successful initialize; transitions through processing states to 'COMPLETED' or 'FAILED' after Finalize.
Source of funds (from the original quote). 'SOURCE_UNSET' when source was not explicitly set.
Crypto asset being debited (e.g. 'USDT', 'USDC', 'BTC').
Destination fiat currency (e.g. 'GHS', 'NGN').
Crypto amount being sent, as a decimal string in from_asset units.
Fiat amount the beneficiary will receive after conversion.
Equivalent amount expressed in BTC for internal pricing.
Equivalent amount expressed in satoshis.
Total fees applied to the payout, as a decimal string in the source asset.
Total fees expressed in cents / smallest fiat unit.
Locked FX details: rate (from_asset → to_currency), btc_rate (for BTC pricing), currency (display).
The full beneficiary object echoed back from the request for auditability.
Reason code provided on the request.
Reference provided on the request.
RFC 3339 / ISO 8601 timestamp. Call Finalize before this time or re-request a quote.
Set once the payout is fully persisted downstream. May be null immediately after initialize.
Request identifier for log correlation and support.
Top-level RFC 3339 / ISO 8601 server timestamp of when the response was generated (UTC).
Initialize returns data.payout.expires_at. You must call POST /api/payouts/:quoteId/finalize before that timestamp — if it passes, re-run the quote and initialize flow to lock a fresh rate.
Account Lookup
Name-resolves a bank or mobile-money account before initiating a payout. Call this after you've chosen a country and bank_code (see Get Banks) to confirm the account number is valid and pre-fill the account_name on your Initialize request.
Query Parameters
Two-letter ISO country code of the destination account (e.g. 'NG', 'KE', 'GH').
Bank or mobile-money provider code from GET /api/payouts/banks/:countryCode.
Account or mobile number to resolve.
Account Lookup Response
Name registered on the resolved account. Use this value as account_name when constructing the beneficiary on Initialize.
Echo of the account number that was resolved.
Normalized bank code from the underlying rail (may differ from the input bank_code).
Display name of the resolving bank or mobile-money provider.
Two-letter ISO country code of the resolved account.
True when the rail successfully resolved a name for the account; false when the account couldn't be verified.
Finalize Payout
Finalizes a payout after the deposit has been confirmed. No request body is required — simply call this endpoint with the quoteId path parameter.
Finalize Payout - Path Parameters
The ID of the quote to finalize.
Finalize Payout Response
The response echoes the same data.payout shape as Initialize, with a few settlement-specific additions once the rail has accepted the transfer.
Transitions from 'INITIATED' to 'PENDING' on finalize, then on to 'COMPLETED' or 'FAILED' once the rail settles. Watch the webhook for the terminal state.
Populated from the quote (e.g. 'OFFCHAIN', 'ONCHAIN'). 'SOURCE_UNSET' is replaced with the real source at finalize.
The company the payout is debited from (populated at finalize).
Settlement amount in cents / smallest fiat unit.
Echoed from the initialize request — URL that will receive lifecycle webhooks.
Identifier returned by the downstream rail (e.g. SWIFT provider / bank). Use it for reconciliation with the provider.
Destination country (populated at finalize, e.g. 'GH').
Timestamps of each milestone in the payout's journey: quote_at, initialized_at, and (on completion) finalized_at.
When the quote was created.
When the beneficiary was attached and the payout was initialized.
Now populated (not null) — the payout has been persisted through the finalize pipeline.
All other fields (id, quote_id, from_asset, to_currency, amount, settlement_amount, btc_amount, sat_amount, fees, cent_fees, nested exchange_rate, echoed beneficiary, reference, payment_reason, expires_at) use the same shape as the Initialize response — refer to that section for per-field explanations.
List Payouts
Retrieve historical payouts for your company — quotes, initialized, pending, completed, and failed records all appear here. Supports offset/limit pagination and returns a total_count so you can drive page numbers on your UI.
Query Parameters
Maximum number of payouts to return in a single page. Default 20.
Number of records to skip from the start of the result set. Use with total_count to paginate (e.g. offset=20 for page 2 with limit=20).
Response Fields
Page of payout records. Each entry uses the same shape as the Initialize / Finalize response — refer to those sections for per-field explanations.
Total number of payouts for your company across all pages.
Echo of the limit applied to this page.
Echo of the offset applied to this page.
True when offset + limit < total_count — i.e. more records are available beyond this page.
Lifecycle state of the payout: 'QUOTE' (quote created, not initialized), 'INITIATED', 'PENDING' (awaiting settlement), 'COMPLETED', or 'FAILED'.
Milestone timestamps. 'QUOTE'-status rows only carry quote_at; initialized_at and finalized_at populate as the payout progresses.
Request identifier for log correlation and support.
Top-level RFC 3339 / ISO 8601 server timestamp of when the response was generated (UTC).
Payouts you created but never initialized still show up here with status: "QUOTE". They expire at expires_at and are never rerun — treat them as abandoned quotes for reconciliation purposes.
Get Payout by ID
Retrieve a single payout by its internal id (the UUID returned on Initialize / Finalize, not the quote_id). Returns the full payout record including its current lifecycle status, settlement details, and the beneficiary object that was attached at initialize.
Path Parameters
The payout's internal identifier (from data.payout.id on Initialize / Finalize, or data.items[].id on List Payouts). Not the quote_id.
Response
The response shape is identical to Finalize — the wrapped data.payout object with the same settlement, beneficiary, and trip fields. The trip object grows as the payout progresses:
quote_at — set when the quote was created.
initialized_at — set once Initialize succeeds.
processing_start — set when the rail picks up the payout (appears once status: "PROCESSING").
completion_time — set on the terminal COMPLETED or FAILED state.
For per-field explanations, see the Finalize Payout Response section — data.payout.* uses the same shape.
Get Supported Countries
Returns every country you can send payouts to, along with each country's supported currencies and payout rails (destination_types). Use this to populate country pickers in your UI or to discover which corridors are open for a given destination.
Each corridor lists destination_types — values include bank, swift, mobile_money, sepa_eur, domestic_gbp, wire, ach, alipay, wechatpay, paybill, paytill. Combine the destination type with GET /api/payouts/supported-countries/:country to get the exact field schema.
Response Fields
Full list of countries payouts are supported in.
ISO 3166-1 alpha-2 country code (e.g. 'GB', 'NG').
Display name of the country.
Country flag emoji for UI rendering.
International dial prefix. Occasionally a comma-separated list when the country has multiple (e.g. DO → '1809,1829,1849').
Supported payout corridors for this country. Each entry pairs a currency with the destination types available for that currency.
ISO 4217 currency code (e.g. 'USD', 'EUR', 'GHS').
Payout rails available for this (country, currency) pair. See the list above for possible values.
The country list you get in sandbox is a subset of the production list. Always call this endpoint against the environment you're integrating with before hard-coding corridor support.
Get Country Details
Returns the full beneficiary field schema for a single country — a machine-readable destination_types object keyed by rail (bank, swift, mobile_money, alipay, wechatpay, etc.). Each entry lists required fields, their type / component / pattern / min_length / max_length validators, plus any enum options and per-rail limits.
This is the source of truth you should consult before constructing an Initialize payload. Use it to drive dynamic forms, client-side validation, or per-country payload builders.
Path Parameters
ISO 3166-1 alpha-2 country code — must appear in Get Supported Countries (e.g. 'CN', 'NG', 'GB').
Response Fields
Country identity block — same shape as entries in Get Supported Countries.
Map keyed by rail name (e.g. 'bank', 'swift', 'alipay', 'wechatpay', 'mobile_money'). Each value is the full field schema for that rail.
Human-readable label for the rail (e.g. 'Bank Transfer', 'International Wire (SWIFT)').
Ordered list of fields required on the beneficiary for this rail. Each field carries key/label/type/required/component plus optional pattern/min_length/max_length/placeholder/options. Fields of type 'group' contain nested fields; type 'variant' is a discriminated union keyed by variant_key.
Populated for rails that require a bank selector (e.g. 'bank'). Same shape as Get Banks.
Per-rail amount limits: min_amount, max_amount, currency.
The response is designed to drive auto-generated forms. Each field's component (text, select, date, country_select, fieldset, variant_fieldset) maps one-to-one to a form control; options_ref points at shared enum lists so you don't re-hardcode them per country.
Get Banks
Returns the bank and mobile-money directory for a country — a flat list of providers with their code and display name. Use the returned code as the bank_code input for Account Lookup and as the beneficiary's bank_code on Initialize.
Path Parameters
Two-letter ISO country code (e.g. 'NG', 'KE', 'GH'). Must be one of the codes returned by Get Supported Countries.
Response Fields
Echo of the ISO country code you requested.
Display name of the country (e.g. 'Nigeria').
Total number of bank / provider entries returned (e.g. Nigeria returns 358).
Banks and mobile-money providers supported in the country. Typically sorted alphabetically by bank_name.
Provider code — pass this as bank_code on Account Lookup and as the beneficiary's bank_code on Initialize.
Display name of the bank or mobile-money provider (e.g. 'Access bank', 'PAYSTACK PAYMENTS LIMITED').
Get Limits
Returns the minimum and maximum payout amount for every supported (country, currency) corridor, with pre-computed USD equivalents so you can validate amounts client-side before creating a quote.
Response Fields
One entry per (country, currency) corridor. Countries that support multiple currencies (e.g. GB supports both EUR and GBP) return one limit per currency.
ISO 3166-1 alpha-2 country code (e.g. 'NG', 'GH', 'GB').
ISO 4217 currency code for this corridor (e.g. 'NGN', 'GHS', 'GBP').
Minimum amount for a single payout, expressed as a decimal string in the local currency.
Maximum amount for a single payout, expressed as a decimal string in the local currency.
min_amount converted to USD using the current exchange rate. Useful for validating USD-denominated amounts client-side without calling a conversion endpoint.
max_amount converted to USD using the current exchange rate.
The USD equivalents are computed at request time against the live FX rate. Cache the response sparingly — stale USD bounds may drift from what the quote endpoint will accept.
SWIFT Payouts
Cross-border payouts use the SWIFT destination type when the destination country/currency has no local corridor (ACH, SEPA, domestic GBP, mobile money, etc.). This is the fallback wire rail for USD, EUR, HKD, and similar currencies into most countries.
SWIFT flows use the standard three-step sequence — beneficiary details go on Initialize, not on the quote.
SWIFT is not a separate endpoint — the quote, initialize, and finalize responses all use the same data.payout envelope you see for bank, mobile_money, ACH, and SEPA corridors. The only architectural difference is destination_type: "swift" (plus the SWIFT-specific fields like swift_code, remittance_purpose, and the discriminated sender variant) on the Initialize beneficiary. Refer to Create Payouts Quote, Initialize Payout, and Finalize Payout above for full per-field explanations — the SWIFT steps below focus on what's SWIFT-specific.
Step 1 — Create Quote
Describes currency, amount, and destination country. No beneficiary info on the quote. The quote is the same shape used for every payout corridor — see Create Payouts Quote above for the full field reference.
The quote body has no beneficiary_info and no destination_type. Both belong on Initialize.
Step 2 — Initialize (the SWIFT payload)
The SWIFT beneficiary lives inside the top-level beneficiary object on POST /api/payouts/:quoteId/initialize. All API fields are lowercase snake_case; the adapter normalises them to the downstream provider's format.
Top-Level Fields
SWIFT payload — see the three tables below. Validated against the corridor's field schema for (country, destination_type=swift).
Client-set idempotency / tracking reference. Overrides or supplements the quote's reference.
Short reason code for the payment (free text at this level).
Per-payout webhook URL override.
Optional link to a persisted customer record.
Arbitrary client metadata string.
beneficiary — Core SWIFT Fields
Set to 'swift' (lowercase). Selects the SWIFT wire rail.
ISO 3166-1 alpha-2 of the destination account. Must match the country on the quote.
Recipient name. Must have ≥2 space-separated parts, each ≥2 chars. No honorifics (Mr/Mrs/Dr). No single-letter-plus-dot (e.g. 'J. Doe' is rejected; 'John Doe' passes).
For IBAN countries (GB, DE, AT, AD, BE, BG, CH, FR, …): IBAN format, 15–34 chars, ^[A-Z0-9]+$. For non-IBAN countries (AR, BW, CN, …): plain account number, 5–34 chars.
8 or 11 chars, ^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$. Uppercase only. HK uses a stricter pattern that pins chars 5–6 to literal 'HK'.
Display name of the destination bank.
Street address of the destination bank branch.
City of the destination bank branch.
Postal / ZIP code of the destination bank branch.
ISO 3166-1 alpha-2 of the destination bank's country.
Lowercase snake_case value from the remittance purposes enum (see reference below).
Recipient's physical address (nested object — see table below).
Sender identity, discriminated by `type` ('business' or 'individual'). See table below.
beneficiary.beneficiary — Recipient Address
ISO 3166-1 alpha-2 of the recipient.
City.
Postal / ZIP code.
Street address.
Rwanda (RW) and Singapore (SG) additionally require type on the nested beneficiary — one of "business" or "individual". No other SWIFT-supporting country requires this.
beneficiary.sender — Payer Identity
'business' or 'individual' (case-insensitive, normalised server-side).
Legal name of the sender (same name-validation rules as recipient account_name).
Sender's ISO 3166-1 alpha-2 country code.
Sender's city.
Sender's street address.
Sender's postal code.
Business registration / tax ID, 2–30 chars. REQUIRED when type='business'; ignored for individuals.
YYYY-MM-DD. REQUIRED when type='individual'.
ISO 3166-1 alpha-2. REQUIRED when type='individual'.
Country-Specific Extras
A handful of countries add required fields beyond the core SWIFT schema. The corridor config at GET /api/payouts/supported-countries/:country is the source of truth.
Australia: 6-digit BSB code, pattern ^\d{6}$. The corridor placeholder shows '062-000' but dashes are NOT accepted — strip them ('062000').
India: 11-char IFSC code, pattern ^[A-Z]{4}0[A-Z0-9]{6}$. Note the key is `IFSCode` (camelCase) — unlike every other field in the payload. Sending `ifs_code` or `ifsc_code` silently fails validation.
Hong Kong uses a stricter SWIFT regex: ^[A-Za-z]{4}HK[A-Za-z0-9]{2}([A-Za-z0-9]{3})?$ — chars 5–6 are pinned to literal 'HK'. Case-insensitive on HK only.
Rwanda and Singapore require `type` ('business' or 'individual') on the nested beneficiary object.
Step 3 — Finalize
No body. Commit the debit and release the payment to the SWIFT rail.
Any body you send on Finalize is ignored. The server only reads the :quoteId from the path.
Discovering SWIFT-Enabled Countries
Before building a SWIFT payload, fetch the corridor config for the destination country and look for destination_types containing "swift". The returned schema for that destination type is the source of truth for that country's required fields.
Validation Failures
http | code | cause |
|---|---|---|
400 | INVALID_BODY | Request body isn't valid JSON or doesn't match the handler shape. |
400 | INVALID_BENEFICIARY_DATA | `country` missing from beneficiary; or a required SWIFT field is missing / fails regex / exceeds length limits. The error message lists each failing field. |
410 | QUOTE_EXPIRED | Quote TTL elapsed before Initialize or Finalize was called. Run the quote + initialize flow again. |
422 | UNSUPPORTED_CORRIDOR | (country, destination_type) pair has no schema — e.g. requesting `swift` for a country that doesn't support it. |
422 | AMOUNT_OUTSIDE_LIMITS | Quote amount is below or above the corridor's min/max limits. |
422 | INSUFFICIENT_BALANCE | Company balance can't cover the payout when source is `offchain`. |
Common Pitfalls
Beneficiary on the quote. It doesn't go there. Quote has no beneficiary_info, no destination_type. Both live on Initialize.
Casing on enums. API is lowercase snake_case for destination_type, remittance_purpose, sender.type. The adapter uppercases them before dispatch — send lowercase.
Casing on SWIFT/BIC. Must be uppercase (DEUTDEFF, not deutdeff). Length is exactly 8 or 11.
Three address blocks. bank_* = destination bank; nested beneficiary.* = recipient's physical address; sender.* = payer's address. All three are independent.
Finalize body. There is none — sending a body is ignored.
country appears twice. Once at the top of beneficiary (corridor lookup key), once nested inside beneficiary.beneficiary.country (recipient's country — often the same value).
Sender variant mismatch. type: "individual" with registration_number → registration_number is silently ignored. type: "business" without registration_number → validation fails.
Name format. account_name / sender.account_name must have ≥2 space-separated parts, each ≥2 chars. No Mr/Mrs/Dr. No single-letter-plus-dot. J. Doe rejects; John Doe passes.
IFSCode is the one camelCase field. Every other SWIFT field is snake_case; India's IFSC is literally IFSCode. Sending ifs_code / ifsc_code silently fails as "missing required field".
AU BSB has no dashes. Placeholder shows 062-000; regex ^\d{6}$ rejects the dash. Submit as 062000.