Skip to content
VertexBridgeDocs
On this page

Idempotency

Every request that moves money carries an Idempotency-Key header. Send the same request with the same key as often as you need to: it happens once, and every repeat gets the original answer back.

Why it exists

Networks fail in the worst place: after we have acted, before you hear about it. Your request times out, and you cannot tell whether the payout was made. Without a key, the only safe-looking move — sending it again — is the one that pays twice. With a key, sending it again is always safe, because we recognise the second request as the first.

Where it is required

On every endpoint that moves money. A request to one of these without the header is refused with 400 idempotency_key_required before its body is even read.

  • POST /v1/partner/payouts
  • POST /v1/partner/purchases/airtime
  • POST /v1/partner/purchases/cable
  • POST /v1/partner/purchases/data
  • POST /v1/partner/purchases/electricity
  • POST /v1/partner/purchases/exam_pin

The rules

  1. One key per payout. A random UUID is ideal. Create it when you decide to make the payout, not when you send the request.
  2. Save it before you send. Store the key with your own record of the payout, so that a crash between sending and recording cannot lose it.
  3. Retry with the same key and the identical request. Never change a request and keep its key; never send the same payout under a new key.
  4. At most 255 characters. A longer key is refused with 422 validation_failed.
  5. Keys are yours alone. They are scoped to your account, so another partner using the same key never collides with you. One key covers every endpoint: do not reuse a key from one operation on another.
  6. We remember a key for 30 days. After that, the same key starts a new operation — which is one more reason never to reuse keys.

What each answer means

StatusMeansDo
201 or 202The first time we saw this key. This request did the work.Record the answer.
200A replay: we have seen this key and request before. This is the original answer, and nothing was done again.Treat it exactly as you would have treated the original.
409 request_in_progressThe first request with this key is still being processed. (A payout can also answer 409 funds_not_cleared, which is about your balance, not your key — branch on code.)Wait a moment and send the identical request again.
422 idempotency_key_reusedThis key was already used for a different request.Find the bug that reused it. A new payout needs a new key.
400 idempotency_key_requiredNo key was sent.Send one.

If a request is refused before any money moves — 402 insufficient_funds, 409 funds_not_cleared, or a 422 validation_failed because we could not confirm the account — nothing was sent and the key is released. Send the request again once the cause is fixed, with the same key or a new one. A 503 is the same on a payout, but not on a purchase, where it can mean the purchase reached the biller: see Errors.

A replay is the original answer — not the current state

A replay returns exactly what the first request returned. If that was 202 with the state pending, every replay returns pending, however long ago it resolved. Replaying is how you safely find out what happened to your request, not how you follow a payout afterwards.

To follow a payout or a purchase, read it.

GET /v1/partner/payouts/{reference} and GET /v1/partner/purchases/{reference} return what is true now, using the reference from the first answer. That is the call to make after a 202, or any pending or indeterminate answer — and until it says succeeded or failed, follow Indeterminate transactions: do not refund, and do not send it again. If you never received our answer, so have no reference, find the transaction by the your_reference you sent, with GET /v1/partner/transactions?your_reference=….

What we compare

When a key arrives a second time, we compare the new request with the first on amount_minor, bank_code and a hash of the full account_number. A difference in any of them is refused with 422 idempotency_key_reused. We keep only a one-way hash of the account number for this, never the number itself.

your_reference is not compared: a key sent again with a different your_reference but the same payout is a replay, and gets the first answer back. That is the check working as intended — but it is a check, not a substitute for giving every payout its own key.

In code

Sending a payout safely
import { randomUUID } from 'node:crypto'

// 1. Create the key ONCE, when you decide to make the payout, and save it
//    with your own record of the payout — before sending anything.
const payout = await db.payouts.create({ ...details, idempotencyKey: randomUUID() })

// 2. Send. On a timeout or a dropped connection, send the SAME request again
//    with the SAME key. You will get the original answer, never a second payout.
const res = await fetch(process.env.VERTEXBRIDGE_API_BASE + '/v1/partner/payouts', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.VERTEXBRIDGE_API_KEY,
    'Idempotency-Key': payout.idempotencyKey,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(payout.request),
})

// 3. 200 means "you already sent this — here is what happened then".
//    201 and 202 mean this request was the one that did the work.
const wasReplay = res.status === 200

The request body is described in the API reference.