RADOK

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

POST/v1/payouts
Auth
Secret key
Scope
payouts:write

Requires an Idempotency-Key header. API key only — never a dashboard session.

Headers

FieldTypeDescription
Idempotency-KeyrequiredstringAny unique string; a UUID is conventional. Generate it once per logical payout and reuse it on every retry. See Idempotency.

Body

FieldTypeDescription
amountrequiredobject{ "value": integer, "currency": "NGN" } in kobo. Must be at least the configured minimum (default 100000 — ₦1,000).
payoutAccountIdstringA saved, name-enquiry-verified payout account. The recommended way to name a destination.
destinationobjectA raw destination: bankCode, accountNumber, accountName. Not verified for you — whatever you send is where the money goes.

Send at most one of the two

Passing both 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 — to the default account
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" } }'
201 Created
{
  "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"
  }
}
Node.js — to a specific saved account
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

It means the payout was accepted and the funds reserved. The transfer happens asynchronously. Wait for payout.success or payout.failed before telling anyone the money has landed.

Live payouts need KYB

In live mode, create returns 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

FieldTypeDescription
pendinginitialCreated and funds reserved. Awaiting execution, or awaiting approval.
processingin flightSubmitted to the bank. Waiting on a final confirmation.
succeededterminalMoney delivered. Fires payout.success.
failedterminalThe 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.
POST/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

GET/v1/payouts
Auth
Secret key or dashboard session
Scope
payouts:read

Query

FieldTypeDescription
limitinteger1–100. Defaults to 20.
cursorstringFrom meta.nextCursor.
environmentstringRequired for dashboard sessions only.
curl
curl "https://api.radokpay.com/v1/payouts?limit=20" \
  -H "Authorization: Bearer $RADOK_SECRET_KEY"

Retrieve a payout

GET/v1/payouts/{reference}
Auth
Secret key or dashboard session
Scope
payouts:read
curl
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

  1. Set a default payout account, and check the verified name is the one you expect.
  2. Persist your Idempotency-Key before sending, not after.
  3. Subscribe to payout.success and payout.failed, and handle both.
  4. 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.
  5. Treat 201 as “accepted”, never as “paid”.