Introduction
The API is a plain JSON-over-HTTPS REST API. Every purchase is charged to the wallet of the account that owns the API key, at your reseller price. Fund that wallet from your console before going live.
Authentication
Create a key in Reseller console → Developers (up to 5 active keys). Keys look like esd_live_… and are shown once — store yours in an environment variable, never in client-side code. Send it in the Authorization header:
Authorization: Token esd_live_4f9c2b7e1a…Authorization: Bearer <key> and X-Api-Key: <key> work too. You buy at your account's price (resellers pay the platform price) and no transaction PIN is needed — the key is the credential. Bodies may be JSON, form-encoded or multipart; a field missing from the body is also read from the query string. Trailing slashes are optional, and your store's own address works as a base URL too (https://yourstore.data.esys.ng/api/).
Idempotency & retries
Networks and gateways time out. To make retries safe, send a unique Idempotency-Key header (or a request-id / request_id body field, up to 100 characters) with every purchase. Repeating a request with the same key returns the original transaction with the same 201 body and the header X-Idempotent-Replay: true — you are not charged again. Reusing a key for a different kind of purchase answers 409 IDEMPOTENCY_CONFLICT.
Responses & statuses
Every purchase answers 201 as soon as a transaction exists — even if it later fails — so always read Status:
Delivered. Done.
Rejected by the network — your wallet was refunded.
Being confirmed with the network. Poll GET /api/<service>/<id> or wait for the webhook — don't retry with a new key.
Every record has id (the ESD… reference), ident (the transaction uuid), Status, api_response (a sentence you can show your customer), balance_before / balance_after (strings — the debit on this transaction; a refund is a separate wallet entry) and create_date (ISO-8601, UTC). Money fields are strings such as "268.0". Response headers: X-Transaction-Id (= id) and X-Wallet-Balance (your wallet after this request, after any refund).
Errors use a 4xx/5xx status, create no transaction and charge nothing:
{ "error": "Insufficient wallet balance. Fund your wallet and try again.", "code": "INSUFFICIENT_FUNDS" }Validation errors add per-field lists:
{ "mobile_number": ["Enter a valid 11-digit Nigerian phone number."],
"network": ["The plan does not belong to this network."],
"error": "mobile_number: Enter a valid 11-digit Nigerian phone number.", "code": "VALIDATION" }/api/user/Account & catalogue
Your account, wallet balance and the whole catalogue priced for you: data plans per network (grouped by plan type), cable plans, airtime top-up rates, exam PIN prices, networks with airtime limits, discos and the electricity fee. topuppercentage.VTU is what you pay per ₦100 of airtime.
No parameters.
curl "https://data.esys.ng/api/user/" \
-H "Authorization: Token $ESD_API_KEY"{
"user": {
"id": "a111572d-…", "email": "you@example.com", "username": "you",
"FullName": "Amaka Okafor", "Phone": "08031111111", "user_type": "API",
"role": "reseller", "store": "yourstore",
"Account_Balance": 20000, "wallet_balance": "20000.0", "bonus_balance": "0.0"
},
"Dataplans": {
"MTN_PLAN": { "ALL": [ … ], "SME": [ … ], "GIFTING": [ … ], "CORPORATE_GIFTING": [ … ] },
"GLO_PLAN": { … }, "AIRTEL_PLAN": { … }, "9MOBILE_PLAN": { … }
},
"Cableplan": {
"GOTVPLAN": [{ "id": 1674, "cableplan_id": "1674", "cablename": 1, "cable": "GOTV",
"package": "GOtv Smallie - Monthly", "plan_amount": "1900.0" }],
"DSTVPLAN": [ … ], "STARTIMEPLAN": [ … ],
"cablename": [{ "id": 1, "name": "GOTV" }, { "id": 2, "name": "DSTV" }, { "id": 3, "name": "STARTIMES" }]
},
"topuppercentage": { "MTN": { "VTU": 98 }, "GLO": { "VTU": 97 }, "9MOBILE": { "VTU": 98 }, "AIRTEL": { "VTU": 98 } },
"Exam": { "WAEC": { "id": 1704, "amount": 5223 }, "NECO": { "id": 1705, "amount": 2246 } },
"Networks": [{ "id": 1, "name": "MTN", "min_airtime": 50, "max_airtime": 50000 }, …],
"Disco": [{ "id": 3, "name": "Abuja Electric", "code": "AEDC" }, …],
"Electricity": { "fee": 20, "min": 1000, "max": 200000, "meter_types": { "1": "PREPAID", "2": "POSTPAID" } }
}/api/network/Data plans
Every active data plan grouped by network, priced for you. Use a plan's id as plan when buying data.
No parameters.
curl "https://data.esys.ng/api/network/" \
-H "Authorization: Token $ESD_API_KEY"{
"MTN_PLAN": [
{ "id": 1424, "dataplan_id": "1424", "network": 1, "plan_type": "AWOOF GIFTING",
"plan_network": "MTN", "month_validate": "1 day", "plan": "1GB", "plan_amount": "268.0" }
],
"GLO_PLAN": [ … ], "AIRTEL_PLAN": [ … ], "9MOBILE_PLAN": [ … ]
}/api/data/Buy data
Buys a data plan for a phone number. Network ids: 1 MTN · 2 Glo · 3 9mobile · 4 Airtel (see Networks in GET /api/user/).
Body
true skips the prefix check for ported numbersIdempotency-Key header), ≤ 100 charscurl -X POST "https://data.esys.ng/api/data/" \
-H "Authorization: Token $ESD_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"network":1,"mobile_number":"08031234567","plan":1424,"Ported_number":true}'{
"id": "ESD26093041BAD6C88A",
"ident": "4b9d6ee4-b91c-4bf4-82b2-0b354b08c985",
"network": 1,
"mobile_number": "08031234567",
"plan": 1424,
"plan_network": "MTN",
"plan_name": "1GB",
"plan_type": "AWOOF GIFTING",
"plan_amount": "268.0",
"Status": "successful",
"api_response": "You have successfully purchased MTN 1GB AWOOF GIFTING (1 day) for 08031234567.",
"balance_before": "20000.0",
"balance_after": "19732.0",
"create_date": "2026-09-30T09:53:19.273Z",
"Ported_number": true
}/api/topup/Buy airtime
VTU airtime at your discounted rate. amount is the airtime value; paid_amount is what your wallet was charged.
Body
min_airtime–max_airtimecurl -X POST "https://data.esys.ng/api/topup/" \
-H "Authorization: Token $ESD_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"network":1,"amount":100,"mobile_number":"08031234567","Ported_number":true,"airtime_type":"VTU"}'{
"id": "ESD26093029465219B6", "ident": "21933207-a463-4a70-b187-ed8c31f9b363",
"airtime_type": "VTU", "network": 1, "mobile_number": "08031234567",
"amount": "100.0", "paid_amount": "98.0", "plan_amount": "98.0", "plan_network": "MTN",
"balance_before": "19464.0", "balance_after": "19366.0",
"Status": "successful", "create_date": "2026-09-30T09:53:28.641Z", "Ported_number": true,
"api_response": "You have successfully purchased MTN airtime ₦100 for 08031234567."
}/api/validateiucValidate smartcard
Checks a DStv / GOtv / StarTimes smartcard (IUC) number and returns the customer's name. Free.
Query
curl "https://data.esys.ng/api/validateiuc?smart_card_number=7012345678&cablename=1" \
-H "Authorization: Token $ESD_API_KEY"{ "invalid": false, "name": "OKAFOR AMAKA" }/api/cablesub/Cable TV subscription
Renews a DStv, GOtv or StarTimes bouquet. Plan ids come from Cableplan in GET /api/user/.
Body
curl -X POST "https://data.esys.ng/api/cablesub/" \
-H "Authorization: Token $ESD_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"cablename":1,"cableplan":1674,"smart_card_number":"7012345678"}'{
"id": "ESD2609307C4E1D2B90", "ident": "0f5c…",
"cablename": 1, "cable_name": "GOtv", "cableplan": 1674, "plan_name": "GOtv Smallie - Monthly",
"smart_card_number": "7012345678", "customer_name": null,
"plan_amount": "1900.0", "paid_amount": "1900.0",
"balance_before": "16446.0", "balance_after": "14546.0",
"Status": "successful", "api_response": "…", "create_date": "2026-09-30T09:54:02.118Z"
}/api/validatemeterValidate meter
Checks a meter number and returns the customer's name and address before you sell. Free.
Query
Disco in GET /api/user/)curl "https://data.esys.ng/api/validatemeter?meternumber=45012345678&disconame=1&mtype=1" \
-H "Authorization: Token $ESD_API_KEY"{ "invalid": false, "name": "OKAFOR AMAKA", "address": "12 Allen Avenue, Ikeja" }/api/billpayment/Buy electricity
Buys a prepaid token or pays a postpaid bill. You are charged amount + the electricity fee (paid_amount). Prepaid tokens are returned in token.
Body
Electricity.min–maxcurl -X POST "https://data.esys.ng/api/billpayment/" \
-H "Authorization: Token $ESD_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"disco_name":1,"amount":1000,"meter_number":"45012345678","MeterType":1}'{
"id": "ESD2609305A1F45BEAB", "ident": "067e4fc5-fe87-439b-b37c-a3423f8bdb9c",
"disco_name": 1, "disco": "Ikeja Electric", "amount": "1000.0", "paid_amount": "1020.0",
"meter_number": "45012345678", "MeterType": 1, "meter_type_name": "PREPAID",
"token": "1763-9841-5423-2867-6443", "units": "42.5 kWh", "customer_name": "OKAFOR AMAKA",
"balance_before": "17466.0", "balance_after": "16446.0",
"Status": "successful", "api_response": "…", "create_date": "2026-09-30T09:53:31.592Z"
}/api/epin/Exam PINs
Buys 1–5 result checker PINs. The PINs (and serials, where issued) are returned in pins.
Body
curl -X POST "https://data.esys.ng/api/epin/" \
-H "Authorization: Token $ESD_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"exam_name":"WAEC","quantity":1}'{
"id": "ESD26093088D1A2B3C4", "ident": "9a1f…",
"exam_name": "WAEC", "quantity": 1, "amount": "5223.0", "paid_amount": "5223.0",
"pins": [{ "pin": "906620591321", "serial": "WRN306938270" }],
"balance_before": "…", "balance_after": "…",
"Status": "successful", "api_response": "…", "create_date": "2026-09-30T09:55:10.004Z"
}/api/data/Transaction history
Your own transactions of one service, newest first, 20 per page (?page=N). Works the same for /api/topup/, /api/cablesub/, /api/billpayment/ and /api/epin/. next/previous are absolute URLs or null.
No parameters.
curl "https://data.esys.ng/api/data/" \
-H "Authorization: Token $ESD_API_KEY"{
"count": 23,
"next": "https://data.esys.ng/api/data/?page=2",
"previous": null,
"results": [ { "id": "ESD26093041BAD6C88A", "Status": "successful", … } ]
}/api/data/ESD26093041BAD6C88AGet one transaction
One record by id (the ESD… reference) or ident (uuid) — use it to poll a processing sale. Same for every service path. Another account's transaction, or one of another service, is 404.
No parameters.
curl "https://data.esys.ng/api/data/ESD26093041BAD6C88A" \
-H "Authorization: Token $ESD_API_KEY"{ "id": "ESD26093041BAD6C88A", "ident": "4b9d6ee4-…", "Status": "successful", … }Error codes
An error status means no transaction was created and nothing was charged. Once a transaction exists you always get 201 and a Status.
Webhooks
Set a webhook URL in Reseller console → Developers. When a sale on your store — including API sales — reaches a final status we POST the event to it: transaction.successful or transaction.failed (a processing sale sends one of these when it settles). The X-Esysdata-Event header names the event. Answer 2xx within 8 seconds. data.reference is the API's id and data.id its ident; make your handler idempotent.
{
"event": "transaction.successful",
"data": {
"id": "4b9d6ee4-b91c-4bf4-82b2-0b354b08c985",
"reference": "ESD26093041BAD6C88A",
"service": "data", "status": "successful", "refunded": false,
"description": "MTN 1GB AWOOF GIFTING (1 day)", "recipient": "08031234567",
"amount": 268, "channel": "api",
"created_at": "2026-09-30T09:53:19.273Z", "completed_at": "2026-09-30T09:53:21.004Z"
}
}Every request carries X-Esysdata-Signature: the hex HMAC-SHA256 of the raw request body, keyed with your store's webhook signing secret — shown under Reseller console → Developers. Compute it over the exact bytes you received and compare in constant time. Reject anything that doesn't match.
Use Send test event on the Developers page to try your endpoint — it sends the event test with "test": true in the body.
import crypto from "node:crypto";
// Express: use express.raw({ type: "application/json" }) so you verify the exact bytes.
app.post("/webhooks/esysdata", express.raw({ type: "application/json" }), (req, res) => {
const expected = crypto
.createHmac("sha256", process.env.ESD_WEBHOOK_SECRET)
.update(req.body) // raw Buffer
.digest("hex");
const given = req.get("X-Esysdata-Signature") ?? "";
const ok = given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
if (!ok) return res.status(401).end();
const { event, data } = JSON.parse(req.body.toString("utf8"));
// Update your order by data.reference; make this idempotent.
res.sendStatus(200);
});Migrating from another provider
The developer API above speaks our own plan, network, cable and disco ids and picks the best provider with automatic fail-over — use it for new integrations. If your software is hard-wired to another provider's ids, point it at one of these compatible base URLs instead: they accept that provider's own ids and shapes, bill your Esystem wallet at your price and show every sale in your dashboard. Use the same API key.
https://data.esys.ng/functions/v1/husmodata/api/https://data.esys.ng/functions/v1/geodnatechsub/api/https://data.esys.ng/functions/v1/flutterwave/v3/Husmodata / Geodnatech ids
Exactly the endpoints and shapes documented above (/user/, /network/, /data/, /topup/, /cablesub/, /billpayment/, /epin/, history and validation) — but every id is that provider's: send its plan id and you get it back in plan. The sale is fulfilled by that provider only (no fail-over). /network/ and /user/ list only the plans that provider sells, keyed by its ids and priced for you; user is your own account. An id we can't match answers 422 PLAN_NOT_MAPPED, and a plan the provider can't sell at or below the platform price comes back failed (refunded).
curl -X POST "https://data.esys.ng/functions/v1/husmodata/api/data/" \
-H "Authorization: Token $ESD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"network": 1, "mobile_number": "08031234567", "plan": 482}'
# 201 {"id": "ESD26093009A9CA5957", …, "plan": 482, "plan_name": "1GB", "Status": "successful", …}{ "plan": ["Plan 465 is not available through this mirror."], "error": "Plan 465 is not available through this mirror.", "code": "PLAN_NOT_MAPPED" }Flutterwave bills
Flutterwave's own paths and {status, message, data} shapes, authenticated with your Esystem key (never a Flutterwave key). Reference reads are cached and limited to 60 requests a minute: GET banks/:country, POST accounts/resolve, GET bill-categories, GET top-bill-categories, GET bills/:category/billers, GET billers, GET billers/:biller_code/items, GET bill-items/:item_code/validate.
POST billers/:biller_code/items/:item_code/payment with {country: "NG", customer_id, amount, reference} is billed from your wallet: airtime items sell airtime of amount; data and cable items sell the matching plan at your price (amount is ignored); electricity items sell amount of power. reference is the idempotency key. A failed (refunded) payment answers 400 with status: "error". Check a payment with GET bills/:reference. Payouts, charges, balances and other writes are refused (405 READ_ONLY_MIRROR) — fund and withdraw from your console.
curl -X POST "https://data.esys.ng/functions/v1/flutterwave/v3/billers/BIL108/items/MD494/payment" \
-H "Authorization: Token $ESD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"country": "NG", "customer_id": "+2348031234567", "amount": 800, "reference": "my-ref-001"}'