VAS catalog

Value-added services — airtime, data bundles, and utility payments (ECG, DSTV, GWCL) — are exposed under POST /api/v1/vas/{airtime|data|utilities}, with catalog and validation lookups (e.g. meter/smartcard validation) before payment.

These endpoints ship with the VAS milestone (A8). Once live, they will appear in the API Reference with full request/response schemas and code samples, and this guide will document each service, the provider catalog, and validation flows.

Like every fund-moving call, VAS purchases compute a fee breakdown, post to the ledger, and return the standard envelope.

Your account must be enabled for the service

A vend consults the service StarPay granted you, the same way POST /transfers consults your disbursements grant:

You are buying The grant it needs
Airtime airtime_data
Data airtime_data
A utility bill utility_payments

You have the grant unless StarPay rejected it. If it was rejected, the call answers 403 permission_denied, and error.details.requirement names which one:

{ "status": false,
  "error": { "code": "permission_denied",
             "message": "This service is not enabled for your account. Contact StarPay support.",
             "details": { "requirement": "airtime_data" } } }

Your own metadata

A vend accepts an optional metadata object of your own labels, stored on the purchase and returned with it. Two keys are reserved for StarPay's own use, and a request that sends either is refused with 400 validation_error, before anything is reserved: selfservice_order and selfservice_role. They were added in October 2026: StarPay's own self-service orders use them to find the purchase they settle. The payments API refuses the same two names. If your integration used either as its own label, rename it (for example my_order).

Reading a purchase back

A vend often answers pending and settles seconds or minutes later. Poll it:

GET /api/v1/vas/{reference}

It needs the read scope, and it is scoped to your key's mode, so a test key never reads a live purchase. The payload adds two fields a failed vend needs:

Field What it is
failure_code A short, stable code, for example provider_rejected
failure_reason One sentence you can show a person
updated_at When it last changed, so you can stop polling

A VAS purchase is not a transaction, so it never appears in GET /api/v1/transactions. This endpoint is how you learn its outcome.

Bank codes

POST /transfers and POST /resolve/bank-name both take a bank_code. List the valid ones with:

GET /api/v1/banks

The response names the rail as well as the banks, because a code only means something inside the namespace of the rail that pays it. Read the codes again if your account's routing changes.