Skip to content
VertexBridgeDocs
On this page

Payouts

Send naira from your prepaid balance to any account at a Nigerian bank, in four calls: pick the bank, confirm who owns the account, send, and follow the payout to the end.

Before you start

  • A key with the payout scope. Every payout endpoint requires it — even the bank list — and answers 403 without it. See Scopes.
  • Payouts enabled on your account. GET /v1/partner/me lists payout in allowed_products once they are.
  • A balance that covers the payout and our fee. If it does not, the payout is refused with 402 insufficient_funds before anything is sent.
  • Cleared money, not just money. Funds you have just added may be held while they clear — GET /v1/partner/balance shows how much, and next_clears_at when some of it clears. Held money cannot fund a payout. If the part of your balance that has cleared cannot cover the payout and our fee, the payout is refused with 409 funds_not_cleared before anything is sent — wait the retry_after_seconds it gives, then send the same request again. See Errors: the time it gives is when the next part clears, which may not be enough on its own.
  • Read Idempotency and Indeterminate transactions first. They are the two things a payout integration must get right.

The flow

StepCall
1. Pick the bankGET /v1/partner/payouts/banks
2. Confirm the account holderPOST /v1/partner/payouts/enquiry
3. SendPOST /v1/partner/payouts, with an Idempotency-Key
4–5. Read how it endedGET /v1/partner/payouts/{reference}

1. Pick the bank

The banks a payout can reach, sorted by name — build your bank picker from this.

GET /v1/partner/payouts/banks
curl $VERTEXBRIDGE_API_BASE/v1/partner/payouts/banks \
  -H "X-API-Key: $VERTEXBRIDGE_API_KEY"
200 OK — two of the entries
[
  { "code": "044", "name": "Access Bank" },
  { "code": "035A", "name": "ALAT by Wema" }
]
A bank code is an entry in this list, not a format.

Most codes are three digits, but not all: some are longer, and 035A (ALAT by Wema) contains a letter. Take code from this list and send it back in bank_code exactly as given. Do not validate it, pad it, trim it or convert it to a number on your side — a check like “three digits” will reject real banks.

2. Confirm the account

Ask who owns the account before you send anything. Show the name to whoever is sending the money, and have them confirm it is the person they mean.

POST /v1/partner/payouts/enquiry
curl -X POST $VERTEXBRIDGE_API_BASE/v1/partner/payouts/enquiry \
  -H "X-API-Key: $VERTEXBRIDGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "account_number": "0000000000", "bank_code": "044" }'
  • account_name is the holder's name, as the bank spells it.
  • If it is null, the account could not be confirmed, and message says why — the account was not found, or it could not be checked right now. That is an answer, not an error: the response is still 200. Do not send.
  • account_number is exactly 10 digits. Enquiries have a rate limit of their own, on top of your account's.

3. Send

POST /v1/partner/payouts
curl -X POST $VERTEXBRIDGE_API_BASE/v1/partner/payouts \
  -H "X-API-Key: $VERTEXBRIDGE_API_KEY" \
  -H "Idempotency-Key: 6f1d2c3b-8a4e-4f5d-9b6a-0c1e2d3f4a5b" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_minor": 500000,
    "account_number": "0000000000",
    "bank_code": "044",
    "your_reference": "order-7f3a9c21"
  }'
FieldRequiredMeaning
amount_minorYesWhat the beneficiary receives, in kobo, as a JSON integer greater than zero — 500000 is ₦5,000.00. (Amounts we send back to you are digit strings; amounts you send us are integers.) Payouts are in NGN only.
account_numberYesExactly 10 digits.
bank_codeYesA code from the bank list, unchanged.
your_referenceNoYour own id for the payout, up to 128 characters, returned in the answer.

There is no name field. We confirm the account holder with the bank again when you send, and pay the name the bank gives — if it can no longer be confirmed, the payout is refused with 422 validation_failed and nothing is sent.

4. Read the answer

StatusMeansDo
201We sent it, and state says how the bank answered: succeeded, failed, pending or indeterminate.Record the reference and the state.
202We sent the instruction to the bank and could not confirm what happened. The state is pending, and the money may already be on its way.Do not retry. Follow it with the read endpoint.
200A replay of a key you already used: the original answer, and nothing sent again.Treat it as the original.
409Two different things — branch on code. request_in_progress: this key is still being processed. funds_not_cleared: the money is still clearing. Nothing new was sent.Wait, then send the identical request again.
402, 422, 503Refused before anything was sent.Fix the cause and send again. See Errors.

The reference is ours, and it is how you ask about this payout from now on — store it.

5. Follow it to the end

succeeded and failed are final. pending and indeterminate are not: read the payout until it is one of the final two.

GET /v1/partner/payouts/{reference}
curl $VERTEXBRIDGE_API_BASE/v1/partner/payouts/$REFERENCE \
  -H "X-API-Key: $VERTEXBRIDGE_API_KEY"

A failed payout gives its money back to your balance. While it is pending or indeterminate, never refund your user and never send it again under a new key — Indeterminate transactions explains why. Another partner's reference, and one that does not exist, both answer 404.

Lost our answer? If your request timed out you will not have our reference — but if you sent a your_reference, find the payout with GET /v1/partner/transactions?your_reference=…. No transaction means nothing was sent. That is why it is worth sending one every time.

The payout object

FieldWhen sendingWhen reading
reference, stateAlwaysAlways
charged — what left your balance: the amount plus our feeOn 201Always
amount — what the beneficiary receivesOn 201 and 202Not returned
fee — oursOn 201Not returned
account_name — the name we paidAlwaysNot returned — we do not keep it
your_referenceIf you sent oneNot returned

So keep what you need from the first answer. Amounts are MoneyOut objects; the full schema is in the API reference.

End to end

A payout, from bank picker to final state
import { randomUUID } from 'node:crypto'

const BASE = process.env.VERTEXBRIDGE_API_BASE + '/v1/partner/payouts'
const auth = { 'X-API-Key': process.env.VERTEXBRIDGE_API_KEY }
const json = { ...auth, 'Content-Type': 'application/json' }

// 1. The bank picker. Keep `code` exactly as given.
const banks = await (await fetch(`${BASE}/banks`, { headers: auth })).json()

// 2. Who owns the account? Show the name and have your user confirm it.
const { account_name, message } = await (
  await fetch(`${BASE}/enquiry`, {
    method: 'POST',
    headers: json,
    body: JSON.stringify({ account_number, bank_code }),
  })
).json()
if (account_name === null) throw new Error(message) // an answer, not a fault: do not send

// 3. Send — with a key created and SAVED before the request.
const payout = await db.payouts.create({ idempotencyKey: randomUUID(), state: 'sending' })
const res = await fetch(BASE, {
  method: 'POST',
  headers: { ...json, 'Idempotency-Key': payout.idempotencyKey },
  body: JSON.stringify({ amount_minor, account_number, bank_code, your_reference: payout.id }),
})
const body = await res.json()
if (!res.ok) throw new Error(`${body.code}: ${body.message}`) // nothing was sent
await db.payouts.update(payout.id, { reference: body.reference, state: body.state })

// 4–5. Not final? Read it later — never send it again.
//      (succeeded and failed are final; pending and indeterminate are not.)