Build on Danmalama
The Danmalama API lets you sell airtime, data, electricity, and cable TV subscriptions directly from your own platform, using the same wallet, pricing, and provider connections that power the Danmalama app. Every endpoint below is documented against the live, production API — not a simplified or hypothetical version of it.
https://api.danmalama.com.ng/api
This API is account-based: you integrate as a Danmalama user, using your own wallet balance to fund purchases you make on behalf of your customers. There is no separate "merchant" account type — any Danmalama account can generate an API key and start integrating immediately.
Authentication
Every request is authenticated with an API key, sent in the X-API-Key header.
Getting a key
API keys are generated from inside the Danmalama app, not through this API — open the app, go to Settings → API Access, and tap Enable. The raw key is shown exactly once. If you lose it, generate a new one (this immediately invalidates the old one — there's no overlap period), or disable API access entirely from the same screen.
X-API-Key: dmk_live_3f9a1b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e
Keys are prefixed dmk_live_ followed by 48 hex characters (57
characters total). Danmalama stores only a one-way hash of your key — support cannot
look up or recover a lost key for you, and neither can you retrieve it again from the
app after the first time it's shown.
There is currently no way to scope a key to "purchases only." A key authenticates as your full Danmalama account — the same as being logged into the app — and can call any endpoint your account can reach, including managing your own API access, changing your PIN, and generating a wallet funding account. Treat it exactly like a password: never embed it in a mobile app, browser extension, or any client your end users can inspect. Keep it server-side only, in your own backend.
Requests & Responses
Send request bodies as JSON with Content-Type: application/json. Every response, success or failure, is wrapped in the same envelope.
Success
{
"success": true,
"data": { ... }
}Error
{
"success": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Insufficient wallet balance.",
"details": null
}
}error.details is present only for some errors — most carry just a code and a human-readable message. See Errors for the one case where details has a fixed, parseable shape.
Errors
Every error uses one of these codes. The HTTP status is always derived from the code — never inspect the status alone without checking error.code, since 400 covers three distinct codes.
| Code | Status | Meaning |
|---|---|---|
VALIDATION_ERROR | 400 | Request body failed validation, or a business rule rejected it (e.g. amount below minimum). |
INSUFFICIENT_BALANCE | 400 | Your wallet balance is lower than the purchase amount. |
UNAUTHORIZED | 401 | Missing or invalid X-API-Key. |
FORBIDDEN | 403 | Your account is frozen, or you don't own the resource you're trying to access. |
NOT_FOUND | 404 | The resource (transaction, voucher, route) doesn't exist. |
CONFLICT | 409 | The request conflicts with existing state — a duplicate in-flight purchase, a voucher you already redeemed, a transaction already refunded. |
RATE_LIMITED | 429 | Reserved for account-level limits (see Rate Limits for how the general per-IP limit differs). |
PIN_LOCKED | 429 | Too many failed transaction PIN attempts — temporarily locked. |
PROVIDER_ERROR | 502 | The upstream network/disco/cable provider declined or errored. Your wallet is automatically refunded before this is returned. |
INTERNAL_ERROR | 500 | Unexpected server error. Safe to retry. |
A purchase can return 200 with the transaction's status field set to processing rather than successful — this happens when the upstream provider doesn't respond in time, not when it declines. Your wallet is still debited. Poll Get a transaction until the status resolves to successful, failed, or refunded. A prompt decline is never a 200 — it comes back as PROVIDER_ERROR with the debit already reversed.
When a request body fails schema validation (missing field, wrong type, out-of-range value), error.details has this specific shape:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request",
"details": {
"formErrors": [],
"fieldErrors": {
"phone": ["Enter an 11-digit phone number starting with 0"],
"amount": ["Number must be greater than 0"]
}
}
}
}Rate Limits
Two limits apply, tracked separately:
The stricter 15/minute limit applies to every purchase endpoint (airtime, data, electricity, cable), meter/smartcard validation, refund requests, and every voucher endpoint. Everything else (listing plans, discos, providers, your account details, transaction history) uses the general 120/minute limit.
Unlike every other error on this API, a rate-limit rejection is a plain text body —
Too many requests, please try again later. — not the
{success,error} envelope. Detect it by status code, not by
parsing the body as JSON. Standard rate-limit headers are included on every response:
RateLimit-Limit: 120 RateLimit-Policy: 120;w=60 RateLimit-Remaining: 0 RateLimit-Reset: 27 Retry-After: 27
Returns your account details, including your current wallet and bonus balance. There is no separate "check balance" endpoint — use this one, or read the balance field returned by any purchase (see below).
curl https://api.danmalama.com.ng/api/auth/me \ -H "X-API-Key: dmk_live_your_key_here"
const res = await fetch("https://api.danmalama.com.ng/api/auth/me", {
headers: { "X-API-Key": process.env.DANMALAMA_API_KEY }
});
const { data } = await res.json();
console.log(data.user.walletBalance);Response
{
"success": true,
"data": {
"user": {
"id": "68f1a2b3c4d5e6f7a8b9c0d1",
"name": "Adiahn Danmalama",
"email": "you@example.com",
"phone": "08031234567",
"avatarUrl": null,
"walletBalance": 15230,
"bonusBalance": 0,
"gafiaBankName": "Wema Bank",
"gafiaAccountNumber": "8123456789",
"role": "customer",
"hasPin": true,
"profileComplete": true,
"referralCode": "DAN-4F2A",
"referralEarnings": 350,
"createdAt": "2026-01-14T09:22:11.000Z"
}
}
}Airtime
Direct top-ups for MTN, Airtel, Glo, and 9mobile.
Buys airtime and credits it directly to the recipient's phone number.
| Field | Type | Description | |
|---|---|---|---|
network | string | required | One of MTN, AIRTEL, GLO, 9MOBILE. |
phone | string | required | 11-digit Nigerian number starting with 0, e.g. 08031234567. |
amount | integer | required | Naira amount, whole number, greater than 0. |
pin | string | required | Your account's 4-digit transaction PIN. |
bypassNetworkCheck | boolean | optional | Default false. The API rejects a number whose prefix doesn't match the given network (likely the wrong network selected) unless this is true — useful for genuinely ported numbers. |
Airtime purchases are capped at ₦5,000 per hour per account, summed across successful purchases in the trailing 60 minutes. Exceeding it returns VALIDATION_ERROR with the remaining amount in the message.
curl -X POST https://api.danmalama.com.ng/api/vending/airtime \
-H "X-API-Key: dmk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"network": "MTN",
"phone": "08031234567",
"amount": 500,
"pin": "1234"
}'const res = await fetch("https://api.danmalama.com.ng/api/vending/airtime", {
method: "POST",
headers: {
"X-API-Key": process.env.DANMALAMA_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
network: "MTN",
phone: "08031234567",
amount: 500,
pin: "1234"
})
});
const { data } = await res.json();Response
{
"success": true,
"data": {
"transaction": {
"_id": "68f9a1b2c3d4e5f6a7b8c9d0",
"reference": "AIRTIME-3F9A1B2C4D5E",
"type": "airtime",
"provider": "MTN",
"phone": "08031234567",
"amount": 500,
"walletDeducted": 500,
"bonusDeducted": 0,
"status": "successful",
"verificationAttempts": 0,
"createdAt": "2026-09-30T10:12:03.000Z",
"updatedAt": "2026-09-30T10:12:05.000Z"
},
"balance": 14730
}
}Data
Data bundles across all four networks. Plans and prices are account-specific — always fetch the live list rather than hardcoding plan IDs.
Lists every data plan you can buy, with pricing already resolved for your account (including any discount you've been assigned). planId is opaque — pass it straight through to Buy data, don't parse it.
curl https://api.danmalama.com.ng/api/vending/data/plans \ -H "X-API-Key: dmk_live_your_key_here"
Response
{
"success": true,
"data": [
{ "planId": "173", "network": "MTN", "label": "1GB - 30 Days", "price": 550 },
{ "planId": "174", "network": "MTN", "label": "2GB - 30 Days", "price": 900 },
{ "planId": "410", "network": "AIRTEL", "label": "5GB Weekly Plan - 7 Days", "price": 1650 }
]
}Buys a data plan and delivers it to the recipient's phone number.
| Field | Type | Description | |
|---|---|---|---|
network | string | required | One of MTN, AIRTEL, GLO, 9MOBILE — must match the plan's network. |
phone | string | required | 11-digit Nigerian number starting with 0. |
planId | string | required | From List data plans. |
pin | string | required | Your account's 4-digit transaction PIN. |
bypassNetworkCheck | boolean | optional | Default false. Same network/phone-prefix check as airtime. |
curl -X POST https://api.danmalama.com.ng/api/vending/data \
-H "X-API-Key: dmk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"network": "MTN",
"phone": "08031234567",
"planId": "173",
"pin": "1234"
}'const res = await fetch("https://api.danmalama.com.ng/api/vending/data", {
method: "POST",
headers: {
"X-API-Key": process.env.DANMALAMA_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
network: "MTN",
phone: "08031234567",
planId: "173",
pin: "1234"
})
});
const { data } = await res.json();Response
{
"success": true,
"data": {
"transaction": {
"_id": "68f9a1b2c3d4e5f6a7b8c9d1",
"reference": "DATA-BD029DE81705",
"type": "data",
"provider": "MTN",
"planId": "173",
"phone": "08031234567",
"amount": 550,
"walletDeducted": 550,
"bonusDeducted": 0,
"status": "successful",
"verificationAttempts": 0,
"createdAt": "2026-09-30T10:14:00.000Z",
"updatedAt": "2026-09-30T10:14:02.000Z"
},
"balance": 14180
}
}Electricity
Prepaid and postpaid electricity across every major Nigerian disco. Always validate the meter before buying — the customer's resolved name from validation is required in the purchase call.
Lists every electricity distribution company (disco) you can buy for.
curl https://api.danmalama.com.ng/api/vending/electricity/discos \ -H "X-API-Key: dmk_live_your_key_here"
Response
{
"success": true,
"data": [
{ "discoId": "ikeja-electric", "name": "Ikeja Electric" },
{ "discoId": "eko-electric", "name": "Eko Electric (EKEDC)" },
{ "discoId": "abuja-electric", "name": "Abuja Electric (AEDC)" }
]
}Treat discoId values as opaque and always fetched live — they're illustrative above, not a fixed contract.
Resolves the customer name on a meter number before you charge anyone. This does not debit your wallet.
| Field | Type | Description | |
|---|---|---|---|
discoId | string | required | From List discos. |
meterNumber | string | required | |
meterType | string | required | prepaid or postpaid. |
amount | integer | required | The amount you intend to purchase — some discos validate differently per amount. |
curl -X POST https://api.danmalama.com.ng/api/vending/electricity/validate-meter \
-H "X-API-Key: dmk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"discoId": "ikeja-electric",
"meterNumber": "04123456789",
"meterType": "prepaid",
"amount": 5000
}'Response
{
"success": true,
"data": {
"customerName": "ADEBAYO O. FASHOLA",
"address": "12 Allen Avenue, Ikeja, Lagos"
}
}Buys electricity credit. customerName must be the value returned by Validate meter — don't ask your own users to type it in.
| Field | Type | Description | |
|---|---|---|---|
discoId | string | required | |
meterNumber | string | required | |
meterType | string | required | prepaid or postpaid. |
amount | integer | required | Minimum ₦1,000. |
customerName | string | required | From Validate meter's response. |
pin | string | required | Your account's 4-digit transaction PIN. |
curl -X POST https://api.danmalama.com.ng/api/vending/electricity \
-H "X-API-Key: dmk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"discoId": "ikeja-electric",
"meterNumber": "04123456789",
"meterType": "prepaid",
"amount": 5000,
"customerName": "ADEBAYO O. FASHOLA",
"pin": "1234"
}'Response
{
"success": true,
"data": {
"transaction": {
"_id": "68f9a1b2c3d4e5f6a7b8c9d2",
"reference": "ELECTRICITY-9C1D2E3F4A5B",
"type": "electricity",
"provider": "ikeja-electric",
"phone": "04123456789",
"amount": 5000,
"walletDeducted": 5000,
"bonusDeducted": 0,
"status": "successful",
"providerResponse": { "token": "1234-5678-9012-3456-7890" },
"verificationAttempts": 0,
"createdAt": "2026-09-30T10:20:11.000Z",
"updatedAt": "2026-09-30T10:20:14.000Z"
},
"balance": 9180
}
}The prepaid token (when applicable) is inside providerResponse — its exact shape depends on the disco.
Cable TV
DStv, GOtv, StarTimes, and Showmax subscriptions — new plan changes or straight renewals.
Lists every cable provider and their available plans.
curl https://api.danmalama.com.ng/api/vending/cable/providers \ -H "X-API-Key: dmk_live_your_key_here"
Response
{
"success": true,
"data": [
{
"billerId": "dstv",
"name": "DStv",
"plans": [
{ "itemId": "dstv-compact", "label": "DStv Compact", "amount": 19000 },
{ "itemId": "dstv-premium", "label": "DStv Premium", "amount": 44500 }
]
},
{
"billerId": "gotv",
"name": "GOtv",
"plans": [
{ "itemId": "gotv-max", "label": "GOtv Max", "amount": 8500 }
]
}
]
}Resolves the customer name on a smartcard/IUC number. If the smartcard already has an active subscription, renewalAmount is returned too — use it for a renew purchase instead of guessing the plan price.
| Field | Type | Description | |
|---|---|---|---|
billerId | string | required | From List providers. |
itemId | string | optional | A specific plan, if known. |
smartcardNumber | string | required |
curl -X POST https://api.danmalama.com.ng/api/vending/cable/validate-smartcard \
-H "X-API-Key: dmk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"billerId": "dstv",
"smartcardNumber": "1234567890"
}'Response
{
"success": true,
"data": {
"customerName": "CHIOMA N. OKEKE",
"renewalAmount": 19000
}
}Buys or renews a cable subscription. There's no amount field — the price is always resolved server-side from the catalog (change) or the freshly re-validated renewal amount (renew), so a client can never supply a price.
| Field | Type | Description | |
|---|---|---|---|
billerId | string | required | |
itemId | string | optional | Required when subscriptionType is change. |
smartcardNumber | string | required | |
customerName | string | required | From Validate smartcard. |
subscriptionType | string | optional | change (default) or renew. |
pin | string | required | Your account's 4-digit transaction PIN. |
curl -X POST https://api.danmalama.com.ng/api/vending/cable \
-H "X-API-Key: dmk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"billerId": "dstv",
"itemId": "dstv-compact",
"smartcardNumber": "1234567890",
"customerName": "CHIOMA N. OKEKE",
"subscriptionType": "change",
"pin": "1234"
}'Response
{
"success": true,
"data": {
"transaction": {
"_id": "68f9a1b2c3d4e5f6a7b8c9d3",
"reference": "CABLE-1A2B3C4D5E6F",
"type": "cable",
"provider": "dstv",
"planId": "dstv-compact",
"phone": "1234567890",
"amount": 19000,
"walletDeducted": 19000,
"bonusDeducted": 0,
"status": "successful",
"verificationAttempts": 0,
"createdAt": "2026-09-30T10:25:40.000Z",
"updatedAt": "2026-09-30T10:25:42.000Z"
},
"balance": 30180
}
}Transactions
Every purchase you make — airtime, data, electricity, cable, and voucher activity — lands here as a single unified record.
Lists your transactions, newest first, capped at 500. Filtering and pagination aren't available yet — filter client-side, or fetch a single transaction by id below once you have a reference to look up.
curl https://api.danmalama.com.ng/api/vending/history \ -H "X-API-Key: dmk_live_your_key_here"
Response
{
"success": true,
"data": [
{
"_id": "68f9a1b2c3d4e5f6a7b8c9d1",
"reference": "DATA-BD029DE81705",
"type": "data",
"provider": "MTN",
"planId": "173",
"phone": "08031234567",
"amount": 550,
"status": "successful",
"createdAt": "2026-09-30T10:14:00.000Z"
}
]
}Fetches one transaction by its MongoDB id (not its reference string). Use this to poll a processing purchase until it resolves.
Returns 404 NOT_FOUND both when the id doesn't exist and when it belongs to a different account — this is deliberate, so a bad request never confirms whether someone else's transaction id exists.
curl https://api.danmalama.com.ng/api/vending/history/68f9a1b2c3d4e5f6a7b8c9d1 \ -H "X-API-Key: dmk_live_your_key_here"
Response
{
"success": true,
"data": {
"_id": "68f9a1b2c3d4e5f6a7b8c9d1",
"reference": "DATA-BD029DE81705",
"type": "data",
"provider": "MTN",
"planId": "173",
"phone": "08031234567",
"amount": 550,
"walletDeducted": 550,
"bonusDeducted": 0,
"status": "successful",
"verificationAttempts": 0,
"createdAt": "2026-09-30T10:14:00.000Z",
"updatedAt": "2026-09-30T10:14:02.000Z"
}
}Flags a transaction for manual admin review. This does not refund anything itself — it's a request, reviewed by a human.
| Field | Type | Description | |
|---|---|---|---|
transactionId | string | required | |
userNote | string | optional | Up to 500 characters, explaining what went wrong. |
curl -X POST https://api.danmalama.com.ng/api/vending/request-refund \
-H "X-API-Key: dmk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"transactionId": "68f9a1b2c3d4e5f6a7b8c9d1",
"userNote": "Customer says data never landed on their line."
}'Response
{ "success": true, "data": { "ok": true } }| Code | Status | When |
|---|---|---|
NOT_FOUND | 404 | Transaction doesn't exist. |
FORBIDDEN | 403 | The transaction belongs to a different account. |
CONFLICT | 409 | Already refunded. |
Vouchers
Pre-paid, shareable codes redeemable for wallet credit, airtime, data, electricity, or
cable — useful for gifting or batch-distributing value without handing out your API
key. You pay for the full batch (amountPerRedemption × maxRedemptions)
up front when you create one.
Creates a voucher and debits its full cost from your wallet immediately.
| Field | Type | Description | |
|---|---|---|---|
type | string | required | wallet, airtime, data, electricity, or cable. |
provider | string | required* | Network / discoId / billerId — required for every type except wallet. |
planId | string | optional | Required for data and cable types. |
amount | number | optional | Required for wallet, airtime, and electricity types — ignored for data/cable, whose price comes from the plan. |
maxRedemptions | integer | required | 1–1000. How many times this code can be redeemed in total. |
expiresAt | string | required | ISO date, must be in the future. |
message | string | optional | Up to 200 characters, shown to whoever looks up the code. |
code | string | optional | 4–24 chars, letters/numbers/dashes. Auto-generated (DMV-XXXXXX) if omitted. |
pin | string | required | Your account's 4-digit transaction PIN. |
curl -X POST https://api.danmalama.com.ng/api/vouchers \
-H "X-API-Key: dmk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"type": "data",
"provider": "MTN",
"planId": "173",
"maxRedemptions": 50,
"expiresAt": "2026-12-31T23:59:59.000Z",
"message": "Happy Sallah from Danmalama!",
"pin": "1234"
}'Response
{
"success": true,
"data": {
"voucher": {
"id": "68fa1b2c3d4e5f6a7b8c9d10",
"code": "DMV-7K3M9Q",
"type": "data",
"provider": "MTN",
"planId": "173",
"amountPerRedemption": 550,
"maxRedemptions": 50,
"totalCost": 27500,
"message": "Happy Sallah from Danmalama!",
"expiresAt": "2026-12-31T23:59:59.000Z",
"createdAt": "2026-09-30T10:30:00.000Z",
"redeemedCount": 0,
"remainingRedemptions": 50
},
"balance": 42500
}
}Lists every voucher you've created, newest first.
curl https://api.danmalama.com.ng/api/vouchers \ -H "X-API-Key: dmk_live_your_key_here"
Previews a voucher before redeeming it — never reveals who created it or who else has redeemed it.
curl https://api.danmalama.com.ng/api/vouchers/lookup/DMV-7K3M9Q \ -H "X-API-Key: dmk_live_your_key_here"
Response
{
"success": true,
"data": {
"code": "DMV-7K3M9Q",
"type": "data",
"providerLabel": "1GB - 30 Days",
"amountPerRedemption": 550,
"message": "Happy Sallah from Danmalama!",
"expiresAt": "2026-12-31T23:59:59.000Z",
"expired": false,
"fullyRedeemed": false,
"alreadyRedeemedByYou": false
}
}Redeems a voucher. Behavior depends on whether you pass reference (the recipient's phone number, meter number, or smartcard number):
- With
reference: delivers immediately and returns the outcome. - Without it: claims your one redemption slot now, delivered later via Deliver a claimed voucher. A
wallet-type voucher always delivers immediately —referencedoesn't apply to it.
| Field | Type | Description | |
|---|---|---|---|
code | string | required | |
reference | string | optional | Phone/meter/smartcard number to deliver to. Omit to claim now, deliver later. |
meterType | string | optional | prepaid or postpaid — required when delivering an electricity voucher. |
pin | string | required | Your account's 4-digit transaction PIN. |
curl -X POST https://api.danmalama.com.ng/api/vouchers/redeem \
-H "X-API-Key: dmk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"code": "DMV-7K3M9Q",
"reference": "08031234567",
"pin": "1234"
}'Response
{ "success": true, "data": { "ok": true, "type": "data" } }Deferred claim (no reference) returns { "ok": true, "type": "data", "deferred": true }. A wallet voucher returns { "ok": true, "type": "wallet", "balance": 15730 } — your new balance, since it credits you directly.
| Code | Status | When |
|---|---|---|
NOT_FOUND | 404 | Code doesn't exist. |
VALIDATION_ERROR | 400 | Expired, or fully redeemed. |
CONFLICT | 409 | You've already redeemed this code — one redemption per account. |
PROVIDER_ERROR | 502 | Delivery failed. Your redemption slot is not consumed — retry with corrected details. |
Delivers a voucher you previously claimed without a reference. No PIN needed here — you already proved intent when you claimed it.
| Field | Type | Description | |
|---|---|---|---|
reference | string | required | Phone/meter/smartcard number to deliver to. |
meterType | string | optional | Required for an electricity voucher. |
curl -X POST https://api.danmalama.com.ng/api/vouchers/DMV-7K3M9Q/use \
-H "X-API-Key: dmk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "reference": "08031234567" }'Response
{ "success": true, "data": { "ok": true, "type": "data" } }Lists vouchers you've redeemed (as opposed to List my vouchers, which lists ones you created).
curl https://api.danmalama.com.ng/api/vouchers/redeemed \ -H "X-API-Key: dmk_live_your_key_here"
Response
{
"success": true,
"data": [
{
"code": "DMV-7K3M9Q",
"type": "data",
"providerLabel": "1GB - 30 Days",
"amountPerRedemption": 550,
"message": "Happy Sallah from Danmalama!",
"redeemedAt": "2026-09-30T11:02:00.000Z",
"delivered": true
}
]
}Wallet
Fund your account balance by bank transfer.
Generates a dedicated bank account number for funding your wallet by transfer. Any transfer into it credits your wallet automatically (minus a small bank charge) — usually within a minute or two.
| Field | Type | Description | |
|---|---|---|---|
bvn | string | optional* | 11 digits. Only required the first time — if you already have a funding account, this returns it and ignores bvn. |
curl -X POST https://api.danmalama.com.ng/api/wallet/virtual-account \
-H "X-API-Key: dmk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "bvn": "12345678901" }'Response
{
"success": true,
"data": {
"accountNumber": "8123456789",
"bankName": "Wema Bank",
"accountName": "DANMALAMA - ADIAHN DANMALAMA"
}
}Ready to integrate?
Enable API access from Settings in the Danmalama app to get your key.
Danmalama API