VatPay for Business

API Guide

Everything you need to sell airtime, data, electricity and cable TV through the VatPay Partner API.

Try it in Swagger

Before you start

  1. Get approved. Apply for an account; we review it and set your commission.
  2. Fund your wallet. Transfer to the funding account shown on your dashboard. Every purchase is paid from this wallet.
  3. 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.
  4. Optional: add your servers' IP addresses to the allowlist so the key only works from them.
Base URLhttps://<partner-api-address>
ProtocolHTTPS, JSON bodies (Content-Type: application/json)
CurrencyNaira (NGN). Every amount is in naira, e.g. 1500 or 1500.50, never kobo.
TimesWest Africa Time (UTC+1), e.g. 2026-10-04T09:26:11
Test environmentThere 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 returns 403 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

HTTPdata.statusWhat happenedYour walletWhat to do
200SUCCESSDelivered. Electricity tokens are in data.token.Charged; commission creditedGive the customer their value.
200PENDINGThe provider hasn't confirmed yet.HeldTell 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.
200FAILEDThe provider refused it after the charge.Refunded automatically (refunded: true)Tell the customer it failed. Retry with a new reference if you want.
422FAILEDRefused before any charge, e.g. low balance or invalid meter.Not chargedRead code and fix the cause.

The purchase object (data) returned by every purchase and requery:

referencestringYour reference
transactionIdnumberVatPay's id for the purchase. Quote it to support. Absent if nothing was charged.
kindstringairtime, data, electricity or cable
servicestringe.g. MTN, IKEDC, DSTV
statusstringSUCCESS, PENDING or FAILED
messagestringWhat happened, in plain words
amountnumberThe purchase amount in naira
amountChargednumberWhat your wallet is currently paying for it: the amount, or 0 once it's refunded or if it was never charged
commissionnumberCommission you earned, credited to your commission balance (separate from your wallet)
refundedbooleantrue when a charged purchase failed and the money is back in your wallet
tokenstringElectricity token, on prepaid SUCCESS only
providerReferencestringThe provider's own reference, when they return one
createdAtdatetimeWhen we received the purchase
Your wallet is debited the full amount; your commission is credited to your commission balance on SUCCESS. If a purchase later fails, both are reversed.

Recommended flow

  1. Once a day: call GET /v1/services and cache the serviceIds. For data and cable, cache the plans too.
  2. Electricity and cable: validate the meter or smartcard first and show the customer name to the customer for confirmation.
  3. Save your order with a new reference before calling the API, so a crash can't lose it.
  4. Call the purchase endpoint with a 60-second timeout.
  5. Act on the result: SUCCESS → deliver; PENDING → requery on a schedule; FAILED → tell the customer. On a timeout, requery the same reference.
  6. Reconcile daily with GET /v1/transactions and look at anything still PENDING.

Endpoints

GET/v1/balance

Your 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" }
}
GET/v1/services

Every 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 }
  ]
}
GET/v1/services/{serviceId}/plans

Data bundles and cable TV bouquets for a data or cable service. Use a plan's planCode when buying.

  • amount is the plan's price in naira. Send exactly that as the purchase amount.
  • A plan with no amount has 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.

POST/v1/validate/meter

Look up an electricity meter before you sell to it, and show the customer the name it returns. Nothing is charged.

FieldTypeRequiredDescription
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.

POST/v1/validate/smartcard

Look up a DSTV or GOtv smartcard (IUC number) and get the customer name. Nothing is charged.

FieldTypeRequiredDescription
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" }
}
POST/v1/purchase/airtime

Top up any MTN, Airtel, Glo or 9mobile number.

FieldTypeRequiredDescription
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"
  }
}
POST/v1/purchase/data

Buy a data bundle. Get planCode and its price from the plans endpoint.

FieldTypeRequiredDescription
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.

POST/v1/purchase/electricity

Buy 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.

FieldTypeRequiredDescription
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"
  }
}
Always show the customer the 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.
POST/v1/purchase/cable

Pay a DSTV or GOtv subscription. Validate the smartcard first and confirm the name with the customer.

FieldTypeRequiredDescription
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.

GET/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.

GET/v1/transactions

Your purchases, newest first. Use it to reconcile against your own records.

FieldTypeRequiredDescription
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.

HTTPcodeMeaningCharged?What to do
400INVALID_REQUESTA field is missing or invalid (details in data)NoFix the request
400UNKNOWN_SERVICEThe serviceId doesn't exist, is switched off, or is the wrong kind for this endpointNoRefresh /v1/services
401INVALID_API_KEYKey missing, wrong or revokedNoCheck the X-Api-Key header
403VENDOR_NOT_ACTIVEYour account is pending or suspendedNoContact VatPay
403IP_NOT_ALLOWEDCalled from an address that isn't on your allowlistNoAdd the IP in the portal
404NOT_FOUNDNo purchase with this referenceNoSafe to send the purchase
409DUPLICATE_REFERENCEReference already used for a different request, or the first one is still being processedNo (new request)Use a new reference, or requery the old one
422INSUFFICIENT_BALANCEYour wallet can't cover the amountNoFund your wallet
422INVALID_CUSTOMERThe meter or smartcard was not foundNoCheck the number with the customer
422PURCHASE_FAILEDRefused before charging, e.g. amount below the meter minimumNoRead message
429RATE_LIMITEDToo many requests per secondNoWait 1 second (Retry-After) and retry
503SERVICE_UNAVAILABLEA provider lookup (plans or validation) is temporarily downNoRetry shortly
5xx(none)Unexpected error or timeoutUnknownRequery 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 reference and 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