Skip to content
VertexBridgeDocs
On this page

Bills & VTU

Buy airtime, data, electricity and cable TV for your customers, charged to your prepaid balance. Read the catalogue, buy with an idempotency key, store what was delivered, and follow each purchase to the end.

Before you start

  • A key with the purchase scope, and each product you sell enabled on your account — GET /v1/partner/me lists them in allowed_products. A product that is not enabled answers 403. Reading the catalogue needs only read.
  • A cleared balance that covers the purchase and our fee. Otherwise the purchase is refused with 402 insufficient_funds or 409 funds_not_cleared before anything is bought.
  • Read Idempotency and Indeterminate transactions first, and send a your_reference with every purchase. Step 3 explains why it matters more here than anywhere else.

The products

ProductEndpointcustomer_referenceCatalogue codes
airtimePOST /v1/partner/purchases/airtimethe phone numbernone
dataPOST /v1/partner/purchases/datathe phone numberbiller_code and package_code
electricityPOST /v1/partner/purchases/electricitythe meter numberbiller_code and package_code
cablePOST /v1/partner/purchases/cablethe smartcard or decoder numberbiller_code and package_code
exam_pinPOST /v1/partner/purchases/exam_pinsee belowbiller_code

Exam PINs cannot be bought correctly yet — see what is not there yet.

1. Read the catalogue

The catalogue covers cable, data and electricity. First, who sells the product:

GET /v1/partner/catalogue/{product}
curl $VERTEXBRIDGE_API_BASE/v1/partner/catalogue/electricity \
  -H "X-API-Key: $VERTEXBRIDGE_API_KEY"

Then what that biller sells:

GET /v1/partner/catalogue/{product}/{biller_code}
curl $VERTEXBRIDGE_API_BASE/v1/partner/catalogue/electricity/$BILLER_CODE \
  -H "X-API-Key: $VERTEXBRIDGE_API_KEY"
  • A code is opaque. Take biller_code and package_code from these lists and send them back unchanged. Never parse one, and never hardcode one you saw once: what a code refers to can change on our side, and a stored code then fails without any warning you could have seen.
  • An empty list is an answer. It means nothing is available for that product right now — not that the product does not exist, and not that something broke.
  • Airtime has no catalogue, by design: it is bought with a phone number and an amount. Asking for its catalogue answers 422, naming the products that have one.
  • A package's amount can be null — a prepaid electricity meter has no fixed price. Then you choose the amount. Never show a null as ₦0.00.

2. Buy

POST /v1/partner/purchases/airtime
curl -X POST $VERTEXBRIDGE_API_BASE/v1/partner/purchases/airtime \
  -H "X-API-Key: $VERTEXBRIDGE_API_KEY" \
  -H "Idempotency-Key: 3c1e9b7a-2d4f-4a6b-9e8c-7f1a2b3c4d5e" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_minor": 10000,
    "customer_reference": "08000000000",
    "your_reference": "order-91c2a7"
  }'
FieldRequiredMeaning
amount_minorYesIn kobo, as a JSON integer greater than zero — 10000 is ₦100.00. For a package with a fixed amount, send that amount: we do not check that the two match, so a mismatch is yours to prevent.
customer_referenceYesWho the purchase is for — see the products table. 3 to 64 characters.
biller_code, package_codePer productFrom the catalogue, unchanged.
your_referenceNo — but send itYour own id for the purchase, up to 128 characters.

Every purchase needs an Idempotency-Key header. The request body also accepts an optional subject_id; there is no endpoint yet that gives you a value for it, so leave it out.

3. Read the answer

StatusMeansDo
201We bought it, and state says how the biller answered.Store the reference, the state and data.
202The biller was told and did not answer. The state is pending.Do not retry and do not refund your customer. Follow it (step 5).
200A replay of a key you already used: the original answer. Nothing was bought again.Treat it as the original.
402, 409, 422, 429This request bought nothing. A 409 request_in_progress means your first request with this key is still running — send the identical request again in a moment.Fix the cause; see Errors.
503Either nothing was bought, or it reached the biller and we could not confirm it.Do not retry. Look it up by your_reference (step 5).
Why your_reference matters most here.

A 503 on a purchase does not say whether the biller was reached, and it carries no reference of ours. If you sent a your_reference, GET /v1/partner/transactions?your_reference=… answers it: no transaction means nothing was bought; otherwise follow its state. Without one, all you can do is contact us with the correlation_id.

4. Store what was delivered

data is what your customer bought — a meter token, a PIN reference. It is on the purchase response when the purchase succeeds straight away, and on GET /v1/partner/purchases/{reference} once it has succeeded — including when that happens later than our first answer to you. It is not in the transactions list.

  • {} means nothing has been delivered yet. It is the same on the purchase response and on the read, so there is one “not yet” to handle, not two.
  • A replay with the same key returns the original answer — so if that was {}, the replay is too. To collect a delivery that arrived later, read the purchase; do not replay it.
  • Store the delivery when you first see it: it is what your customer paid for.

5. Follow it to the end

GET /v1/partner/purchases/{reference} returns the state as it is now. Read it until it is succeeded or failed; while it is pending or indeterminate, do not refund your customer and do not buy again under a new key. A failed purchase gives its money back to your balance. Lost our answer entirely? Find it by the your_reference you sent, with GET /v1/partner/transactions?your_reference=….

What is not there yet

  • Exam PINs. POST /v1/partner/purchases/exam_pin needs a biller_code from the catalogue, but the catalogue does not cover exam PINs, so there is nowhere to get one. Leave exam PINs aside until it does.
  • Checking a customer before you buy. A package can say requires_validation — a meter number belongs to a named person, and buying against the wrong one sends money somewhere it cannot be recovered from. There is no call to check one yet, and the purchase does not check it for you. Confirm the number with your customer before you buy.

End to end

An electricity purchase
import { randomUUID } from 'node:crypto'

const API = process.env.VERTEXBRIDGE_API_BASE + '/v1/partner'
const auth = { 'X-API-Key': process.env.VERTEXBRIDGE_API_KEY }

// 1. Who sells it, and what do they sell? Keep every code exactly as given.
const billers = await (await fetch(`${API}/catalogue/electricity`, { headers: auth })).json()
const biller = billers.find((b) => b.code === chosenBillerCode)
const packages = await (await fetch(`${API}/catalogue/electricity/${biller.code}`, { headers: auth })).json()
const pkg = packages.find((p) => p.code === chosenPackageCode)

// 2. Buy — with a key and your own reference, both SAVED before the request.
const order = await db.orders.create({ idempotencyKey: randomUUID(), state: 'buying' })
const res = await fetch(`${API}/purchases/electricity`, {
  method: 'POST',
  headers: { ...auth, 'Idempotency-Key': order.idempotencyKey, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    amount_minor,                 // what your customer is paying for, in kobo
    customer_reference: meterNumber,
    biller_code: biller.code,
    package_code: pkg.code,
    your_reference: order.id,
  }),
})
const body = await res.json()

// 3–4. Store the delivery when you first see it. If it is {} (not succeeded yet),
//      read GET /purchases/{reference} until it has succeeded — the delivery appears there.
if (res.ok) await db.orders.update(order.id, { reference: body.reference, state: body.state, delivered: body.data })

// 5. Not final? Read it later with GET /purchases/{reference} — never buy it again.