Skip to content
Developer API · v1

Build on Esystem Data

Sell data, airtime, electricity, cable TV and exam PINs from your own website, bot or app. The API uses the VTU request format most Nigerian platforms already speak, so migrating is usually a base-URL change.

Get an API key https://idata.ng/api/
On this page

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.

API key auth
One header on every request
Safe retries
Idempotency keys prevent double charges
Signed webhooks
HMAC-SHA256 on every event

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.idata.ng/api/).

Keep keys server-side
Anyone with your key can spend your wallet. If a key leaks, revoke it in the console and create a new one.

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:

successful

Delivered. Done.

failed

Rejected by the network — your wallet was refunded.

processing

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" }
GET/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://idata.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" } }
}
GET/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://idata.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": [ … ]
}
POST/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

planrequired
integerPlan id from GET /api/network/
mobile_numberrequired
stringRecipient, e.g. 08031234567
network
integerOptional. When sent it must be the plan's network.
Ported_number
booleantrue skips the prefix check for ported numbers
request_id
stringIdempotency key (alternative to the Idempotency-Key header), ≤ 100 chars
curl -X POST "https://idata.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
}
POST/api/topup/

Buy airtime

VTU airtime at your discounted rate. amount is the airtime value; paid_amount is what your wallet was charged.

Body

networkrequired
integerNetwork id
amountrequired
numberAirtime value in naira, within the network's min_airtime–max_airtime
mobile_numberrequired
stringRecipient phone number
Ported_number
booleanSkip the network prefix check
airtime_type
string"VTU"
curl -X POST "https://idata.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."
}
GET/api/validateiuc

Validate smartcard

Checks a DStv / GOtv / StarTimes smartcard (IUC) number and returns the customer's name. Free.

Query

smart_card_numberrequired
stringSmartcard / IUC number
cablenamerequired
integerCable id (1 GOtv · 2 DStv · 3 StarTimes)
curl "https://idata.ng/api/validateiuc?smart_card_number=7012345678&cablename=1" \
  -H "Authorization: Token $ESD_API_KEY"
{ "invalid": false, "name": "OKAFOR AMAKA" }
POST/api/cablesub/

Cable TV subscription

Renews a DStv, GOtv or StarTimes bouquet. Plan ids come from Cableplan in GET /api/user/.

Body

cableplanrequired
integerCable plan id
smart_card_numberrequired
stringSmartcard / IUC number
cablename
integerCable id (optional)
customer_name
stringName from validation (optional)
curl -X POST "https://idata.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"
}
GET/api/validatemeter

Validate meter

Checks a meter number and returns the customer's name and address before you sell. Free.

Query

meternumberrequired
stringMeter number
disconamerequired
integerDisco id (from Disco in GET /api/user/)
mtyperequired
integer1 prepaid · 2 postpaid
curl "https://idata.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" }
POST/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

disco_namerequired
integerDisco id
amountrequired
numberAmount in naira, within Electricity.min–max
meter_numberrequired
stringMeter number
MeterTyperequired
integer1 prepaid · 2 postpaid ("PREPAID"/"POSTPAID" also accepted)
phone
stringCustomer phone (optional)
customer_name
stringName from validation (optional)
curl -X POST "https://idata.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"
}
POST/api/epin/

Exam PINs

Buys 1–5 result checker PINs. The PINs (and serials, where issued) are returned in pins.

Body

exam_namerequired
string"WAEC", "NECO", "NABTEB" — or the exam's id from GET /api/user/
quantityrequired
integer1 to 5
curl -X POST "https://idata.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"
}
GET/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://idata.ng/api/data/" \
  -H "Authorization: Token $ESD_API_KEY"
{
  "count": 23,
  "next": "https://idata.ng/api/data/?page=2",
  "previous": null,
  "results": [ { "id": "ESD26093041BAD6C88A", "Status": "successful", … } ]
}
GET/api/data/ESD26093041BAD6C88A

Get 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://idata.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.

VALIDATION400A field is missing or invalid — per-field lists are included.
INVALID_AMOUNT400Amount is outside the network's or disco's min–max.
INVALID_JSON400The body could not be parsed.
UNAUTHENTICATED401No credential was sent.
INVALID_API_KEY401The key is wrong or has been revoked.
SESSION_EXPIRED401A session token (not a key) has expired.
INSUFFICIENT_FUNDS402Your wallet balance is too low. Nothing was charged.
ACCOUNT_BLOCKED403The account that owns the key is blocked.
STORE_SUSPENDED403Your store is suspended.
FORBIDDEN403The credential cannot use this endpoint.
PLAN_NOT_FOUND404Unknown plan id (`{"plan": ["…"]}`).
TRANSACTION_NOT_FOUND404No such transaction on your account for that service.
NOT_FOUND404No such endpoint or record.
METHOD_NOT_ALLOWED405Wrong HTTP method for this path.
READ_ONLY_MIRROR405Write refused on a read-only mirror path.
PLAN_UNAVAILABLE409The plan is temporarily disabled.
IDEMPOTENCY_CONFLICT409The idempotency key was already used for a different kind of purchase.
PLAN_NOT_MAPPED422Compatible paths only: that provider id has no matching plan.
RATE_LIMITED429Too many requests — slow down and retry later.
PROVIDER_UNAVAILABLE503Validation could not reach any provider. Nothing was charged.
INTERNAL500Unexpected error. Retry with the same idempotency key.

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"
  }
}
Verify the signature

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.

Husmodata-compatible
https://idata.ng/functions/v1/husmodata/api/
Husmodata plan / network / cable / disco ids
Geodnatech-compatible
https://idata.ng/functions/v1/geodnatechsub/api/
Geodnatech Sub plan / network / cable / disco ids
Flutterwave-compatible bills
https://idata.ng/functions/v1/flutterwave/v3/
Flutterwave biller and item codes, {status, message, data} shapes

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://idata.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://idata.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"}'
Create a reseller account Then create a key under Developers.