Quick start
- Get your API key from your account's API settings. Your account must be active.
- Call
catalogto get current IDs and your account's prices. - Verify the customer for electricity and cable.
- Request a quote, then purchase with a unique reference and the exact quoted total.
- Save the reference and check status until the order reaches a final state.
Endpoint: POST /api/v2/. Content type: application/json. Amounts are in NGN. Send monetary values as decimal strings where possible.
Authentication
Authorization: Bearer YOUR_API_KEY Content-Type: application/json
Authorization: Token YOUR_API_KEY is also accepted. Use API keys on your server only. Never expose them in a browser, mobile app, screenshots or logs. API authorization replaces the customer PIN for server-to-server purchases; do not send a wallet PIN.
Existing user, agent and vendor accounts keep their configured pricing. Data, cable and recharge pricing is tier-specific; airtime charges the requested face value, matching the app; education uses its configured product price and electricity uses its configured charge policy.
Payment flow
{"service":"education","action":"catalog"}The response contains products with code, name and price. Do not guess IDs.
{"service":"education","action":"quote",
"product_code":"neco","quantity":1}Use the actual product code returned by catalog, then copy quote.total into the purchase request:
{"service":"education","action":"buy",
"product_code":"neco","quantity":1,
"expected_total":"2250.00",
"reference":"EX_20261016_unique_order_0001"}The price and product above are examples, not a current price promise. Buy, quote and verification fields must agree.
Service reference
| Service | Actions | Purchase fields |
|---|---|---|
data | catalog, quote, buy, status, history | network (name), type, plan_id, phone, expected_total, reference |
airtime | catalog, quote, buy, status, history | network (name), airtime_type (VTU/SHARE), amount (whole naira), phone, expected_total, reference |
cable | catalog, verify, quote, buy, status, history | provider_code, iuc_number, subtype (renew/change), plan_code, phone, verification_token, expected_total, reference |
electricity | catalog, verify, quote, buy, status, history | verification_id, amount, phone, expected_total, reference |
education | catalog, quote, buy, status, history | product_code, quantity, expected_total, reference |
recharge | catalog, quote, buy, status, history | plan_id, quantity, name_on_card, expected_total, reference |
Verify electricity
{"service":"electricity","action":"verify",
"service_id":"CODE_FROM_CATALOG","meter_type":"prepaid",
"meter_number":"YOUR_METER_NUMBER"}Save verification.id. Verification expires after 10 minutes. Quotes include electricity value and service charges.
Verify cable
{"service":"cable","action":"verify",
"provider_code":"CODE_FROM_CATALOG","subtype":"renew",
"iuc_number":"YOUR_SMARTCARD_NUMBER"}Save verification.token. It expires after 15 minutes. Select a plan matching provider and subtype.
Recharge cards
Catalog returns plans[].id, denomination, network and price. Total is plan price multiplied by quantity. The server checks this total again before charging. Quantity is 1–100. Successful receipts include cards with PIN and serial. Keep PINs as strings to preserve leading zeros.
References
Use a unique reference of 8–80 letters, digits, underscores or hyphens, for example my_order_000001. It must be unique across your services. Retry only the identical purchase body with the same reference. A different body or service is rejected with HTTP 409.
Status and safe retries
{"service":"education","action":"status",
"reference":"EX_20261016_unique_order_0001"}success:true means the request was processed; it does not by itself mean the purchase succeeded. Read order.state: pending, successful or failed. All six v2 services use this order format. Pending returns HTTP 202; settled payments return HTTP 200.
On timeout or HTTP 5xx, check status with the same reference. Do not submit a new reference to recover an uncertain payment. A missing lookup result does not prove that an in-flight request failed. Retry lookups with backoff (for example 10, 30, then 60 seconds), and contact support with the reference if uncertainty remains.
Successful education returns order.pins; electricity returns token, units and any KCT codes. A successful payment can have delayed PIN/token delivery. Query status again instead of buying twice. Never expose one customer's receipt to another customer.
History takes page, starting at 1, with 20 items per page and has_more. It lists your v2 requests for the selected service only. Query status for the full PIN/token receipt. Website, app and v1 orders remain in their existing histories.
Data and airtime examples
Use network names MTN, AIRTEL, GLO or 9MOBILE exactly as returned by catalog. Do not guess plan IDs. Examples below are templates; replace placeholders with catalog/quote values.
Data catalog and quote
{"service":"data","action":"catalog"}Read plans[]: plan_id, name, network, type, validity_days and price. Keep network, type and plan together.
{"service":"data","action":"quote","network":"MTN",
"type":"SME","plan_id":"PLAN_ID_FROM_CATALOG"}Buy data
{"service":"data","action":"buy","network":"MTN","type":"SME",
"plan_id":"PLAN_ID_FROM_CATALOG","phone":"08012345678",
"expected_total":"QUOTE_TOTAL","reference":"my_data_order_0001"}Airtime catalog and quote
{"service":"airtime","action":"catalog"}Catalog returns networks and available types (VTU and/or SHARE). Amount must be whole naira within configured limits.
{"service":"airtime","action":"quote","network":"MTN",
"airtime_type":"VTU","amount":"100"}Buy airtime
{"service":"airtime","action":"buy","network":"MTN",
"airtime_type":"VTU","amount":"100","phone":"08012345678",
"expected_total":"100.00","reference":"my_airtime_order_0001"}Wallet balance
{"service":"account","action":"balance"}Returns balance, currency (NGN) and tier: 1 subscriber, 2 agent, 3 vendor.
Quotes for the other services
Verify cable/electricity first using the requests above. Confirm the customer identity before payment. Then request a quote:
{"service":"cable","action":"quote","provider_code":"CATALOG_CODE",
"iuc_number":"SMARTCARD_NUMBER","subtype":"renew","plan_code":"PLAN_CODE",
"phone":"08012345678","verification_token":"VERIFICATION_TOKEN"}{"service":"electricity","action":"quote","verification_id":"VERIFICATION_ID",
"amount":"1000","phone":"08012345678"}{"service":"recharge","action":"quote","plan_id":1,"quantity":10}To buy, send the same fields with action:"buy", a new reference, and expected_total copied from quote.total. Recharge also requires name_on_card (up to 100 bytes). Select enabled plans from the catalog. All phone numbers must be valid 11-digit Nigerian numbers. Exam quantity must not exceed catalog.maximum_quantity.
{"service":"recharge","action":"buy","plan_id":1,"quantity":10,
"name_on_card":"My Store","expected_total":"QUOTE_TOTAL",
"reference":"my_cards_order_0001"}Webhook callbacks and IP restrictions
Open API settings to set your HTTPS webhook URL (port 443). Copy the signing secret securely when it is generated. Leave the URL blank to disable callbacks. API keys, PINs and tokens are not included in callbacks; fetch the authenticated status receipt when needed.
You may restrict API access to up to 20 exact public IPv4/IPv6 addresses separated by commas. Blank allows any source with a valid key. Checks use the direct connection IP. Behind a CDN/proxy, ask the administrator to configure trusted source handling before enabling restrictions; arbitrary forwarded headers are not trusted.
{"event_id":"payment.123.0","event":"payment.successful",
"order":{"reference":"my_data_order_0001","service":"data",
"state":"successful","total":"250.00","created_at":"2026-10-19 10:30:00"}}Headers: X-API-Event-Id, X-API-Timestamp (Unix seconds), X-API-Signature. The signature is hexadecimal HMAC-SHA256 of timestamp + "." + raw_body, using the webhook signing secret.
<?php
$raw = file_get_contents('php://input');
$stamp = $_SERVER['HTTP_X_API_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_API_SIGNATURE'] ?? '';
$secret = getenv('PAYMENT_WEBHOOK_SECRET');
if (!$secret || !ctype_digit($stamp) || abs(time() - (int)$stamp) > 300 ||
!hash_equals(hash_hmac('sha256', $stamp.'.'.$raw, $secret), $signature)) {
http_response_code(401); exit;
}
$event = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
// Add your durable database processing here:
// In ONE transaction, insert event_id with a UNIQUE constraint,
// apply a new event to its owned order, then commit.
// Do not change a final order back to pending.
// Return 2xx for events already processed.
http_response_code(204);This is signature verification, not a complete receiver. Add durable event storage before using it. Match reference, amount and service to your own order. Query authenticated status when an event conflicts with your records.
The administrator must run the callback worker cron. Events reflect observed states; intermediate pending states may be skipped. Delivery may repeat or arrive out of order. Acknowledge with 2xx after durable processing. Failed deliveries retry with increasing delay, up to 12 attempts. Keep status polling as a fallback. Private-network destinations and redirects are not supported.
Server-side examples
cURL: read the education catalog
curl 'https://YOUR_DOMAIN/api/v2/' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
--data '{"service":"education","action":"catalog"}'PHP
<?php
$ch = curl_init('https://YOUR_DOMAIN/api/v2/');
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 45,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer '.getenv('VTU_API_KEY'),
'Content-Type: application/json'
],
CURLOPT_POSTFIELDS => json_encode([
'service' => 'education', 'action' => 'catalog'
])
]);
$raw = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($raw === false || $http >= 500) {
throw new RuntimeException('Response unavailable');
}
$result = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
// Check result.success, then inspect order.state for purchases.These examples read a catalog; they do not initiate a live purchase.
Error handling
Responses may include success:false, message and a machine-readable code where available. Never use message text as your only transaction-state check.
| HTTP | Meaning / action |
|---|---|
| 400 / 422 | Review fields, IDs, reference or price. |
| 401 / 403 | Invalid authorization, inactive account or security restriction. |
| 404 | No recorded v2 request found. Check the original reference and any concurrent call. |
| 409 | Reference conflict, request in progress or price change. Read the response and retry safely. |
| 405 / 413 | Use POST; keep JSON under 16 KiB. |
| 429 / validation message | Too many requests. Back off; do not loop aggressively. |
| 5xx / timeout | Outcome may be unknown. Query status; do not assume failure. |
Existing API integrations
Existing v1 endpoint URLs and payloads remain available for current integrations. The six services documented here use the app's shared purchase engine under /api/v2/. V1 numeric plan IDs are not interchangeable with v2 catalog codes. Migrate deliberately using catalog, verification and status calls; do not change an existing integration's URL alone.
Airtime and data are now available through v2 as documented above. Your wallet is shared across versions. Never run the same order through both versions.