Payouts
Send money out, with idempotency and optional Finance approval.
Contents
Sending money out
A payout debits your Radok balance and transfers to a Nigerian bank account. Creating one reserves the money immediately — the balance check and the debit happen inside a single database transaction, so two concurrent payouts can never both pass a balance check that only one of them could afford.
If the transfer later fails, the reservation is reversed and the exact amount debited, fee included, is credited back.
Create a payout
/v1/payouts- Auth
- Secret key
- Scope
- payouts:write
Requires an Idempotency-Key header. API key only — never a dashboard session.
Headers
| Field | Type | Description |
|---|---|---|
| Idempotency-Keyrequired | string | Any unique string; a UUID is conventional. Generate it once per logical payout and reuse it on every retry. See Idempotency. |
Body
| Field | Type | Description |
|---|---|---|
| amountrequired | object | { "value": integer, "currency": "NGN" } in kobo. Must be at least the configured minimum (default 100000 — ₦1,000). |
| payoutAccountId | string | A saved, name-enquiry-verified payout account. The recommended way to name a destination. |
| destination | object | A raw destination: bankCode, accountNumber, accountName. Not verified for you — whatever you send is where the money goes. |
Send at most one of the two
payoutAccountId and destination is rejected with 422 rather than resolved by precedence. A request carrying two different beliefs about where money is going is not something to guess at.Omit both and the payout goes to your default payout account. That is the safest integration: your code decides the amount, and the destination is a dashboard decision made by someone authorised to make it. With no default set, you get 400 PAYOUT_DESTINATION_REQUIRED.
curl -X POST https://api.radokpay.com/v1/payouts \
-H "Authorization: Bearer $RADOK_SECRET_KEY" \
-H "Idempotency-Key: 8f14e45f-ea38-4b1e-9c22-3d1c7a90b6f2" \
-H "Content-Type: application/json" \
-d '{ "amount": { "value": 250000, "currency": "NGN" } }'{
"data": {
"reference": "pyt_4e8a1c6b3f0d2957",
"amount": { "value": 250000, "currency": "NGN" },
"fee": { "value": 5000, "currency": "NGN" },
"destination": {
"bankCode": "058",
"accountNumber": "0123454821",
"accountName": "ACME STORES LIMITED"
},
"payoutAccountId": "652f1a9c4d7b3e0812345678",
"status": "pending",
"approvalStatus": "not_required",
"createdAt": "2026-08-23T11:30:00.000Z"
}
}import { randomUUID } from "node:crypto";
// Generated and persisted BEFORE the request, so a crash-and-restart retries
// with the same key instead of creating a second payout.
const idempotencyKey = randomUUID();
await db.payouts.insert({ id: idempotencyKey, status: "sending" });
const response = await fetch("https://api.radokpay.com/v1/payouts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RADOK_SECRET_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: { value: 250_000, currency: "NGN" },
payoutAccountId: "652f1a9c4d7b3e0812345678",
}),
});
const body = await response.json();
if (!response.ok) throw new Error(body.error.code);201 does not mean the money arrived
payout.success or payout.failed before telling anyone the money has landed.Live payouts need KYB
403 KYB_LIVE_REQUIRED until business verification is approved. Test mode has no KYB gate.Fees and the amount debited
Your balance is debited amount + fee. The recipient receives amount. In test mode the fee is always zero, so budget from live figures rather than sandbox ones.
An insufficient balance returns 400 INSUFFICIENT_BALANCE. A payout record is still created in status failed with failureReason: "insufficient_balance_at_creation" — no funds move, and no payout.failed webhook is sent for that path. The check includes the fee, so a balance exactly equal to amount is not enough.
Lifecycle
Status
| Field | Type | Description |
|---|---|---|
| pending | initial | Created and funds reserved. Awaiting execution, or awaiting approval. |
| processing | in flight | Submitted to the bank. Waiting on a final confirmation. |
| succeeded | terminal | Money delivered. Fires payout.success. |
| failed | terminal | The transfer did not complete, or the reservation could not be funded at create. When funds were reserved first, the reservation is reversed and your balance restored, and payout.failed fires. Insufficient balance at create fails the record without that webhook. |
Finance approval
When payout approval is enabled on the merchant account, a new payout is created with approvalStatus: "pending_approval" and is not sent anywhere until someone with the Finance, Admin, or Owner role approves it in the dashboard.
- The funds are reserved at creation regardless, so an approved payout cannot fail for lack of balance later.
- Approval is a server-side merchant policy, never a request field — a caller cannot skip it by omitting a body parameter. Enabling it is currently done by Radok (support / ops), not through the public API.
- Approval is dashboard-only and requires re-entering a password and 2FA code. It cannot be automated with an API key.
/v1/payouts/{payoutId}/approve- Auth
- Dashboard session
- Scope
- payouts:write:approve
Requires a step-up token. Listed for completeness — this is a dashboard action, not an integration point.
List payouts
/v1/payouts- Auth
- Secret key or dashboard session
- Scope
- payouts:read
Query
| Field | Type | Description |
|---|---|---|
| limit | integer | 1–100. Defaults to 20. |
| cursor | string | From meta.nextCursor. |
| environment | string | Required for dashboard sessions only. |
curl "https://api.radokpay.com/v1/payouts?limit=20" \
-H "Authorization: Bearer $RADOK_SECRET_KEY"Retrieve a payout
/v1/payouts/{reference}- Auth
- Secret key or dashboard session
- Scope
- payouts:read
curl https://api.radokpay.com/v1/payouts/pyt_4e8a1c6b3f0d2957 \
-H "Authorization: Bearer $RADOK_SECRET_KEY"A failed payout carries failureReason. Surface it — it is usually something actionable like a closed or dormant destination account.
Before you send real money
- Set a default payout account, and check the verified name is the one you expect.
- Persist your
Idempotency-Keybefore sending, not after. - Subscribe to
payout.successandpayout.failed, and handle both. - If you want Finance approval before money leaves, ask Radok to enable it on your account — it is the cheapest control against a compromised integration.
- Treat
201as “accepted”, never as “paid”.