Danmalama API
Get your API key →
Viewing as
Getting Started/Introduction
Danmalama API v1

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.

Base URL (native API)

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.

If your own system already integrates against another VTU reseller API, you don't have to rewrite it to reach Danmalama — two compatibility surfaces, MSORG and ADEX, accept those APIs' exact request/response shapes and auth conventions, internally calling the same purchase engine as the native API above. See the Compatibility APIs section before you start — there's one auth detail (a transaction PIN neither source API has a concept of) that's easy to miss and will 401 every request until you know about it.


Playground

Sandbox

Pick a shape and an endpoint, then send a real request straight from your browser to api.danmalama.com.ng. This isn't a mockup — it's a live call against the production API, using your own credentials. Purchase endpoints spend real wallet balance.

GET

Your key/token is used only for the request you send, never logged, and never sent anywhere but Danmalama's own API — kept in this tab's session storage at most, cleared when you close it.


Authentication

Every request to the native API is authenticated with an API key, sent in the X-API-Key header. (MSORG and ADEX use a different convention — see Compatibility APIs.)

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.

⚠ An API key has full account access

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. The same rule applies to the MSORG/ADEX tokens below — they're built directly from this key.

Requests & Responses

Send request bodies as JSON with Content-Type: application/json. Every native-API response, success or failure, is wrapped in the same envelope. (MSORG and ADEX use their own, different envelopes — documented in each compatibility section.)

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.

CodeStatusMeaning
VALIDATION_ERROR400Request body failed validation, or a business rule rejected it (e.g. amount below minimum).
INSUFFICIENT_BALANCE400Your wallet balance is lower than the purchase amount.
UNAUTHORIZED401Missing or invalid X-API-Key.
FORBIDDEN403Your account is frozen, or you don't own the resource you're trying to access.
NOT_FOUND404The resource (transaction, voucher, route) doesn't exist.
CONFLICT409The request conflicts with existing state — a duplicate in-flight purchase, a voucher you already redeemed, a transaction already refunded.
RATE_LIMITED429Reserved for account-level limits (see Rate Limits for how the general per-IP limit differs).
PIN_LOCKED429Too many failed transaction PIN attempts — temporarily locked.
PROVIDER_ERROR502The upstream network/disco/cable provider declined or errored. Your wallet is automatically refunded before this is returned.
INTERNAL_ERROR500Unexpected server error. Safe to retry.
A 200 response isn't always final

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. The same underlying behavior applies to purchases made through MSORG/ADEX — only the response envelope differs.

Validation error details

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 on the native API, tracked separately:

General requests — 120 / minute Purchases & sensitive actions — 15 / minute

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. The same 15/minute limiter also protects the equivalent MSORG and ADEX purchase/validation endpoints — it's shared infrastructure, not a separate allowance per entry point. ADEX's token-exchange endpoint (POST /compat/adex/api/user) uses a separate, stricter limiter: 20 requests per 15 minutes per IP, the same one that protects native login.

The 429 response is not JSON

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 (or MSORG/ADEX's {status,message,code} 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

Compatibility APIs

Two shim layers sit in front of the same purchase engine as the native API, shaped to match two competitor VTU reseller APIs exactly — so an already-integrated client can switch to Danmalama by changing only its base URL and token, no code changes. Both cover data, airtime, electricity, and cable only (plus each source's own account-check and transaction-lookup endpoints). Neither source API's result-checker/exam-PIN, bulk SMS, or printable recharge/data-card endpoints have a Danmalama equivalent, so those are deliberately not shimmed.

Compatibility mode

MSORG

MSORG matches the request/response field names, route paths, and auth header convention of a widely-used VTU reseller API. If you already have code talking to that API, point it at the base URL below with a Danmalama token and it should work with no other changes.

Base URL

https://api.danmalama.com.ng/compat/msorg

⚠ Your token must carry your transaction PIN — read this before your first call

The source API has no transaction-PIN concept, but every Danmalama purchase requires one regardless of entry point. So the PIN travels folded into the token itself:

Token = dmk_live_xxxxx:1234 (your API key, a colon, your 4-digit transaction PIN)

Sent as Authorization: Token dmk_live_xxxxx:1234 on every request. Build this once from Settings → API Access (for the key) and your own PIN, then paste the whole string wherever your existing code already sends its token. Forgetting the :1234 suffix — or sending just the raw API key, the way you would to the native API's X-API-Key header — is the single most common cause of a 401 here.

Network IDs

The source API's own documented requests use small numeric network ids. These are real, fixed conventions in the shipped code — MSORG accepts either form:

Numeric idNetworkName form (also accepted)
1MTNMTN
2AirtelAIRTEL
3GloGLO
49mobile9MOBILE

Meter type

ValueMeaning
prepaid or 1Prepaid meter
postpaid or 2Postpaid meter
Response shapes here are Danmalama's own design, not the source API's documented behavior

The source API's own documentation has no saved example responses at all — only its requests are documented. Every response shape shown in the MSORG sections below is Danmalama's own reasonable design for this shim, not a guarantee that it matches what that real API actually returns byte-for-byte. If your existing integration strictly parses specific response fields, verify against what you actually get back before relying on it in production.

Catalog ids must be Danmalama's own

plan, disconame/disco_name, and cablename/cableplan values must come from Danmalama's own live catalog — List data plans, List discos, and List providers on the native API (your MSORG API key works there too — same account, just with the X-API-Key header instead of Authorization: Token). MSORG does not accept the source API's own numeric catalog ids for plans, discos, or cable billers/items — only the network id (1–4) and meter-type mapping above are genuinely portable conventions between the two APIs.

Error envelope

MSORG errors use a shape shared with ADEX — { status, message, code } — distinct from both the native API's {success,error{...}} envelope and from MSORG's own success shape:

{
  "status": "error",
  "message": "mobile_number and plan are required.",
  "code": "VALIDATION_ERROR"
}

Note that status is a boolean true on some successful MSORG responses (account lookup, listings, validation) and a string "success" on others (purchase endpoints, mirroring the underlying transaction status like "processing" or "failed") — and a different string, "error", on failure. Don't write a single === true check across every MSORG endpoint; check the HTTP status code first, then branch on whichever form of status that specific endpoint actually documents below.

Compatibility mode

ADEX

ADEX matches the request/response field names and auth flow of another widely-used VTU reseller API, including its two-step auth exchange.

Base URL

https://api.danmalama.com.ng/compat/adex

⚠ Two-step auth, and your password grows a PIN suffix — read this before your first call

The source API has no transaction-PIN concept either, but Danmalama requires one on every purchase. Its auth flow is HTTP Basic → bearer token, so the PIN is folded into the Basic auth password field instead of the token:

Step 1 — exchange for a token. POST /compat/adex/api/user with HTTP Basic auth, where:

Username = your account email
Password = <your account password>:<your 4-digit PIN> — password, a colon, then the PIN

This returns { status, AccessToken, balance, username } — AccessToken is already the same <api-key>:<pin> composite token MSORG uses, ready to use as-is.

Step 2 — use it. Every other ADEX call sends Authorization: Token <AccessToken>, exactly like MSORG.

Requesting a new token this way rotates your account's API key — any previously issued key (native, MSORG, or ADEX) stops working immediately. Don't call this more than once per credential change, and if you do, update every other integration you have running against the old key.

Get a token

curl -X POST https://api.danmalama.com.ng/compat/adex/api/user \
  -u "you@example.com:your_account_password:1234"
const creds = btoa("you@example.com:your_account_password:1234");
const res = await fetch("https://api.danmalama.com.ng/compat/adex/api/user", {
  method: "POST",
  headers: { Authorization: `Basic ${creds}` }
});
const { AccessToken } = await res.json();
// AccessToken is "<api-key>:<pin>" -- use it as Authorization: Token <AccessToken>
// on every other ADEX call below.

Response

{
  "status": "success",
  "AccessToken": "dmk_live_your_key_here:1234",
  "balance": "15230.00",
  "username": "you@example.com"
}

Network IDs & meter type

ADEX shares the exact same network-id and meter-type conventions as MSORG (same underlying helper in Danmalama's code):

Numeric idNetwork
1MTN
2Airtel
3Glo
49mobile

meter_type: prepaid or 1; postpaid or 2.

Response shapes here mirror the source API's real documented examples

Unlike MSORG, the ADEX source API's own documentation did publish real example responses, and ADEX mirrors them closely — field names like oldbal/newbal, wallet_vending, and system come straight from those documented examples. You can rely on these shapes with more confidence than MSORG's — though they're still Danmalama's own implementation of that shim, not a live proxy to that other API, so treat them as accurate-as-documented rather than contractually guaranteed forever.

Catalog ids must be Danmalama's own

Exactly the same constraint as MSORG: data_plan, disco, and cablename/cableplan must be real Danmalama ids from List data plans, List discos, and List providers — ADEX does not accept the source API's own catalog ids. Only the network id (1–4) and meter-type mapping above are genuinely portable.

Error envelope

Same shape as MSORG (shared code): { status: "error", message, code }. ADEX's success responses are more consistent than MSORG's — every purchase and validation endpoint documented below uses the string "success", never a bare boolean.

{
  "status": "error",
  "message": "phone and data_plan are required.",
  "code": "VALIDATION_ERROR"
}

GET /auth/me Native · MSORG · ADEX

Returns your account details, including your current wallet balance. Shown in three shapes below — Danmalama's own API, and the two drop-in compatibility modes.

Auth: X-API-Key: dmk_live_...

There is no separate "check balance" endpoint — use this one, or read the balance field returned by any purchase.

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"
    }
  }
}
Auth: Authorization: Token <api-key>:<pin>

GET /api/user/ — exactly as documented, including the trailing slash.

curl https://api.danmalama.com.ng/compat/msorg/api/user/ \
  -H "Authorization: Token dmk_live_your_key_here:1234"
const res = await fetch("https://api.danmalama.com.ng/compat/msorg/api/user/", {
  headers: { Authorization: "Token dmk_live_your_key_here:1234" }
});
const data = await res.json();
console.log(data.user.wallet_balance);

Response

{
  "status": true,
  "user": {
    "id": "68f1a2b3c4d5e6f7a8b9c0d1",
    "username": "you@example.com",
    "email": "you@example.com",
    "full_name": "Adiahn Danmalama",
    "wallet_balance": "15230.00"
  }
}

wallet_balance is a string (fixed to 2 decimal places) here, unlike the native API's numeric walletBalance — parse it before doing arithmetic on it.

Auth: HTTP Basic — email:password:pin

ADEX has no separate "who am I" call — account info (balance, username) comes back as part of the token-exchange step itself, POST /api/user, fully documented in the ADEX overview above. Repeated here for convenience:

curl -X POST https://api.danmalama.com.ng/compat/adex/api/user \
  -u "you@example.com:your_account_password:1234"
const creds = btoa("you@example.com:your_account_password:1234");
const res = await fetch("https://api.danmalama.com.ng/compat/adex/api/user", {
  method: "POST",
  headers: { Authorization: `Basic ${creds}` }
});
const { balance, username } = await res.json();

Response

{
  "status": "success",
  "AccessToken": "dmk_live_your_key_here:1234",
  "balance": "15230.00",
  "username": "you@example.com"
}

This call also rotates your API key — see the callout in the ADEX overview before calling it more than you need to.


Airtime

Direct top-ups for MTN, Airtel, Glo, and 9mobile. Available in three shapes below — Danmalama's own API, and the MSORG/ADEX compatibility modes.

POST /vending/airtime Native · MSORG · ADEX

Buys airtime and credits it directly to the recipient's phone number. Capped at ₦5,000 per hour per account (summed across successful purchases in the trailing 60 minutes) on every entry point — they share the same underlying purchase engine and limit.

Auth: X-API-Key: dmk_live_...
FieldTypeDescription
networkstringrequiredOne of MTN, AIRTEL, GLO, 9MOBILE.
phonestringrequired11-digit Nigerian number starting with 0, e.g. 08031234567.
amountintegerrequiredNaira amount, whole number, greater than 0.
pinstringrequiredYour account's 4-digit transaction PIN.
bypassNetworkCheckbooleanoptionalDefault 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.
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
  }
}
Auth: Authorization: Token <api-key>:<pin>

POST /api/topup/ — no pin field: it travels inside your token (see MSORG overview).

FieldTypeDescription
networkstring/numberrequiredMTN/AIRTEL/GLO/9MOBILE, or the numeric id 1–4.
mobile_numberstringrequired11-digit Nigerian number starting with 0.
amountintegerrequiredNaira amount, greater than 0.
Ported_numberbooleanoptionalDefault false. Same network/phone-prefix bypass as the native API's bypassNetworkCheck.
curl -X POST https://api.danmalama.com.ng/compat/msorg/api/topup/ \
  -H "Authorization: Token dmk_live_your_key_here:1234" \
  -H "Content-Type: application/json" \
  -d '{
    "network": "MTN",
    "mobile_number": "08031234567",
    "amount": 500
  }'
const res = await fetch("https://api.danmalama.com.ng/compat/msorg/api/topup/", {
  method: "POST",
  headers: {
    Authorization: "Token dmk_live_your_key_here:1234",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    network: "MTN",
    mobile_number: "08031234567",
    amount: 500
  })
});
const data = await res.json();

Response

{
  "status": "success",
  "id": "68f9a1b2c3d4e5f6a7b8c9d0",
  "network": "MTN",
  "mobile_number": "08031234567",
  "amount": 500,
  "message": "₦500 airtime sent to 08031234567.",
  "oldbal": 15230,
  "newbal": 14730
}
Auth: Authorization: Token <AccessToken>

POST /api/topup — no trailing slash, no pin field (it's inside the token).

FieldTypeDescription
networkstring/numberrequiredName or numeric id 1–4.
phonestringrequired11-digit Nigerian number starting with 0.
amountintegerrequiredNaira amount, greater than 0.
request-idstringoptionalYour own idempotency/reference id — echoed back verbatim. Defaults to Danmalama's transaction id if omitted.
bypassbooleanoptionalDefault false. Same network/phone-prefix bypass.
plan_typestringoptionalDefault VTU. Echoed back verbatim — not validated against anything.
curl -X POST https://api.danmalama.com.ng/compat/adex/api/topup \
  -H "Authorization: Token dmk_live_your_key_here:1234" \
  -H "Content-Type: application/json" \
  -d '{
    "network": "MTN",
    "phone": "08031234567",
    "amount": 500
  }'
const res = await fetch("https://api.danmalama.com.ng/compat/adex/api/topup", {
  method: "POST",
  headers: {
    Authorization: "Token dmk_live_your_key_here:1234",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    network: "MTN",
    phone: "08031234567",
    amount: 500
  })
});
const data = await res.json();

Response

{
  "network": "MTN",
  "request-id": "68f9a1b2c3d4e5f6a7b8c9d0",
  "amount": 500,
  "discount": 0,
  "status": "success",
  "message": "Successfully purchased MTN VTU ₦500 for 08031234567",
  "phone_number": "08031234567",
  "oldbal": "15230",
  "newbal": 14730,
  "system": "API",
  "plan_type": "VTU",
  "wallet_vending": "wallet"
}

oldbal is a string and newbal is a number — an inherited quirk of this shim, not a typo. Coerce both with Number() before doing arithmetic.


Data

Data bundles across all four networks. Plans and prices are account-specific — always fetch the live list rather than hardcoding plan IDs. Buying is available in three shapes below; listing plans is native-only (MSORG/ADEX purchases still require a real Danmalama planId from this list).

GET /vending/data/plans API key required

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 }
  ]
}

These planId values are illustrative, not a fixed contract — always fetch live. This same list is what MSORG's plan field and ADEX's data_plan field must contain.

POST /vending/data Native · MSORG · ADEX

Buys a data plan and delivers it to the recipient's phone number.

Auth: X-API-Key: dmk_live_...
FieldTypeDescription
networkstringrequiredOne of MTN, AIRTEL, GLO, 9MOBILE — must match the plan's network.
phonestringrequired11-digit Nigerian number starting with 0.
planIdstringrequiredFrom List data plans.
pinstringrequiredYour account's 4-digit transaction PIN.
bypassNetworkCheckbooleanoptionalDefault 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
  }
}
Auth: Authorization: Token <api-key>:<pin>

POST /api/data/. plan must be a real Danmalama planId — not the source API's own plan code.

FieldTypeDescription
networkstring/numberrequiredName or numeric id 1–4.
mobile_numberstringrequired11-digit Nigerian number starting with 0.
planstringrequiredDanmalama planId from List data plans.
Ported_numberbooleanoptionalDefault false.
curl -X POST https://api.danmalama.com.ng/compat/msorg/api/data/ \
  -H "Authorization: Token dmk_live_your_key_here:1234" \
  -H "Content-Type: application/json" \
  -d '{
    "network": "MTN",
    "mobile_number": "08031234567",
    "plan": "173"
  }'
const res = await fetch("https://api.danmalama.com.ng/compat/msorg/api/data/", {
  method: "POST",
  headers: {
    Authorization: "Token dmk_live_your_key_here:1234",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    network: "MTN",
    mobile_number: "08031234567",
    plan: "173"
  })
});
const data = await res.json();

Response

{
  "status": "success",
  "id": "68f9a1b2c3d4e5f6a7b8c9d1",
  "network": "MTN",
  "mobile_number": "08031234567",
  "plan": "173",
  "amount": 550,
  "message": "Data plan delivered to 08031234567.",
  "oldbal": 15230,
  "newbal": 14680
}

MSORG also exposes GET /api/data/ (lists your data-type transactions) and GET /api/data/:id (looks up one by id — doubles as the airtime lookup too). Both are covered under Get a transaction.

Auth: Authorization: Token <AccessToken>

POST /api/data. data_plan must be a real Danmalama planId — not the source API's own plan code.

FieldTypeDescription
networkstring/numberrequiredName or numeric id 1–4.
phonestringrequired11-digit Nigerian number starting with 0.
data_planstringrequiredDanmalama planId.
request-idstringoptionalEchoed back verbatim; defaults to the transaction id.
bypassbooleanoptionalDefault false.
curl -X POST https://api.danmalama.com.ng/compat/adex/api/data \
  -H "Authorization: Token dmk_live_your_key_here:1234" \
  -H "Content-Type: application/json" \
  -d '{
    "network": "MTN",
    "phone": "08031234567",
    "data_plan": "173"
  }'
const res = await fetch("https://api.danmalama.com.ng/compat/adex/api/data", {
  method: "POST",
  headers: {
    Authorization: "Token dmk_live_your_key_here:1234",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    network: "MTN",
    phone: "08031234567",
    data_plan: "173"
  })
});
const data = await res.json();

Response

{
  "network": "MTN",
  "request-id": "68f9a1b2c3d4e5f6a7b8c9d1",
  "amount": "550",
  "dataplan": "173",
  "status": "success",
  "message": "Data delivered to 08031234567.",
  "response": "Data delivered to 08031234567.",
  "phone_number": "08031234567",
  "oldbal": "15230",
  "newbal": 14680,
  "system": "API",
  "plan_type": "GIFTING",
  "wallet_vending": "wallet"
}

amount and oldbal are strings here, newbal is a number — same coerce-before-you-add caution as Buy airtime.


Electricity

Prepaid and postpaid electricity across every major Nigerian disco. Validate/buy are available in three shapes below; listing discos is native-only.

GET /vending/electricity/discos API key required

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. This is also the only source of valid disconame/disco_name/disco values for MSORG and ADEX below — neither accepts the source APIs' own disco ids.

POST /vending/electricity/validate-meter Native · MSORG · ADEX

Resolves the customer name on a meter number before you charge anyone. This does not debit your wallet on any entry point.

Auth: X-API-Key: dmk_live_...
FieldTypeDescription
discoIdstringrequiredFrom List discos.
meterNumberstringrequired
meterTypestringrequiredprepaid or postpaid.
amountintegerrequiredThe 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"
  }
}
Auth: Authorization: Token <api-key>:<pin>

GET /api/validatemeter — a GET with query-string parameters, not a POST body.

Query paramTypeDescription
disconamestringrequiredDanmalama discoId.
meternumberstringrequired
mtypestring/numberrequiredprepaid/1 or postpaid/2.
amountintegeroptionalDefault 1000 if omitted.
curl "https://api.danmalama.com.ng/compat/msorg/api/validatemeter?disconame=ikeja-electric&meternumber=04123456789&mtype=prepaid&amount=5000" \
  -H "Authorization: Token dmk_live_your_key_here:1234"

Response

{
  "status": true,
  "customer_name": "ADEBAYO O. FASHOLA",
  "address": "12 Allen Avenue, Ikeja, Lagos"
}
Auth: Authorization: Token <AccessToken>

GET /api/bill/bill-validation.

Query paramTypeDescription
discostringrequiredDanmalama discoId.
meter_numberstringrequired
meter_typestring/numberrequiredprepaid/1 or postpaid/2.
amountintegeroptionalDefault 1000.
curl "https://api.danmalama.com.ng/compat/adex/api/bill/bill-validation?disco=ikeja-electric&meter_number=04123456789&meter_type=prepaid&amount=5000" \
  -H "Authorization: Token dmk_live_your_key_here:1234"

Response

{
  "status": "success",
  "name": "ADEBAYO O. FASHOLA"
}

Field is name, not customer_name — and there's no address at all here, unlike native and MSORG.

POST /vending/electricity Native · MSORG · ADEX

Buys electricity credit. Minimum ₦1,000 on every entry point.

Auth: X-API-Key: dmk_live_...

customerName must be the value returned by Validate meter — don't ask your own users to type it in.

FieldTypeDescription
discoIdstringrequired
meterNumberstringrequired
meterTypestringrequiredprepaid or postpaid.
amountintegerrequiredMinimum ₦1,000.
customerNamestringrequiredFrom Validate meter's response.
pinstringrequiredYour 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.

Auth: Authorization: Token <api-key>:<pin>

POST /api/billpayment/. Unlike the native API, there's no customerName field — this endpoint validates the meter itself internally before purchasing, using whatever name comes back.

FieldTypeDescription
disco_namestringrequiredDanmalama discoId.
meter_numberstringrequired
MeterTypestring/numberrequiredNote the PascalCase field name — prepaid/1 or postpaid/2.
amountintegerrequiredMinimum 1000.
curl -X POST https://api.danmalama.com.ng/compat/msorg/api/billpayment/ \
  -H "Authorization: Token dmk_live_your_key_here:1234" \
  -H "Content-Type: application/json" \
  -d '{
    "disco_name": "ikeja-electric",
    "meter_number": "04123456789",
    "MeterType": "prepaid",
    "amount": 5000
  }'

Response

{
  "status": "success",
  "id": "68f9a1b2c3d4e5f6a7b8c9d2",
  "disco_name": "ikeja-electric",
  "meter_number": "04123456789",
  "amount": 5000,
  "token": "1234-5678-9012-3456-7890",
  "message": "Electricity payment successful.",
  "oldbal": 14680,
  "newbal": 9680
}

token is only present for prepaid meters where the disco returned one — omitted otherwise.

Auth: Authorization: Token <AccessToken>

POST /api/bill. Also auto-validates the meter internally — no customerName field needed.

FieldTypeDescription
discostringrequiredDanmalama discoId.
meter_numberstringrequired
meter_typestring/numberrequiredprepaid/1 or postpaid/2.
amountintegerrequiredMinimum 1000.
request-idstringoptionalEchoed back; defaults to the transaction id.
curl -X POST https://api.danmalama.com.ng/compat/adex/api/bill \
  -H "Authorization: Token dmk_live_your_key_here:1234" \
  -H "Content-Type: application/json" \
  -d '{
    "disco": "ikeja-electric",
    "meter_number": "04123456789",
    "meter_type": "prepaid",
    "amount": 5000
  }'

Response

{
  "disco_name": "ikeja-electric",
  "request-id": "68f9a1b2c3d4e5f6a7b8c9d2",
  "amount": 5000,
  "charges": 0,
  "status": "success",
  "message": "Transaction successful ikeja-electric PREPAID ₦5000 to 04123456789",
  "meter_number": "04123456789",
  "meter_type": "PREPAID",
  "oldbal": "14680",
  "newbal": 9680,
  "system": "API",
  "token": "1234-5678-9012-3456-7890",
  "wallet_vending": "wallet"
}

meter_type is echoed back upper-cased in the response. charges is always 0 — Danmalama doesn't add a separate convenience fee on this shim.


Cable TV

DStv, GOtv, StarTimes, and Showmax subscriptions — new plan changes or straight renewals. Validate/buy are available in three shapes below; listing providers is native-only.

GET /vending/cable/providers API key required

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 }
      ]
    }
  ]
}

These billerId/itemId values are illustrative, not a fixed contract — always fetch live. This is also the only source of valid cablename/cableplan values for MSORG and ADEX below.

POST /vending/cable/validate-smartcard Native · MSORG · ADEX

Resolves the customer name on a smartcard/IUC number.

Auth: X-API-Key: dmk_live_...

If the smartcard already has an active subscription, renewalAmount is returned too — use it for a renew purchase instead of guessing the plan price.

FieldTypeDescription
billerIdstringrequiredFrom List providers.
itemIdstringoptionalA specific plan, if known.
smartcardNumberstringrequired
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
  }
}
Auth: Authorization: Token <api-key>:<pin>

GET /api/validateiuc.

Query paramTypeDescription
cablenamestringrequiredDanmalama billerId.
smart_card_numberstringrequired
curl "https://api.danmalama.com.ng/compat/msorg/api/validateiuc?cablename=dstv&smart_card_number=1234567890" \
  -H "Authorization: Token dmk_live_your_key_here:1234"

Response

{
  "status": true,
  "customer_name": "CHIOMA N. OKEKE",
  "renewal_amount": 19000
}
Auth: Authorization: Token <AccessToken>

GET /api/cable/cable-validation.

Query paramTypeDescription
cablestringrequiredDanmalama billerId.
iucstringrequiredSmartcard/IUC number.
curl "https://api.danmalama.com.ng/compat/adex/api/cable/cable-validation?cable=dstv&iuc=1234567890" \
  -H "Authorization: Token dmk_live_your_key_here:1234"

Response

{
  "status": "success",
  "name": "CHIOMA N. OKEKE"
}

No renewal amount here at all — unlike native and MSORG, ADEX's validation response is just the resolved name. If you need the renewal price, call List providers on the native API instead.

POST /vending/cable Native · MSORG · ADEX

Buys or renews a cable subscription. There's no amount field on any entry point — price is always resolved server-side.

Auth: X-API-Key: dmk_live_...

Price is resolved from the catalog (change) or the freshly re-validated renewal amount (renew) — a client can never supply a price.

FieldTypeDescription
billerIdstringrequired
itemIdstringoptionalRequired when subscriptionType is change.
smartcardNumberstringrequired
customerNamestringrequiredFrom Validate smartcard.
subscriptionTypestringoptionalchange (default) or renew.
pinstringrequiredYour 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
  }
}
Auth: Authorization: Token <api-key>:<pin>

POST /api/cablesub/. No customerName field — this endpoint re-validates the smartcard internally before purchasing. subscriptionType is inferred automatically: sending cableplan means change, omitting it means renew.

FieldTypeDescription
cablenamestringrequiredDanmalama billerId.
cableplanstringoptionalDanmalama itemId. Omit to renew the existing plan.
smart_card_numberstringrequired
curl -X POST https://api.danmalama.com.ng/compat/msorg/api/cablesub/ \
  -H "Authorization: Token dmk_live_your_key_here:1234" \
  -H "Content-Type: application/json" \
  -d '{
    "cablename": "dstv",
    "cableplan": "dstv-compact",
    "smart_card_number": "1234567890"
  }'

Response

{
  "status": "success",
  "id": "68f9a1b2c3d4e5f6a7b8c9d3",
  "cablename": "dstv",
  "cableplan": "dstv-compact",
  "smart_card_number": "1234567890",
  "amount": 19000,
  "message": "Cable subscription successful.",
  "oldbal": 44680,
  "newbal": 25680
}
Auth: Authorization: Token <AccessToken>

POST /api/cable. Same auto-validate and cableplan-presence inference as MSORG.

FieldTypeDescription
cablenamestringrequiredDanmalama billerId.
cableplanstringoptionalDanmalama itemId. Omit to renew.
smart_card_numberstringrequired
request-idstringoptionalEchoed back; defaults to the transaction id.
curl -X POST https://api.danmalama.com.ng/compat/adex/api/cable \
  -H "Authorization: Token dmk_live_your_key_here:1234" \
  -H "Content-Type: application/json" \
  -d '{
    "cablename": "dstv",
    "cableplan": "dstv-compact",
    "smart_card_number": "1234567890"
  }'

Response

{
  "cablename": "dstv",
  "cableplan": "dstv-compact",
  "amount": 19000,
  "status": "success",
  "message": "Successfully subscribed dstv for Smart Card 1234567890",
  "smart_card_number": "1234567890",
  "request-id": "68f9a1b2c3d4e5f6a7b8c9d3",
  "oldbal": 44680,
  "newbal": 25680,
  "system": "API"
}

Unlike its own data/airtime/electricity responses, ADEX's cable oldbal here is a number, not a string — a further inconsistency inherited from the shim, worth defensive coercion regardless.


Transactions

Every purchase you make — airtime, data, electricity, cable, and voucher activity — lands here as a single unified record, regardless of whether it was made through the native API, MSORG, or ADEX. Listing is native-only; MSORG additionally exposes its own per-type listing (GET /api/data/ for data-type transactions) with no equivalent for airtime/electricity/cable or a combined "all types" list. Looking up a single transaction by id is documented below.

GET /vending/history API key required

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"
    }
  ]
}
GET /vending/history/:id Native · MSORG

Fetches one transaction by its MongoDB id. Use this to poll a processing purchase until it resolves. ADEX has no transaction-lookup-by-id endpoint in this compatibility layer — the purchase response itself (see Buy airtime / Buy data / etc. above) is the only confirmation an ADEX-shaped client gets back.

Auth: X-API-Key: dmk_live_...

Use the MongoDB _id (not the reference string). Returns 404 NOT_FOUND both when the id doesn't exist and when it belongs to a different account — 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"
  }
}
Auth: Authorization: Token <api-key>:<pin>

MSORG has no single generic lookup — instead, three type-specific endpoints, matching how the source API documents its own "Query Data/Airtime/Electricity/Cable Transaction" calls. Use the one matching the transaction's type:

Transaction typePath
Data or AirtimeGET /api/data/:id
ElectricityGET /api/billpayment/:id
CableGET /api/cablesub/:id

The data endpoint doubles as the airtime lookup too — the source's own docs point both at the same path.

curl https://api.danmalama.com.ng/compat/msorg/api/data/68f9a1b2c3d4e5f6a7b8c9d1 \
  -H "Authorization: Token dmk_live_your_key_here:1234"
const res = await fetch(
  "https://api.danmalama.com.ng/compat/msorg/api/data/68f9a1b2c3d4e5f6a7b8c9d1",
  { headers: { Authorization: "Token dmk_live_your_key_here:1234" } }
);
const data = await res.json();

Response — data / airtime (/api/data/:id)

{
  "id": "68f9a1b2c3d4e5f6a7b8c9d1",
  "network": "MTN",
  "mobile_number": "08031234567",
  "plan": "173",
  "amount": 550,
  "status": "success",
  "created_at": "2026-09-30T10:14:00.000Z"
}

For an airtime transaction, plan comes back null — airtime purchases have no plan id.

Response — electricity (/api/billpayment/:id)

{
  "status": true,
  "id": "68f9a1b2c3d4e5f6a7b8c9d2",
  "disco_name": "ikeja-electric",
  "meter_number": "04123456789",
  "amount": 5000,
  "status_detail": "successful"
}

Response — cable (/api/cablesub/:id)

{
  "status": true,
  "id": "68f9a1b2c3d4e5f6a7b8c9d3",
  "cablename": "dstv",
  "smart_card_number": "1234567890",
  "amount": 19000,
  "status_detail": "successful"
}

Note the shapes genuinely differ across the three: data/airtime uses status (the string form) and created_at; electricity/cable use a boolean status plus a separate status_detail for the actual transaction status, and no timestamp at all. This isn't a documentation simplification — it's what the shipped code returns.

ADEX has no transaction-lookup-by-id endpoint -- see the note above. The purchase response itself (Buy airtime / Buy data / Buy electricity / Buy cable) is the only confirmation an ADEX-shaped client gets back.
POST /vending/request-refund API key required

Flags a transaction for manual admin review. This does not refund anything itself — it's a request, reviewed by a human. Native API only — there's no MSORG/ADEX equivalent, since neither source API documents a refund-request endpoint.

FieldTypeDescription
transactionIdstringrequired
userNotestringoptionalUp 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 } }
CodeStatusWhen
NOT_FOUND404Transaction doesn't exist.
FORBIDDEN403The transaction belongs to a different account.
CONFLICT409Already 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. Native API only — vouchers have no MSORG/ADEX equivalent.

POST /vouchers API key required

Creates a voucher and debits its full cost from your wallet immediately.

FieldTypeDescription
typestringrequiredwallet, airtime, data, electricity, or cable.
providerstringrequired*Network / discoId / billerId — required for every type except wallet.
planIdstringoptionalRequired for data and cable types.
amountnumberoptionalRequired for wallet, airtime, and electricity types — ignored for data/cable, whose price comes from the plan.
maxRedemptionsintegerrequired1–1000. How many times this code can be redeemed in total.
expiresAtstringrequiredISO date, must be in the future.
messagestringoptionalUp to 200 characters, shown to whoever looks up the code.
codestringoptional4–24 chars, letters/numbers/dashes. Auto-generated (DMV-XXXXXX) if omitted.
pinstringrequiredYour 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
  }
}
GET /vouchers API key required

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"
GET /vouchers/lookup/:code API key required

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
  }
}
POST /vouchers/redeem API key required

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 — reference doesn't apply to it.
FieldTypeDescription
codestringrequired
referencestringoptionalPhone/meter/smartcard number to deliver to. Omit to claim now, deliver later.
meterTypestringoptionalprepaid or postpaid — required when delivering an electricity voucher.
pinstringrequiredYour 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.

CodeStatusWhen
NOT_FOUND404Code doesn't exist.
VALIDATION_ERROR400Expired, or fully redeemed.
CONFLICT409You've already redeemed this code — one redemption per account.
PROVIDER_ERROR502Delivery failed. Your redemption slot is not consumed — retry with corrected details.
POST /vouchers/:code/use API key required

Delivers a voucher you previously claimed without a reference. No PIN needed here — you already proved intent when you claimed it.

FieldTypeDescription
referencestringrequiredPhone/meter/smartcard number to deliver to.
meterTypestringoptionalRequired 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" } }
GET /vouchers/redeemed API key required

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. Native API only.

POST /wallet/virtual-account API key required

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.

FieldTypeDescription
bvnstringoptional*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.

Open the app →