Payouts API — Integration Guide
This guide walks you through everything needed to send a payout — from authenticating your first request to confirming that funds have reached your beneficiary.
A payout delivers local fiat (e.g. NGN, KES, GHS) to a beneficiary's bank or mobile-money account, funded from crypto. You create payouts through a simple, quote-based flow:
Authenticate → Create quote → Add beneficiary → Finalize → Fund → Track to completion
1. Account setup
1.1 Create your account
Visit https://app.bitnob.com/ and create an account with the company name of your choice. Once you're set up, let us know on Slack immediately so the compliance team can enable your account for payouts.
1.2 Get your API credentials
From the home page, open the Settings page and click API Keys. There you'll obtain:
what | notes |
|---|---|
Base URL (live) | https://api.bitnob.com |
Client ID | Public identifier for your API key. |
Client Secret | Shown once when you generate your API key — store it securely. |
1.3 Whitelist your IP
For extra security, whitelist your server's IP address before going live. In the dashboard, go to Settings → IP (bottom-left of the vertical navigation), or use this shortcut: http://app.bitnob.com/settings?section=ip&view=endpoints
Requests from non-whitelisted IPs will be rejected once whitelisting is enabled.
Always test end-to-end in sandbox first — it lets you complete a full payout (including funding) without moving real money. Then switch the base URL to https://api.bitnob.com and your live API keys for production.
Throughout this guide, {BASE_URL} stands for your environment's base URL — https://api.bitnob.com for live, or your sandbox URL for testing.
2. Authentication
All API requests are made against the Bitnob base URL https://api.bitnob.com and authenticated with an HMAC-SHA256 signature. You sign each request with your Client Secret and send four headers. Both your CLIENT_ID and CLIENT_SECRET are required. Signatures are time-limited, so requests can't be replayed.
A ready-made signing function is available on the Authentication page.
2.1 Required headers
header | value |
|---|---|
X-Auth-Client | Your Client ID |
X-Auth-Timestamp | Current Unix time, in seconds |
X-Auth-Nonce | A unique random value per request (a UUID works well) |
X-Auth-Signature | HMAC-SHA256 signature (hex-encoded) — see below |
2.2 How to build the signature
- Build the payload string by joining four parts with colons:
REQUEST_BODY is the exact raw JSON body you send. For requests with no body (e.g. GET, or the finalize call), use an empty string.
- Sign it with your Client Secret using HMAC-SHA256 and hex-encode the result:
Sign the body byte-for-byte as it is sent. If you re-serialize or reformat the JSON after signing, the signature will not match.
2.3 Signing example (bash)
2.4 Signing example (Node.js)
2.5 Common auth errors
problem | fix |
|---|---|
Invalid signature | Confirm the Client Secret and that the signed body exactly matches the sent body. |
Timestamp outside acceptable range | Sync your clock; send the timestamp in seconds, not milliseconds. |
401 Unauthorized | A header is missing or the signature is wrong. |
403 Forbidden | Authenticated, but your key lacks permission for this action. |
3. Understanding funding: offchain vs onchain
When you create a quote you choose a source, which decides where the funds come from:
source | meaning |
|---|---|
offchain | Your Bitnob account balance is charged directly. No external deposit — the balance must cover the payout. |
onchain | You send a crypto deposit to a generated address to fund the payout. The deposit can be BTC (on-chain), Lightning, or stablecoins. |
Everything else in the flow is identical; only the funding step differs.
4. Reference lookups (recommended first)
Use these read-only calls to build a valid request. They help you pick a supported country, the exact beneficiary fields it needs, valid bank codes, and transaction limits.
Verify a beneficiary account before paying it (returns the resolved account name):
For mobile-money destinations, pass type=mobile_money and use the mobile-money provider as the bank_code (e.g. MTN). The account_number is the recipient's phone number in international format.
Example response:
Check is_verified is true and confirm account_name is who you expect before continuing. (For mobile money, bank_code / bank_name may come back empty.)
5. The payout flow
Step 1 — Create a quote
Locks in the exchange rate and fees for your payout.
Request:
field | required | notes |
|---|---|---|
from_asset | ✅ | The crypto asset funding the payout, e.g. BTC, USDT. |
to_currency | ✅ | Fiat the beneficiary receives, e.g. KES, GHS, NGN. |
source | ✅ | offchain (charge balance) or onchain (crypto deposit). |
country | ✅ | Destination country code, e.g. KE. |
chain | for onchain | How you'll fund the deposit: lightning, bitcoin, or a stablecoin chain. |
settlement_amount | one of | Amount the beneficiary should receive, in to_currency. |
amount | one of | Alternatively, the amount in from_asset. Provide one side. |
payment_reason | optional | Shown for your records. |
reference | optional | Your own unique reference — use it to make retries safe and to look the payout up later. |
Response — the created quote is returned under data.payout. Note the status is QUOTE and it carries a short expires_at, so continue promptly.
Key response fields:
field | meaning |
|---|---|
quote_id | Use this in the initialize and finalize calls. |
status | QUOTE at this stage. |
source / chain | Echoed back, e.g. ONCHAIN / CHAIN_TYPE_LIGHTNING. |
sat_amount / btc_amount / amount | The crypto you'll deposit (satoshis / BTC). |
cent_amount / settlement_amount | What the beneficiary receives (minor units / currency units). |
fees / cent_fees | Payout fees. |
exchange_rate | The locked rate details for this quote. |
expires_at | Quote expiry — finalize before this time. |
Another example — Ghana (GHS)
Request:
Response:
Step 2 — Add the beneficiary (initialize)
Attach the recipient's details to the quote. The exact beneficiary fields depend on the country (see the reference lookup in Section 4).
Request:
callback_url is where we'll notify your system of status changes. Recommended.
customer_id (optional) links the payout to a customer in your account.
client_meta_data (optional) is a JSON string echoed back to you for your own reconciliation.
Response — the payout moves to status PENDING_ADDRESS_DEPOSIT and, for onchain funding, returns the address to pay. For chain: lightning this is a BOLT11 Lightning invoice (lnbc…); pay it to fund the payout.
The address field is your funding instruction — pay it in Step 4 before expires_at:
chain | what address contains |
|---|---|
lightning | A BOLT11 invoice (lnbc…) — pay it from any Lightning wallet. |
bitcoin | A BTC on-chain address — send the exact btc_amount. |
stablecoin | The deposit address on the relevant chain. |
Another example — Ghana mobile money (GHS)
For mobile money, the beneficiary uses the recipient's phone number as account_number and has no bank_code.
Request:
Response:
Step 3 — Finalize (approve and submit)
Approves the payout and submits it for processing. No request body — remember to sign with an empty body string.
Confirms the payout and submits it for settlement.
For onchain payouts, the funding address was already returned by the initialize step (Step 2). You can pay it as soon as you have it — see Step 4.
For offchain payouts, your account balance is charged at this step.
The finalize response returns the payout object (same shape as Step 2) with the updated status. Field names/values match the initialize response above.
Step 4 — Fund the payout
onchain — pay the address returned by the initialize step (Step 2) before its expires_at. For chain: lightning this is a BOLT11 invoice (lnbc…) you pay from any Lightning wallet; for bitcoin send the exact btc_amount on-chain; for stablecoins send to the provided deposit address. Once the payment is received, settlement to the beneficiary proceeds.
offchain — nothing to do. Your balance was charged at finalize, and the payout proceeds automatically to settlement.
You can't broadcast a real deposit in sandbox, so simulate it with the request below.
Response:
Step 5 — Track to completion
We settle the fiat to the beneficiary and update the payout status. Track it by polling, or (preferred) by receiving webhooks at your callback_url.
Example response (single payout):
The list endpoint returns the same objects inside a data.payouts array along with pagination fields (limit, offset, total).
6. Payout statuses
A payout moves through these states (status values are upper-case):
status | meaning |
|---|---|
QUOTE | Quote created; not yet initialized. |
PENDING_ADDRESS_DEPOSIT | Initialized; awaiting your crypto deposit (pay the address). |
PROCESSING | Deposit received; settlement to the beneficiary is in progress. |
COMPLETED | Funds delivered to the beneficiary. ✅ |
FAILED | The payout could not be completed. |
Treat COMPLETED as the only success state. Always confirm final status via a status lookup or webhook — don't assume success from the finalize response alone.
7. Full worked example (onchain)
8. Webhooks
If you set callback_url when initializing, we send a notification to that URL whenever the payout's status changes.
Example webhook payload (sent to your callback_url):
The event reflects the new state, e.g. payout.processing, payout.completed, or payout.failed (a failed event includes a reason). Field names above are illustrative — always key your logic off status and your own reference.
Best practices:
Respond quickly with a 2xx. Do heavy processing asynchronously.
Be idempotent — the same event may be delivered more than once.
Use your reference to reconcile events against your own records.
Don't trust a webhook blindly — confirm by calling GET {BASE_URL}/api/payouts/reference/{reference} before releasing goods/services.
9. Error handling
Errors use standard HTTP status codes with a consistent JSON body:
status | meaning |
|---|---|
400 | Invalid request — check the message and field details. |
401 | Authentication failed (missing/invalid signature or headers). |
403 | Authenticated but not permitted for this action. |
429 | Rate limited — slow down and retry with backoff. |
5xx | Temporary issue on our side — retry with backoff. |
Include the request_id from the response when contacting support — it helps us trace the exact request.
10. Go-live checklist
Completed a full sandbox payout end-to-end (completed status observed).
Signature generation verified for both bodied and empty-body requests.
Store your reference for every payout and reconcile via lookups/webhooks.
Handle pending → processing → completed/failed in your system.
Client Secret stored securely (never in client-side code or version control).
Switched Base URL and API keys from sandbox to live.
Support
Include the request_id from the failing response and your payout reference when reaching out. Reach your Bitnob integration contact or the support channel provided with your account.