meter.ng
Developer API
Build electricity payments into any product. Verify meters, purchase tokens, and automate recharges for every DISCO in Nigeria โ with one API.
The flow
Every electricity purchase follows three steps. Verify once, purchase forever.
All amounts are in kobo. โฆ1,000 = 100000 kobo. This eliminates floating-point errors on financial amounts.
Base URL
https://api.meter.ng/api/v1
Quick start
# 1 โ Request OTP curl -X POST https://api.meter.ng/api/v1/auth/request-otp \ -H "Content-Type: application/json" \ -d '{"phone":"+2348012345678"}' # 2 โ Verify OTP โ get accessToken curl -X POST https://api.meter.ng/api/v1/auth/verify-otp \ -H "Content-Type: application/json" \ -d '{"phone":"+2348012345678","otp":"123456"}' # 3 โ Verify a meter curl -X POST https://api.meter.ng/api/v1/meters/verify \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"meterNumber":"04177243013","disco":"AEDC"}' # 4 โ Purchase electricity curl -X POST https://api.meter.ng/api/v1/tokens/purchase \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "meterNumber": "04177243013", "disco": "AEDC", "amount": 100000, "paymentMethod": "WALLET", "paymentReference": "ref-001", "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000", "accessToken": "TOKEN_FROM_VERIFY_STEP" }'
Always use idempotency keys (UUID v4) on purchases. If your request times out and you retry with the same key, you get the original transaction back โ never a duplicate charge.
Authentication
meter.ng uses phone number + OTP. No passwords. Every session returns a short-lived accessToken (15 minutes) and a long-lived refreshToken (30 days).
Using tokens
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Lifecycle
Never store accessTokens in localStorage. Use secure in-memory storage or httpOnly cookies. AccessTokens can authorize real money transfers.
Auth Endpoints
Body
| Field | Type | Description | |
|---|---|---|---|
| phone | string | required | E.164 format โ +2348012345678 |
{
"success": true,
"message": "Verification code sent to +234****78",
"expiresIn": 600
}
Body
| Field | Type | Description | |
|---|---|---|---|
| phone | string | required | Same phone used in request-otp |
| otp | string | required | 6-digit code from SMS |
| name | string | optional | Required for new registrations |
{
"success": true,
"isNewUser": false,
"user": {
"id": "0a43fcfb-6d4a-4126-a4b6-9113402db798",
"phone": "+2348012345678",
"name": "Ada Okafor",
"role": "CUSTOMER"
},
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Body
| Field | Type | Description | |
|---|---|---|---|
| refreshToken | string | required | The refresh token from verify-otp |
Meter Endpoints
Register a meter once โ your customer can recharge forever with a single call. Meter details come live from the DISCO at registration time.
accessToken required for purchase.The accessToken returned is a DISCO-issued validation reference. It expires within minutes. Pass it immediately to /tokens/purchase.
Body
| Field | Type | Description | |
|---|---|---|---|
| meterNumber | string | required | 11โ13 digit meter number |
| disco | string | required | DISCO code โ see DISCO Reference |
{
"success": true,
"meter": {
"meterNumber": "04177243013",
"customerName": "OKAFOR ADA CHIDINMA",
"address": "NO 5 GIMBIYA ST AREA 11 GARKI ABUJA",
"tariff": "NON-MD",
"minimumAmount": "900",
"accessToken": "20260911001862199779",
"disco": "AEDC"
}
}
Body
| Field | Type | Description | |
|---|---|---|---|
| meterNumber | string | required | 11โ13 digit meter number |
| disco | string | required | DISCO code |
| accessToken | string | required | From the verify step โ do not generate yourself |
| customerName | string | optional | From verify response |
| address | string | optional | From verify response |
| minimumAmount | number | optional | In naira, from verify response |
| nickname | string | optional | "Home", "Office", "Mum's place" |
{
"success": true,
"meters": [{
"id": "cb74be0e-f40f-41d3-bd3d-d2b66255dbe5",
"meterNumber": "04177243013",
"disco": "AEDC",
"customerName": "OKAFOR ADA CHIDINMA",
"nickname": "Home",
"autoPowerEnabled": true,
"autoPowerAmountKobo": "500000",
"minimumPurchaseKobo": "90000"
}]
}
Token Endpoints
Purchase electricity tokens and track their delivery. The failsafe vend engine tries multiple providers in ranked order until one succeeds.
Use a fresh UUID v4 as idempotencyKey on every purchase. Retrying with the same key returns the original transaction โ never a duplicate charge.
Body
| Field | Type | Description | |
|---|---|---|---|
| meterNumber | string | required | 11โ13 digit meter number |
| disco | string | required | DISCO code |
| amount | number | required | In kobo. Min 50000 (โฆ500) ยท Max 10000000 (โฆ100k) |
| paymentMethod | string | required | WALLET ยท CARD ยท VIRTUAL_ACCOUNT |
| paymentReference | string | required | Your Paystack reference, or any string for WALLET |
| idempotencyKey | string | required | UUID v4. Generate fresh per purchase. Never reuse. |
| accessToken | string | required | From the verify step โ expires quickly. Verify and purchase in the same flow. |
Token delivered immediately
{
"success": true,
"transactionId": "ac6f30e7-dfa1-4368-a436-23de33b14c28",
"token": "6311-2411-2060-2008-2022",
"units": "117.6",
"message": "Token sent to your phone."
}
Still processing (202)
{
"success": true,
"transactionId": "ac6f30e7-dfa1-4368-a436-23de33b14c28",
"message": "Payment received. Token arrives shortly via SMS."
}
On 202, poll GET /tokens/:id every 5 seconds โ or use the token.delivered webhook instead. Webhooks are more efficient.
{
"success": true,
"transaction": {
"id": "ac6f30e7-dfa1-4368-a436-23de33b14c28",
"status": "COMPLETED",
"amountPaid": "100000",
"disco": "AEDC",
"meterNumber": "04177243013",
"token": "6311-2411-2060-2008-2022",
"units": "117.6",
"tokenDeliveredAt": "2026-09-11T00:27:04.000Z"
}
}
Transaction statuses
| PAYMENT_CONFIRMED | Payment received, vending in progress |
| VENDING | Active request sent to provider |
| VENDING_PENDING | First attempt failed, retrying with next provider |
| COMPLETED | Token generated and delivered by SMS โ |
| FAILED | All providers failed โ refund initiated |
| REQUERY_REQUIRED | Manual review needed โ contact support |
page, limit (max 100).Wallet
Every account has a NGN wallet backed by a dedicated Wema Bank virtual account. Fund it by bank transfer โ credits automatically within 30 seconds.
GET /wallet until status is ready.Body
| Field | Type | Description | |
|---|---|---|---|
| string | required | Valid email โ used by Paystack for the virtual account |
{
"success": true,
"wallet": {
"balanceKobo": "500000",
"status": "ready",
"accountNumber": "9816907903",
"bankName": "Wema Bank",
"accountName": "WWWMETERNG/ADA OKAFOR"
}
}
Transfer any amount to the account number from any Nigerian bank. A Paystack DVA fee of 1% (capped at โฆ300) applies per transfer.
AutoPower
After 3โ4 purchases, meter.ng calculates the customer's electricity burn rate and automatically recharges the meter at 75% depletion โ before they ever run out.
Body
| Field | Type | Description | |
|---|---|---|---|
| enabled | boolean | required | true to enable, false to disable |
| amountKobo | number | optional | Auto-recharge amount. Default: 500000 (โฆ5,000) |
Merchants
Merchants are community agents who purchase electricity on behalf of customers, earn commission per transaction, and act as guarantors for Borrow Power loans.
Body
| Field | Type | Description | |
|---|---|---|---|
| businessName | string | required | Trading or business name |
| businessAddress | string | required | Physical location |
| nin | string | required | 11-digit National ID Number |
| referralCode | string | optional | Code from the merchant who recruited you |
{
"success": true,
"merchant": {
"tier": "SILVER",
"vendsThisMonth": 87,
"walletBalanceKobo": "565500",
"commissionRate": 6500,
"referralCode": "ABCD123456",
"nextTier": { "tier": "GOLD", "vendsNeeded": 113 }
}
}
Commission tiers
| Tier | Monthly Vends | Commission |
|---|---|---|
| Bronze | 0โ50 | โฆ50 per vend |
| Silver | 51โ200 | โฆ65 per vend |
| Gold | 200+ | โฆ80 per vend |
| Sub-agent bonus | โ | +โฆ20 per vend from agents you recruited |
Body
| Field | Type | Description | |
|---|---|---|---|
| meterNumber | string | required | Customer's meter number |
| disco | string | required | DISCO code |
| amountKobo | number | required | Amount in kobo. Min 90000 (โฆ900) |
| accessToken | string | required | From verify step |
| idempotencyKey | string | required | UUID v4 |
| customerPhone | string | optional | Send token to customer's phone instead of yours |
DISCO Reference
Use these exact codes. Case-sensitive.
Minimum purchase amounts vary per meter. Always use the minimumAmount from the verify endpoint โ never hardcode amounts.
Errors
All errors follow one shape:
{
"success": false,
"error": "ERROR_CODE",
"message": "Human-readable description"
}
Error codes
| Code | HTTP | Meaning |
|---|---|---|
| VALIDATION_ERROR | 400 | Request body invalid |
| OTP_INVALID | 400 | Wrong OTP |
| OTP_EXPIRED | 400 | OTP expired โ request a new one |
| BELOW_DISCO_MINIMUM | 400 | Amount below DISCO minimum for this meter |
| METER_NOT_VERIFIED | 400 | accessToken missing โ verify meter first |
| UNAUTHORIZED | 401 | Missing or invalid Authorization header |
| FORBIDDEN | 403 | Insufficient role for this endpoint |
| NOT_A_MERCHANT | 403 | Merchant-only endpoint |
| MERCHANT_NOT_APPROVED | 403 | KYC pending or rejected |
| METER_NOT_FOUND | 404 | Meter not in DISCO database |
| TRANSACTION_NOT_FOUND | 404 | Transaction not found or not owned by this user |
| METER_ALREADY_REGISTERED | 409 | Meter already on this account |
| EMAIL_IN_USE | 409 | Email linked to a different account |
| INSUFFICIENT_WALLET_BALANCE | 402 | Wallet too low โ top up first |
| INSUFFICIENT_MERCHANT_BALANCE | 402 | Merchant wallet too low for this vend |
| SERVICE_UNAVAILABLE | 503 | All electricity providers temporarily unavailable |
Webhooks
Register a webhook URL to receive real-time events. More efficient than polling โ especially for token delivery.
Security
Every delivery includes X-Meterng-Signature. Verify it before processing:
// Verify webhook signature (Node.js) const crypto = require('crypto'); function verifyWebhook(rawBody, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(rawBody) // must be the raw Buffer, not parsed JSON .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); }
Return 200 OK within 5 seconds even if processing asynchronously. We retry failed deliveries up to 5 times with exponential backoff.
Events
{
"event": "token.delivered",
"data": {
"transactionId": "ac6f30e7-dfa1-4368-a436-23de33b14c28",
"meterNumber": "04177243013",
"disco": "AEDC",
"token": "6311-2411-2060-2008-2022",
"units": "117.6",
"amountKobo": "300000",
"deliveredAt": "2026-09-11T00:27:04.000Z"
}
}
{
"event": "wallet.credited",
"data": {
"userId": "0a43fcfb-6d4a-4126-a4b6-9113402db798",
"amountKobo": "500000",
"newBalanceKobo": "1000000",
"creditedAt": "2026-09-11T02:49:52.000Z"
}
}
{
"event": "autopower.fired",
"data": {
"meterId": "cb74be0e-f40f-41d3-bd3d-d2b66255dbe5",
"meterNumber": "04177243013",
"amountKobo": "500000",
"transactionId": "550e8400-e29b-41d4-a716-446655440000",
"firedAt": "2026-09-11T06:00:00.000Z"
}
}