SmartVTU API
Sell data, airtime, cable TV, electricity and exam pins from your own website or app. You pay from your wallet at your level's prices (this page shows Standard prices).
Authentication
Create keys on your Developer page. Send the secret key as a Bearer token and the public key in X-API-Key. Keep the secret on your server only.
curl "/api/v1/balance" \
-H "Authorization: Bearer sk_live_xxxxxxxx" \
-H "X-API-Key: pk_live_xxxxxxxx"<?php
$ch = curl_init('/api/v1/balance');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer sk_live_xxxxxxxx',
'X-API-Key: pk_live_xxxxxxxx',
'Content-Type: application/json',
],
]);
$res = json_decode(curl_exec($ch), true);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);const res = await fetch('/api/v1/balance', {
headers: {
'Authorization': 'Bearer sk_live_xxxxxxxx',
'X-API-Key': 'pk_live_xxxxxxxx',
'Content-Type': 'application/json'
}
});
const data = await res.json();403 ip_not_allowed, and a key with too many rejected requests is paused automatically.Request signing optional
For extra safety, sign each request. When a key has "Require signed requests" on, unsigned calls are refused. Signed requests do not need the Bearer header.
| Field | Type | Description |
|---|---|---|
X-API-Key required | header | Your public key. |
X-Timestamp required | header | Current Unix time in seconds. Must be within 5 minutes of our clock. |
X-Signature required | header | Lowercase hex HMAC-SHA256 of timestamp + "\n" + METHOD + "\n" + path + "\n" + raw body, keyed with your secret. Path is the URL path, e.g. /api/v1/data. |
# Build the signature in your language, then:
curl -X POST "/api/v1/data" \
-H "X-API-Key: pk_live_xxxxxxxx" \
-H "X-Timestamp: 1767787200" \
-H "X-Signature: 5f2b...e9" \
-H "Content-Type: application/json" \
-d '{"network":"mtn","plan_id":12,"phone":"08012345678","reference":"order-1001"}'<?php
$body = json_encode(['network' => 'mtn', 'plan_id' => 12, 'phone' => '08012345678', 'reference' => 'order-1001']);
$ts = (string)time();
$path = '/api/v1/data';
$sig = hash_hmac('sha256', $ts . "\n" . 'POST' . "\n" . $path . "\n" . $body, 'sk_live_xxxxxxxx');
// send headers X-API-Key, X-Timestamp: $ts, X-Signature: $sig with $bodyconst crypto = require('crypto');
const body = JSON.stringify({ network: 'mtn', plan_id: 12, phone: '08012345678', reference: 'order-1001' });
const ts = Math.floor(Date.now() / 1000).toString();
const path = '/api/v1/data';
const sig = crypto.createHmac('sha256', 'sk_live_xxxxxxxx').update(ts + '\n' + 'POST' + '\n' + path + '\n' + body).digest('hex');
// send headers X-API-Key, X-Timestamp: ts, X-Signature: sig with bodyResponses
Every response is JSON with the same shape. Check the HTTP code first, then code.
{
"ok": true,
"code": "ok",
"message": "Human readable text",
"data": {
"...": "..."
}
} Status and error codes
| HTTP | code | Meaning |
|---|---|---|
| 200 | ok / successful | Done. |
| 202 | pending | Accepted and processing. Poll the status endpoint; it ends as successful or refunded. |
| 400 | failed / refunded | The order failed. Your wallet was refunded. |
| 400 | bad_json | The body is not valid JSON. |
| 401 | unauthorized / bad_signature | Missing or wrong key, or a bad signature. |
| 402 | insufficient_balance | Fund your wallet and retry with the same reference. |
| 403 | key_disabled / ip_not_allowed / no_api_access / blocked | The key is paused, the IP is not whitelisted, your level has no API access, or our safety checks stopped the order. |
| 404 | not_found | Unknown endpoint or reference. |
| 409 | possible_duplicate / in_progress | Same order seconds ago, or the same reference is still processing. |
| 422 | validation_error / invalid_number / verification_unavailable | Fix the fields named in message. |
| 429 | rate_limited | Slow down. Limit is per key per minute. |
| 503 | paused / service_unavailable / api_disabled | Temporarily unavailable. Retry later with the same reference. |
Idempotent references
Every purchase needs your own reference. If your request times out, send it again with the same reference: you get the original order back with "replayed": true, and nothing is charged twice.
GET /balance
Your wallet balance in naira.
{
"ok": true,
"code": "ok",
"message": "Wallet balance.",
"data": {
"balance": 15250.5,
"referral_balance": 1200,
"currency": "NGN",
"wallet_status": "active"
}
}GET /services
Which services are on, network and decoder codes, disco codes, exam codes and prices, airtime discount and limits for your account.
curl "/api/v1/services" \
-H "Authorization: Bearer sk_live_xxxxxxxx" \
-H "X-API-Key: pk_live_xxxxxxxx"<?php
$ch = curl_init('/api/v1/services');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer sk_live_xxxxxxxx',
'X-API-Key: pk_live_xxxxxxxx',
'Content-Type: application/json',
],
]);
$res = json_decode(curl_exec($ch), true);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);const res = await fetch('/api/v1/services', {
headers: {
'Authorization': 'Bearer sk_live_xxxxxxxx',
'X-API-Key': 'pk_live_xxxxxxxx',
'Content-Type': 'application/json'
}
});
const data = await res.json();GET /plans
Data or cable plans with your price. Use plan_id when buying.
| Field | Type | Description |
|---|---|---|
service | string | data (default) or cable. |
network | string | Data: mtn, airtel, glo, 9mobile. Cable: dstv, gotv, startimes. |
curl "/api/v1/plans?service=data&network=mtn" \
-H "Authorization: Bearer sk_live_xxxxxxxx" \
-H "X-API-Key: pk_live_xxxxxxxx"<?php
$ch = curl_init('/api/v1/plans?service=data&network=mtn');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer sk_live_xxxxxxxx',
'X-API-Key: pk_live_xxxxxxxx',
'Content-Type: application/json',
],
]);
$res = json_decode(curl_exec($ch), true);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);const res = await fetch('/api/v1/plans?service=data&network=mtn', {
headers: {
'Authorization': 'Bearer sk_live_xxxxxxxx',
'X-API-Key': 'pk_live_xxxxxxxx',
'Content-Type': 'application/json'
}
});
const data = await res.json();{
"ok": true,
"code": "ok",
"message": "1 plan(s).",
"data": {
"service": "data",
"plans": [
{
"plan_id": 12,
"name": "MTN SME 1GB",
"network": "mtn",
"data_type": "sme",
"size": "1GB",
"validity": "30 days",
"price": 255
}
]
}
}POST /airtime
| Field | Type | Description |
|---|---|---|
network required | string | mtn, airtel, glo, 9mobile |
phone required | string | 11-digit Nigerian number. |
amount required | integer | Airtime value in naira (50 to 50,000). |
ported | boolean | Set true when the number moved networks. |
reference required | string | Your own unique ID for this order (4 to 64 characters: letters, numbers, - _ .). Sending the same reference again never charges twice. |
allow_duplicate | boolean | Set true to buy the same thing for the same number again within a few minutes. |
curl -X POST "/api/v1/airtime" \
-H "Authorization: Bearer sk_live_xxxxxxxx" \
-H "X-API-Key: pk_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"network":"mtn","phone":"08012345678","amount":500,"reference":"order-1000"}'<?php
$ch = curl_init('/api/v1/airtime');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['network' => 'mtn','phone' => '08012345678','amount' => 500,'reference' => 'order-1000']),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer sk_live_xxxxxxxx',
'X-API-Key: pk_live_xxxxxxxx',
'Content-Type: application/json',
],
]);
$res = json_decode(curl_exec($ch), true);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);const res = await fetch('/api/v1/airtime', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_xxxxxxxx',
'X-API-Key': 'pk_live_xxxxxxxx',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"network": "mtn",
"phone": "08012345678",
"amount": 500,
"reference": "order-1000"
})
});
const data = await res.json();POST /data
| Field | Type | Description |
|---|---|---|
network required | string | mtn, airtel, glo, 9mobile |
plan_id required | integer | From live plan IDs or GET /plans. |
phone required | string | 11-digit Nigerian number. |
ported | boolean | Number moved networks. |
reference required | string | Your own unique ID for this order (4 to 64 characters: letters, numbers, - _ .). Sending the same reference again never charges twice. |
allow_duplicate | boolean | Set true to buy the same thing for the same number again within a few minutes. |
curl -X POST "/api/v1/data" \
-H "Authorization: Bearer sk_live_xxxxxxxx" \
-H "X-API-Key: pk_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"network":"mtn","plan_id":12,"phone":"08012345678","reference":"order-1001"}'<?php
$ch = curl_init('/api/v1/data');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['network' => 'mtn','plan_id' => 12,'phone' => '08012345678','reference' => 'order-1001']),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer sk_live_xxxxxxxx',
'X-API-Key: pk_live_xxxxxxxx',
'Content-Type: application/json',
],
]);
$res = json_decode(curl_exec($ch), true);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);const res = await fetch('/api/v1/data', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_xxxxxxxx',
'X-API-Key': 'pk_live_xxxxxxxx',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"network": "mtn",
"plan_id": 12,
"phone": "08012345678",
"reference": "order-1001"
})
});
const data = await res.json();{
"ok": true,
"code": "successful",
"message": "Order completed.",
"data": {
"reference": "TX24110712AB9F",
"client_reference": "order-1001",
"service": "data",
"network": "mtn",
"data_type": "sme",
"plan_id": 12,
"recipient": "08012345678",
"description": "MTN SME 1GB to 08012345678",
"amount": 255,
"face_value": 255,
"status": "successful",
"message": "",
"token": null,
"channel": "api",
"created_at": "2026-11-07 12:00:00",
"completed_at": "2026-11-07 12:00:03"
}
}POST /cable
| Field | Type | Description |
|---|---|---|
cable required | string | dstv, gotv, startimes |
plan_id required | integer | Package ID from GET /plans?service=cable. |
iuc required | string | Smartcard / IUC number. |
phone | string | Customer phone (optional). |
confirm_unverified | boolean | Buy even when the IUC cannot be verified right now. |
reference required | string | Your own unique ID for this order (4 to 64 characters: letters, numbers, - _ .). Sending the same reference again never charges twice. |
allow_duplicate | boolean | Set true to buy the same thing for the same number again within a few minutes. |
curl -X POST "/api/v1/cable" \
-H "Authorization: Bearer sk_live_xxxxxxxx" \
-H "X-API-Key: pk_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"cable":"gotv","plan_id":40,"iuc":"7012345678","reference":"order-1002"}'<?php
$ch = curl_init('/api/v1/cable');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['cable' => 'gotv','plan_id' => 40,'iuc' => '7012345678','reference' => 'order-1002']),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer sk_live_xxxxxxxx',
'X-API-Key: pk_live_xxxxxxxx',
'Content-Type: application/json',
],
]);
$res = json_decode(curl_exec($ch), true);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);const res = await fetch('/api/v1/cable', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_xxxxxxxx',
'X-API-Key': 'pk_live_xxxxxxxx',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"cable": "gotv",
"plan_id": 40,
"iuc": "7012345678",
"reference": "order-1002"
})
});
const data = await res.json();POST /electricity
| Field | Type | Description |
|---|---|---|
disco required | string | ikeja-electric, eko-electric, abuja-electric |
meter required | string | Meter number. |
meter_type | string | prepaid (default) or postpaid. |
amount required | integer | Units value in naira. |
phone | string | Customer phone (optional). |
confirm_unverified | boolean | Buy even when the meter cannot be verified right now. |
reference required | string | Your own unique ID for this order (4 to 64 characters: letters, numbers, - _ .). Sending the same reference again never charges twice. |
allow_duplicate | boolean | Set true to buy the same thing for the same number again within a few minutes. |
curl -X POST "/api/v1/electricity" \
-H "Authorization: Bearer sk_live_xxxxxxxx" \
-H "X-API-Key: pk_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"disco":"ikeja-electric","meter":"45012345678","meter_type":"prepaid","amount":5000,"reference":"order-1003"}'<?php
$ch = curl_init('/api/v1/electricity');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['disco' => 'ikeja-electric','meter' => '45012345678','meter_type' => 'prepaid','amount' => 5000,'reference' => 'order-1003']),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer sk_live_xxxxxxxx',
'X-API-Key: pk_live_xxxxxxxx',
'Content-Type: application/json',
],
]);
$res = json_decode(curl_exec($ch), true);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);const res = await fetch('/api/v1/electricity', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_xxxxxxxx',
'X-API-Key': 'pk_live_xxxxxxxx',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"disco": "ikeja-electric",
"meter": "45012345678",
"meter_type": "prepaid",
"amount": 5000,
"reference": "order-1003"
})
});
const data = await res.json();Prepaid tokens come back in data.token once the order is successful.
POST /exam
| Field | Type | Description |
|---|---|---|
exam required | string | waec, neco, nabteb |
quantity | integer | 1 to 5. |
reference required | string | Your own unique ID for this order (4 to 64 characters: letters, numbers, - _ .). Sending the same reference again never charges twice. |
allow_duplicate | boolean | Set true to buy the same thing for the same number again within a few minutes. |
curl -X POST "/api/v1/exam" \
-H "Authorization: Bearer sk_live_xxxxxxxx" \
-H "X-API-Key: pk_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"exam":"waec","quantity":1,"reference":"order-1004"}'<?php
$ch = curl_init('/api/v1/exam');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['exam' => 'waec','quantity' => 1,'reference' => 'order-1004']),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer sk_live_xxxxxxxx',
'X-API-Key: pk_live_xxxxxxxx',
'Content-Type: application/json',
],
]);
$res = json_decode(curl_exec($ch), true);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);const res = await fetch('/api/v1/exam', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_xxxxxxxx',
'X-API-Key': 'pk_live_xxxxxxxx',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"exam": "waec",
"quantity": 1,
"reference": "order-1004"
})
});
const data = await res.json();POST /verify/meter and /verify/smartcard
Check the customer name before you buy. Meter: disco, meter, meter_type. Smartcard: cable, iuc.
curl -X POST "/api/v1/verify/smartcard" \
-H "Authorization: Bearer sk_live_xxxxxxxx" \
-H "X-API-Key: pk_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"cable":"dstv","iuc":"7012345678"}'<?php
$ch = curl_init('/api/v1/verify/smartcard');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['cable' => 'dstv','iuc' => '7012345678']),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer sk_live_xxxxxxxx',
'X-API-Key: pk_live_xxxxxxxx',
'Content-Type: application/json',
],
]);
$res = json_decode(curl_exec($ch), true);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);const res = await fetch('/api/v1/verify/smartcard', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_xxxxxxxx',
'X-API-Key': 'pk_live_xxxxxxxx',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"cable": "dstv",
"iuc": "7012345678"
})
});
const data = await res.json();{
"ok": true,
"code": "ok",
"message": "Verified.",
"data": {
"customer_name": "ADA OBI"
}
}GET /transactions/{reference}
Use our reference or yours. Poll every 10 to 30 seconds while the status is pending.
curl "/api/v1/transactions/order-1001" \
-H "Authorization: Bearer sk_live_xxxxxxxx" \
-H "X-API-Key: pk_live_xxxxxxxx"<?php
$ch = curl_init('/api/v1/transactions/order-1001');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer sk_live_xxxxxxxx',
'X-API-Key: pk_live_xxxxxxxx',
'Content-Type: application/json',
],
]);
$res = json_decode(curl_exec($ch), true);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);const res = await fetch('/api/v1/transactions/order-1001', {
headers: {
'Authorization': 'Bearer sk_live_xxxxxxxx',
'X-API-Key': 'pk_live_xxxxxxxx',
'Content-Type': 'application/json'
}
});
const data = await res.json();GET /transactions
| Field | Type | Description |
|---|---|---|
status | string | successful, pending, failed or refunded. |
service | string | airtime, data, cable, electricity, exam, sms. |
channel | string | api to show only API orders. |
from / to | date | YYYY-MM-DD. |
page / limit | integer | Up to 100 per page. |
Live plan IDs
Updated automatically, prices for Standard.
Plans show here as soon as they are added.