Payout API
One integration, every corridor. Prefund a USD float with Stablique, and send international payouts to bank accounts and mobile wallets across Africa, the Middle East, and Asia. We screen each instruction, debit your float at the quoted rate, and route it to the licensed partner with the best all-in price for that corridor at that moment.
Overview
Every payout follows the same three steps, whichever corridor it goes to:
- Quote — price a corridor and lock a rate.
- Create — send against that quote with the beneficiary details.
- Track — receive a
payout.statuswebhook on every state change, and reconcile against the daily file.
https://api.sandbox.stablique.xyz · Production: https://api.stablique.xyz. Both are provisioned with your API key during onboarding — email developers@stablique.xyz to request sandbox access. All amounts you send are in USD; all timestamps are RFC 3339 in UTC.Authentication
Authenticate with your API key as a bearer token. Keys are issued per institution and per environment; sandbox keys carry the sk_sandbox_ prefix, live keys sk_live_. Keys are secret — use them server-side only, and rotate them from the dashboard.
Authorization: Bearer sk_sandbox_7f3a9c1e4b8d2056a1c9e7f4b3d80a26
Content-Type: application/jsonQuickstart
A complete payout in two calls. The quote locks the rate; the payout consumes the quote and debits your float.
# 1 — price the corridor
curl -X POST https://api.sandbox.stablique.xyz/v1/quotes \
-H "Authorization: Bearer $STABLIQUE_KEY" \
-H "Content-Type: application/json" \
-d '{ "corridor": "AE", "amount": 25000 }'
# 2 — send against the quote
curl -X POST https://api.sandbox.stablique.xyz/v1/payouts \
-H "Authorization: Bearer $STABLIQUE_KEY" \
-H "Content-Type: application/json" \
-d '{
"quote_id": "qt_9f1c2b3a4d5e6f70",
"reference": "DBK-IMP-8801",
"beneficiary": {
"name": "Gulf Star General Trading LLC",
"country": "AE",
"account_number": "AE070331234567890123456",
"bank_name": "Emirates NBD"
}
}'Payout lifecycle
A payout moves through four states, and one terminal failure state. Each transition fires a webhook.
receivedInstruction accepted and screened. Value plus fees reserved against your float.routedRouting engine selected a licensed partner and handed off the instruction.settledFunds settled to the partner. Your float is debited and the ledger entry posts.paid_outPartner credited the beneficiary in local currency. Terminal success state.failedRejected, with a reason_code. The reserve is released in full — a failed payout costs you nothing.Corridors
Lists live corridors, supported payout methods, limits, and expected delivery windows. Corridors are configuration on our side: new destinations appear here without a release on yours.
{
"object": "list",
"data": [
{
"object": "corridor",
"code": "AE",
"country": "United Arab Emirates",
"source_currency": "USD",
"destination_currency": "AED",
"payout_methods": ["bank_transfer"],
"min_amount": 10,
"max_amount": 500000,
"estimated_delivery": { "min_minutes": 5, "max_minutes": 45 },
"routing_depth": 3
}
]
}Quotes
Prices your instruction with every partner able to serve the corridor and locks the winning rate. Quotes expire — rates move, and an expired quote cannot be used.
| Parameter | Type | Description |
|---|---|---|
corridor required | string | Destination country, ISO 3166-1 alpha-2. For example AE, CN, IN, TR, ZM, ZA. |
amount required | number | Amount to send, in USD. Must sit inside the corridor’s limits. |
payout_method | string | bank_transfer or mobile_money. Defaults to the corridor’s primary method. |
{
"id": "qt_9f1c2b3a4d5e6f70",
"object": "quote",
"corridor": "AE",
"payout_method": "bank_transfer",
"source_amount": 25000,
"source_currency": "USD",
"destination_amount": 91632.52,
"destination_currency": "AED",
"rate": 3.66530064,
"fee": 114.00,
"total_debit": 25114.00,
"estimated_delivery_minutes": 15,
"status": "active",
"expires_at": "2026-08-13T04:13:53Z",
"created_at": "2026-08-13T04:12:23Z"
}Payouts
Creates a payout against an active quote. Idempotent by reference: replaying the same reference returns the original payout rather than sending twice.
| Parameter | Type | Description |
|---|---|---|
quote_id required | string | An active quote. Consumed by this call. |
reference required | string | Your reference. Must be unique — this is the idempotency key for sending. |
beneficiary.name required | string | Legal name of the party being paid. |
beneficiary.account_number | string | Required for bank_transfer. IBAN or local account number. |
beneficiary.mobile_number | string | Required for mobile_money. E.164 format. |
beneficiary.bank_name, bank_code, country | string | Optional routing detail. Improves straight-through processing. |
metadata | object | Free-form keys stored with the payout and returned on every read. |
Retrieve a payout
Returns the payout with its full status_history. Poll this if you need state on demand, but prefer webhooks for tracking.
List payouts
Filter with status, corridor, reference, and limit. Paginate with starting_after.
{
"id": "po_1a2b3c4d5e6f7080",
"object": "payout",
"status": "paid_out",
"reference": "DBK-IMP-8801",
"quote_id": "qt_9f1c2b3a4d5e6f70",
"corridor": "AE",
"payout_method": "bank_transfer",
"source_amount": 25000,
"source_currency": "USD",
"fee": 114.00,
"total_debit": 25114.00,
"destination_amount": 91632.52,
"destination_currency": "AED",
"rate": 3.66530064,
"beneficiary": {
"name": "Gulf Star General Trading LLC",
"country": "AE",
"account_number": "AE070331234567890123456",
"bank_name": "Emirates NBD"
},
"metadata": {},
"created_at": "2026-08-13T04:22:02Z",
"updated_at": "2026-08-13T04:22:31Z",
"status_history": [
{ "status": "received", "detail": "Instruction accepted, screened, and float reserved",
"occurred_at": "2026-08-13T04:22:02Z" },
{ "status": "routed", "detail": "Routed to payout partner",
"occurred_at": "2026-08-13T04:22:06Z" },
{ "status": "settled", "detail": "Funds settled to payout partner, float debited",
"occurred_at": "2026-08-13T04:22:13Z" },
{ "status": "paid_out", "detail": "Beneficiary paid in AED",
"occurred_at": "2026-08-13T04:22:22Z" }
]
}Balance
Your prefunded USD float, split three ways:
| Field | Meaning |
|---|---|
balance | Settled position. Payouts debit this when they reach settled. |
reserved | Committed to payouts in flight. Released on settlement or failure. |
available | balance − reserved. What the next payout can draw on. |
available returns 402 insufficient_float. No credit is extended and no payout is ever partially sent.Webhooks
Every state change delivers a payout.status event carrying the full payout object. One event type means one handler: switch on data.status.
POST https://your-endpoint.example/webhooks/stablique
Stablique-Event: payout.status
Stablique-Signature: t=1755060151,v1=8f4c1d2e…
{
"id": "evt_4ceb7e4155a6bd63",
"object": "event",
"type": "payout.status",
"created_at": "2026-08-13T04:22:22Z",
"data": { "id": "po_1a2b3c4d5e6f7080", "status": "paid_out", ... }
}Verifying the signature
Compute HMAC-SHA256 over {timestamp}.{raw body} with your endpoint secret, and compare in constant time. Reject stale timestamps to prevent replay.
import crypto from "node:crypto";
// Header: Stablique-Signature: t=1755060000,v1=9f86d081…
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((part) => part.split("=")),
);
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
// Constant-time compare, and reject anything older than five minutes.
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
const match = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(parts.v1),
);
return fresh && match;
}2xx within 10 seconds. Anything else is retried with exponential backoff for up to eight attempts, so your handler must be idempotent — treat data.id plus data.status as the deduplication key.Idempotency
Retries are safe. Send an Idempotency-Key header on any POST: replaying the key with an identical body returns the stored response with Idempotent-Replay: true. Reusing a key with a different body returns 409.
Payout creation is additionally idempotent on your reference, so a network timeout on send can never pay a beneficiary twice.
Errors
Every failure returns the same envelope, so you write one error handler and never special-case an endpoint.
{
"error": {
"type": "invalid_request_error",
"code": "quote_expired",
"message": "This quote expired at 2026-08-13T04:13:53Z. Rates move — request a new quote."
}
}| Status | Code | What to do |
|---|---|---|
| 400 | parameter_missing, parameter_invalid | Fix the field named in param and retry. |
| 400 | corridor_unsupported, amount_out_of_range | Check GET /v1/corridors for live coverage and limits. |
| 401 | invalid_api_key, revoked_api_key | Check the key and the environment it belongs to. |
| 402 | insufficient_float | Top up the float. Nothing was sent. |
| 409 | quote_expired, quote_already_used | Request a fresh quote and resend. |
| 409 | duplicate_reference | The payout already exists. Fetch it with GET /v1/payouts?reference=…. |
| 503 | no_partner_available | No partner can serve that corridor right now. Retry shortly. |
Testing
Sandbox runs the full lifecycle against mock partners, compressed into seconds. Sandbox accounts are prefunded, and no real money moves. These triggers force specific outcomes:
| Trigger | Result |
|---|---|
reference starting with TEST_FAIL | Partner rejects the payout. Ends failed with beneficiary_account_invalid, reserve released. |
account_number of 000000000000 | Same rejection path, triggered from the beneficiary side. |
| Amount above your available float | 402 insufficient_float at creation. |
| Reusing a consumed or expired quote | 409 quote_already_used or 409 quote_expired. |
File channel
Not every institution can ship an API integration on day one. Upload a CSV of payout instructions from the dashboard and the rows run through the same routing engine, the same float, and the same lifecycle as an API call — the file is just a different door. Start on files, graduate to the API, keep the same reconciliation.
reference,corridor,amount,payout_method,beneficiary_name,beneficiary_country,account_number,bank_name,bank_code,mobile_number
DBK-IMP-8801,AE,42500.00,bank_transfer,Gulf Star General Trading LLC,AE,AE070331234567890123456,Emirates NBD,EBILAEAD,
DBK-IMP-8805,ZM,9800.00,mobile_money,Copperfields Logistics Ltd,ZM,,,,+260971234567Reconciliation files map one-to-one to the instructions you sent, and are downloadable for any date range from the Float & ledger screen.
