API Guide
Everything you need to sell airtime, data, electricity and cable TV through the VatPay Partner API.
Before you start
- Get approved. Apply for an account; we review it and set your commission.
- Fund your wallet. Transfer to the funding account shown on your dashboard. Every purchase is paid from this wallet.
- Create an API key on the API Keys page. The full key is shown once, so copy it straight into your server's secret store.
- Optional: add your servers' IP addresses to the allowlist so the key only works from them.
| Base URL | https://<partner-api-address> |
|---|---|
| Protocol | HTTPS, JSON bodies (Content-Type: application/json) |
| Currency | Naira (NGN). Every amount is in naira, e.g. 1500 or 1500.50, never kobo. |
| Times | West Africa Time (UTC+1), e.g. 2026-10-04T09:26:11 |
| Test environment | There is no sandbox yet. Test with small real purchases, such as ₦50 airtime to your own phone. |
Authentication
Send your API key in the X-Api-Key header on every request:
X-Api-Key: vp_live_3f9a12bc07_xxxxxxxxxxxxxxxxxxxxxxxx
- Call the API from your server only. Never put the key in a mobile app, a web page or a public repository.
- You can hold more than one key. To rotate one, create a new key, deploy it, then revoke the old one in the portal.
- A missing, wrong or revoked key returns
401 INVALID_API_KEY. A key used from an address that isn't on your allowlist returns403 IP_NOT_ALLOWED.
Requests and responses
Field names are camelCase. Every response, success or error, has the same envelope:
{
"code": "OK", // "OK", or an error code from the Errors table
"message": "Purchase successful.", // human-readable; show it to your staff, not your logic
"data": { ... } // the result; may be absent on errors
}
Base your logic on the HTTP status, code and, for purchases, data.status. Fields with no value are left out of the response rather than sent as null.
An invalid body returns 400 INVALID_REQUEST, with each problem field listed in data:
{
"code": "INVALID_REQUEST",
"message": "The request is invalid.",
"data": {
"phoneNumber": [ "The PhoneNumber field is required." ],
"reference": [ "reference may only contain letters, digits and _ - . :" ]
}
}
References and retries
Every purchase carries your own reference: your order or transaction id. It is what makes the API safe to retry.
- 1 to 64 characters: letters, digits and
_ - . :. It must be unique across all your purchases. - Same reference + same body returns the original purchase, with its current status. You are never charged twice, even if two identical requests arrive together.
- Same reference + different body returns
409 DUPLICATE_REFERENCE, and nothing is charged. - Got a timeout or a network error? Don't create a new reference. Re-send the same request, or call Check a purchase with it.
Purchase status and money
| HTTP | data.status | What happened | Your wallet | What to do |
|---|---|---|---|---|
| 200 | SUCCESS | Delivered. Electricity tokens are in data.token. | Charged; commission credited | Give the customer their value. |
| 200 | PENDING | The provider hasn't confirmed yet. | Held | Tell the customer it's processing. Requery every 30 to 60 seconds until it changes. Most settle within minutes; a few take up to 6 hours. |
| 200 | FAILED | The provider refused it after the charge. | Refunded automatically (refunded: true) | Tell the customer it failed. Retry with a new reference if you want. |
| 422 | FAILED | Refused before any charge, e.g. low balance or invalid meter. | Not charged | Read code and fix the cause. |
The purchase object (data) returned by every purchase and requery:
| reference | string | Your reference |
| transactionId | number | VatPay's id for the purchase. Quote it to support. Absent if nothing was charged. |
| kind | string | airtime, data, electricity or cable |
| service | string | e.g. MTN, IKEDC, DSTV |
| status | string | SUCCESS, PENDING or FAILED |
| message | string | What happened, in plain words |
| amount | number | The purchase amount in naira |
| amountCharged | number | What your wallet is currently paying for it: the amount, or 0 once it's refunded or if it was never charged |
| commission | number | Commission you earned, credited to your commission balance (separate from your wallet) |
| refunded | boolean | true when a charged purchase failed and the money is back in your wallet |
| token | string | Electricity token, on prepaid SUCCESS only |
| providerReference | string | The provider's own reference, when they return one |
| createdAt | datetime | When we received the purchase |
Recommended flow
- Once a day: call GET /v1/services and cache the
serviceIds. For data and cable, cache the plans too. - Electricity and cable: validate the meter or smartcard first and show the customer name to the customer for confirmation.
- Save your order with a new
referencebefore calling the API, so a crash can't lose it. - Call the purchase endpoint with a 60-second timeout.
- Act on the result: SUCCESS → deliver; PENDING → requery on a schedule; FAILED → tell the customer. On a timeout, requery the same reference.
- Reconcile daily with GET /v1/transactions and look at anything still PENDING.
Endpoints
/v1/balanceYour wallet balance (what you can spend) and your commission balance, in naira.
curl https://<partner-api-address>/v1/balance -H "X-Api-Key: $VATPAY_API_KEY"
{
"code": "OK",
"data": { "balance": 248500.00, "commissionBalance": 3120.50, "currency": "NGN" }
}
/v1/servicesEvery service you can buy, with the serviceId to send in purchases and your commission on it. kind tells you which purchase endpoint to use.
curl https://<partner-api-address>/v1/services -H "X-Api-Key: $VATPAY_API_KEY"
{
"code": "OK",
"data": [
{ "serviceId": 7, "kind": "airtime", "product": "Airtime", "service": "MTN", "commissionPercent": 2.0 },
{ "serviceId": 3, "kind": "data", "product": "Data", "service": "MTN", "commissionPercent": 2.5 },
{ "serviceId": 13, "kind": "electricity", "product": "Electricity", "service": "IKEDC", "commissionPercent": 1.0 },
{ "serviceId": 1, "kind": "cable", "product": "Cable TV", "service": "DSTV", "commissionPercent": 0.5 }
]
}
/v1/services/{serviceId}/plansData bundles and cable TV bouquets for a data or cable service. Use a plan's planCode when buying.
amountis the plan's price in naira. Send exactly that as the purchaseamount.- A plan with no
amounthas an open price: you choose the amount. - Plans change from time to time. Refresh your cached list daily.
curl https://<partner-api-address>/v1/services/3/plans -H "X-Api-Key: $VATPAY_API_KEY"
{
"code": "OK",
"data": [
{ "planCode": "10903", "name": "1.5GB Monthly", "amount": 1500.00 },
{ "planCode": "10905", "name": "10GB Monthly", "amount": 5000.00 }
]
}
Airtime and electricity have no plans: asking for them returns 400 UNKNOWN_SERVICE.
/v1/validate/meterLook up an electricity meter before you sell to it, and show the customer the name it returns. Nothing is charged.
| Field | Type | Required | Description |
|---|---|---|---|
| serviceId | number | Yes | An electricity service from /v1/services |
| meterNumber | string | Yes | The customer's meter (or account) number |
| meterType | string | Yes | PREPAID or POSTPAID |
| amount | number | No | What the customer wants to buy. Some electricity companies check it against the meter's minimum; without it we check at ₦1,000. |
curl -X POST https://<partner-api-address>/v1/validate/meter \
-H "X-Api-Key: $VATPAY_API_KEY" -H "Content-Type: application/json" \
-d '{ "serviceId": 13, "meterNumber": "45012345678", "meterType": "PREPAID", "amount": 5000 }'
{
"code": "OK",
"data": {
"customerName": "ADEBAYO OLUWASEGUN",
"address": "12 ALLEN AVENUE, IKEJA",
"accountNumber": "45012345678",
"minimumAmount": 1000.00,
"outstandingBalance": 0.00
}
}
An unknown meter returns 422 INVALID_CUSTOMER with the provider's reason in message.
/v1/validate/smartcardLook up a DSTV or GOtv smartcard (IUC number) and get the customer name. Nothing is charged.
| Field | Type | Required | Description |
|---|---|---|---|
| serviceId | number | Yes | A cable service from /v1/services |
| smartCardNumber | string | Yes | The smartcard / IUC number |
| planCode | string | No | The bouquet the customer wants; any plan works for the lookup |
curl -X POST https://<partner-api-address>/v1/validate/smartcard \
-H "X-Api-Key: $VATPAY_API_KEY" -H "Content-Type: application/json" \
-d '{ "serviceId": 1, "smartCardNumber": "7012345678" }'
{
"code": "OK",
"data": { "customerName": "CHIOMA NWOSU", "accountNumber": "7012345678" }
}
/v1/purchase/airtimeTop up any MTN, Airtel, Glo or 9mobile number.
| Field | Type | Required | Description |
|---|---|---|---|
| reference | string | Yes | Your unique reference (see References and retries) |
| serviceId | number | Yes | An airtime service from /v1/services |
| amount | number | Yes | Naira |
| phoneNumber | string | Yes | e.g. 08031234567 |
curl -X POST https://<partner-api-address>/v1/purchase/airtime \
-H "X-Api-Key: $VATPAY_API_KEY" -H "Content-Type: application/json" \
-d '{ "reference": "ORD-10231", "serviceId": 7, "amount": 500, "phoneNumber": "08031234567" }'
{
"code": "OK",
"message": "Purchase successful.",
"data": {
"reference": "ORD-10231",
"transactionId": 884213,
"kind": "airtime",
"service": "MTN",
"status": "SUCCESS",
"message": "Purchase successful.",
"amount": 500.00,
"amountCharged": 500.00,
"commission": 10.00,
"refunded": false,
"providerReference": "FBN|WEB|MX12345|04-10-2026|123456",
"createdAt": "2026-10-04T09:26:11"
}
}
/v1/purchase/dataBuy a data bundle. Get planCode and its price from the plans endpoint.
| Field | Type | Required | Description |
|---|---|---|---|
| reference | string | Yes | Your unique reference |
| serviceId | number | Yes | A data service from /v1/services |
| planCode | string | Yes | From /v1/services/{serviceId}/plans |
| amount | number | Yes | The plan's amount in naira |
| phoneNumber | string | Yes | The number to receive the bundle |
curl -X POST https://<partner-api-address>/v1/purchase/data \
-H "X-Api-Key: $VATPAY_API_KEY" -H "Content-Type: application/json" \
-d '{ "reference": "ORD-10232", "serviceId": 3, "planCode": "10903", "amount": 1500, "phoneNumber": "08031234567" }'
The response is a purchase object, as for airtime.
/v1/purchase/electricityBuy a prepaid token or pay a postpaid bill. We validate the meter again just before charging, so an invalid meter is refused without a charge.
| Field | Type | Required | Description |
|---|---|---|---|
| reference | string | Yes | Your unique reference |
| serviceId | number | Yes | An electricity service from /v1/services |
| meterNumber | string | Yes | Meter (or account) number |
| meterType | string | Yes | PREPAID or POSTPAID |
| amount | number | Yes | Naira; at least the meter's minimumAmount |
| phoneNumber | string | No | Customer's phone number |
| customerEmail | string | No | Customer's email |
curl -X POST https://<partner-api-address>/v1/purchase/electricity \
-H "X-Api-Key: $VATPAY_API_KEY" -H "Content-Type: application/json" \
-d '{ "reference": "ORD-10233", "serviceId": 13, "meterNumber": "45012345678",
"meterType": "PREPAID", "amount": 5000, "phoneNumber": "08031234567" }'
{
"code": "OK",
"message": "Purchase successful.",
"data": {
"reference": "ORD-10233",
"transactionId": 884214,
"kind": "electricity",
"service": "IKEDC",
"status": "SUCCESS",
"amount": 5000.00,
"amountCharged": 5000.00,
"commission": 50.00,
"refunded": false,
"token": "5555 6666 7777 8888 9999",
"createdAt": "2026-10-04T09:28:40"
}
}
token and keep it in your records. Postpaid payments return no token. Some electricity companies cap the commission per purchase, and a few postpaid accounts earn none: data.commission always shows what you actually earned./v1/purchase/cablePay a DSTV or GOtv subscription. Validate the smartcard first and confirm the name with the customer.
| Field | Type | Required | Description |
|---|---|---|---|
| reference | string | Yes | Your unique reference |
| serviceId | number | Yes | A cable service from /v1/services |
| smartCardNumber | string | Yes | Smartcard / IUC number |
| planCode | string | Yes | The bouquet, from /v1/services/{serviceId}/plans |
| amount | number | Yes | The bouquet's amount in naira |
| planName | string | No | Bouquet name, for your records |
| phoneNumber | string | No | Customer's phone |
curl -X POST https://<partner-api-address>/v1/purchase/cable \
-H "X-Api-Key: $VATPAY_API_KEY" -H "Content-Type: application/json" \
-d '{ "reference": "ORD-10234", "serviceId": 1, "smartCardNumber": "7012345678",
"planCode": "10902", "planName": "DStv Compact", "amount": 15700 }'
The response is a purchase object. StarTimes is not available through the API yet.
/v1/transactions/{reference}The current status of one purchase, by your reference. Use it for PENDING purchases and after timeouts. Calling it never charges anything.
curl https://<partner-api-address>/v1/transactions/ORD-10233 -H "X-Api-Key: $VATPAY_API_KEY"
Returns the purchase object, or 404 NOT_FOUND if we never received that reference. If you get 404 after a timeout, it is safe to send the purchase again with the same reference.
/v1/transactionsYour purchases, newest first. Use it to reconcile against your own records.
| Field | Type | Required | Description |
|---|---|---|---|
| from | date | No | First day, inclusive (yyyy-MM-dd) |
| to | date | No | Last day, inclusive (yyyy-MM-dd) |
| status | string | No | SUCCESS, PENDING or FAILED |
| page | number | No | Page number, from 1 (default 1) |
| pageSize | number | No | Items per page, 1 to 100 (default 50) |
curl "https://<partner-api-address>/v1/transactions?from=2026-10-01&to=2026-10-04&status=PENDING&page=1&pageSize=50" \ -H "X-Api-Key: $VATPAY_API_KEY"
{
"code": "OK",
"data": {
"page": 1,
"pageSize": 50,
"total": 2,
"items": [ { "reference": "ORD-10240", "status": "PENDING", ... }, ... ]
}
}
Errors
Every error uses the same envelope, with the error in code and an explanation in message.
| HTTP | code | Meaning | Charged? | What to do |
|---|---|---|---|---|
| 400 | INVALID_REQUEST | A field is missing or invalid (details in data) | No | Fix the request |
| 400 | UNKNOWN_SERVICE | The serviceId doesn't exist, is switched off, or is the wrong kind for this endpoint | No | Refresh /v1/services |
| 401 | INVALID_API_KEY | Key missing, wrong or revoked | No | Check the X-Api-Key header |
| 403 | VENDOR_NOT_ACTIVE | Your account is pending or suspended | No | Contact VatPay |
| 403 | IP_NOT_ALLOWED | Called from an address that isn't on your allowlist | No | Add the IP in the portal |
| 404 | NOT_FOUND | No purchase with this reference | No | Safe to send the purchase |
| 409 | DUPLICATE_REFERENCE | Reference already used for a different request, or the first one is still being processed | No (new request) | Use a new reference, or requery the old one |
| 422 | INSUFFICIENT_BALANCE | Your wallet can't cover the amount | No | Fund your wallet |
| 422 | INVALID_CUSTOMER | The meter or smartcard was not found | No | Check the number with the customer |
| 422 | PURCHASE_FAILED | Refused before charging, e.g. amount below the meter minimum | No | Read message |
| 429 | RATE_LIMITED | Too many requests per second | No | Wait 1 second (Retry-After) and retry |
| 503 | SERVICE_UNAVAILABLE | A provider lookup (plans or validation) is temporarily down | No | Retry shortly |
| 5xx | (none) | Unexpected error or timeout | Unknown | Requery the reference; don't assume it failed |
Limits and security
- Rate limit: 20 requests per second per account by default, across all your keys. Ask us if you need more.
- Amounts: ₦1 to ₦10,000,000 per request. Providers set their own minimums (e.g. electricity usually ₦1,000).
- Logging: we record every call (without your key) so we can investigate disputes quickly. Send us your
referenceand we can see exactly what you sent and what we replied. - Lost or leaked key: revoke it in the portal at once and create a new one. Revoking takes effect immediately.
Go-live checklist
- The key is stored on your server only, and the IP allowlist is set
- Every order gets a unique reference, saved before the API call
- Timeouts and 5xx errors trigger a requery, not a new purchase
- PENDING purchases are requeried on a schedule until they settle
- Electricity and cable customers are validated, and the name is confirmed
- Electricity tokens are shown to the customer and stored
- You watch your wallet balance (GET /v1/balance) and top up before it runs out
- One small real purchase of each kind you sell has succeeded
Services
Airtime, data, electricity and cable TV. Sign in to see the serviceIds and your commission on each.
Apply for an account