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.
Before you begin
You will need:
what | where to get it |
|---|---|
Base URL (live) | https://api.bitnob.com |
Base URL (sandbox) | Provided with your account — use it for testing. |
Client ID | From your dashboard, under API keys. |
Client Secret | Shown once when you generate your API key — store it securely. |
Always test end-to-end in sandbox first. Sandbox 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.
Authentication
Every API request is authenticated with an HMAC-SHA256 signature. You sign each request with your Client Secret and send four headers. Signatures are time-limited, so requests can't be replayed.
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 |
How to build the signature
Build the payload string by joining four parts with colons, then sign it with your Client Secret using HMAC-SHA256 and hex-encode the result.
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 the body byte-for-byte as it is sent. If you re-serialize or reformat the JSON after signing, the signature will not match.
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. |
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.
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 — the lookup returns the resolved account name.
Step 1 — Create a quote
Locks in the exchange rate and fees for your payout. The response returns a quote_id, the rate, fees, and an expiry. Quotes expire — continue promptly.
field | required | notes |
|---|---|---|
from_asset | Required | The crypto asset funding the payout, e.g. BTC, USDT. |
to_currency | Required | Fiat the beneficiary receives, e.g. NGN. |
source | Required | offchain (charge balance) or onchain (crypto deposit). |
country | Required | Destination country code, e.g. NG. |
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. |
Step 2 — Add the beneficiary (initialize)
Attach the recipient's details to the quote. The exact beneficiary fields depend on the country (see Reference Lookups).
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.
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.
For offchain payouts, your account balance is charged at this step.
Step 4 — Fund the payout
offchain — nothing to do. Your balance was charged at finalize, and the payout proceeds automatically to settlement.
onchain — send a crypto deposit to the address / instruction returned from the previous step. You can fund it with BTC (on-chain), Lightning, or stablecoins. Once the deposit confirms, settlement proceeds.
You can't broadcast a real deposit in sandbox, so simulate it with POST {BASE_URL}/api/payouts/quotes/{quoteId}/deposits and a body of { "amount": "50000" }.
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.
Payout statuses
A payout moves through these states:
status | meaning |
|---|---|
pending | Created and awaiting funding. |
processing | Funded; 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.
Full worked example (offchain)
The sequence on the right assumes a signed-request helper that sets the X-Auth-* headers for each call. It creates a quote, attaches the beneficiary, finalizes, and tracks by reference — reusing one reference throughout.
Webhooks
If you set callback_url when initializing, we send a notification to that URL whenever the payout's status changes. 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.
Error handling
Errors use standard HTTP status codes with a consistent JSON body. Include the request_id from the response when contacting support — it helps us trace the exact request.
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. |
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.