Skip to content
Lodacks

API v1

Lodacks reseller API

Sell mobile data from your own app or website. Top up a prepaid wallet, place orders at your reseller prices, and get webhooks as each order moves.

Base URL
https://lodacks.com/api/v1
Format
JSON over HTTPS · amounts in pesewas (GHS × 100)

Every response shown on this page is an example: ids, amounts and dates are illustrative, not real products or prices. Call the API for live data.

Getting started

  1. Create a Lodacks account, then apply for API access. We review every application.
  2. In the portal, create an API key (choose its permissions; optionally lock it to your server's IPs).
  3. Top up your wallet with Paystack. It's credited once the payment is confirmed.
  4. List products, place orders, and set a webhook URL to hear about results.

There's no test mode: every order is real and charges your wallet. Start with a small top-up.

Authentication

Send your key as a bearer token on every request. Keys look like ldk_live_xxxxxxxx_… and are shown only once, when created. Keep them on your server.

curl https://lodacks.com/api/v1/balance \
  -H "Authorization: Bearer ldk_live_xxxxxxxx_your_secret_key"

Permissions per key: products:read, orders:read, orders:write, wallet:read.

Errors

Errors use HTTP status codes and a stable code you can branch on:

{
  "error": {
    "code": "insufficient_balance",
    "message": "Your wallet balance is too low for this order. Top up in the developer portal.",
    "details": { "balance": 500, "price": 1150 }
  }
}
Error codes
CodeHTTPMeaning
invalid_api_key401Missing, malformed or revoked key.
account_inactive403Your reseller account is pending, suspended or rejected.
ip_not_allowed403The key has an IP allow-list and this address isn't on it.
insufficient_scope403The key lacks the permission this endpoint needs.
rate_limited429Too many requests. Wait for Retry-After seconds.
idempotency_key_required400POST /orders needs an Idempotency-Key header.
idempotency_key_reused422The key was used before with different parameters.
request_in_progress409The first request with this key is still running.
invalid_parameter422A parameter is missing or malformed (see details.field).
invalid_phone422Not a Ghana mobile number.
product_unavailable422Unknown product, switched off, or not sold through the API.
insufficient_balance402Top up your wallet. Nothing was charged; you may retry with the same key.
number_check_unavailable503We couldn't check the number with our suppliers just now. Nothing was charged; try again in a few minutes.
number_not_verified422This number can't receive data bundles from us yet (not verified with the network's supplier). Nothing was charged; try another number.
ordering_paused503New orders are paused for a while. Nothing was charged; try again later.
duplicate_client_reference409You already used this client_reference (details.order_id has the order).
order_not_found404No order with that id or client_reference on your account.
not_found404No such endpoint.

Rate limits

Each key may make a set number of requests a minute (60 to start), and your account a set number of orders a minute (20 to start). Every response carries X-RateLimit-Limit and X-RateLimit-Remaining; a 429 carries Retry-After. Ask us if you need more.

Idempotency

Every POST /orders needs an Idempotency-Key header: a unique value you generate per order (a UUID is ideal). If the connection drops, send the same request with the same key: you get the original answer (with Idempotent-Replayed: true) and never a second order or charge. Reusing a key with different parameters is refused.

Answers that may change on retry (402 insufficient balance, 409 in progress, 429, 5xx) aren't stored, so you can retry them with the same key.

Products

GET /products · products:read · optional ?network=mtn

Bundles you can order, with your price. Prices are set by Lodacks and can change; the price charged is the one at the moment you order, shown on the order.

{
  "data": [
    {
      "id": "MTN-2GB-30D",
      "network": "MTN",
      "network_key": "mtn",
      "volume_mb": 2048,
      "volume": "2GB",
      "validity_days": 30,
      "price": 1150,
      "currency": "GHS"
    }
  ]
}

Create an order

POST /orders · orders:write

Charges your wallet the product's price and starts delivery. Parameters: product (a product id), phone (the Ghana number to receive the data, any common format), optional client_reference (your own id, unique per account, up to 64 characters).

curl -X POST https://lodacks.com/api/v1/orders \
  -H "Authorization: Bearer $LODACKS_KEY" \
  -H "Idempotency-Key: 6f1c2b7e-1d4e-4a7b-9b55-1e0f5c2a9d10" \
  -H "Content-Type: application/json" \
  -d '{"product": "MTN-2GB-30D", "phone": "0241234567", "client_reference": "INV-1042"}'

201 Created:

{
  "data": {
    "id": "LDK-7Q2M9XKD",
    "client_reference": "INV-1042",
    "status": "pending",
    "product": "MTN-2GB-30D",
    "network": "MTN",
    "volume_mb": 2048,
    "volume": "2GB",
    "validity_days": 30,
    "phone": "0241234567",
    "amount": 1150,
    "currency": "GHS",
    "refunded_amount": null,
    "created_at": "2026-10-05T09:12:44+00:00",
    "paid_at": "2026-10-05T09:12:44+00:00",
    "delivered_at": null,
    "refunded_at": null
  }
}

Get an order

GET /orders/{id} · orders:read

{id} is our order id (LDK-…) or your client_reference. Returns the order object above.

List orders

GET /orders · orders:read

Newest first. Filters: status, phone. Paging: page (from 1), limit (1–100, default 25).

{ "data": [ { …order… } ], "meta": { "page": 1, "limit": 25, "total": 140, "has_more": true } }

Order statuses

pending
: paid from your wallet and on its way (checks, queue, sent to the network).
processing
: the network accepted it; waiting for confirmation.
success
: delivered. Final.
failed
: couldn't be delivered. We check it and refund your wallet; it then becomes refunded.
refunded
: the amount is back in your wallet (refunded_amount). Final.

An order is never retried blindly: when a network's answer is unclear we confirm it first, so you're never charged for a double delivery. That can keep an order pending for a while.

Balance and transactions

GET /balance · wallet:read

{ "data": { "balance": 48500, "currency": "GHS", "formatted": "GH₵ 485.00" } }

GET /transactions · wallet:read · ?type=deposit|order_charge|refund · page, limit

{
  "data": [
    { "id": "wtx_812", "type": "order_charge", "amount": -1150, "balance_after": 48500,
      "currency": "GHS", "order_id": "LDK-7Q2M9XKD", "description": "Order LDK-7Q2M9XKD",
      "created_at": "2026-10-05T09:12:44+00:00" }
  ],
  "meta": { "page": 1, "limit": 25, "total": 31, "has_more": true }
}

Webhooks

Set an https URL in the portal. We POST one event per order change:

  • order.created
  • order.processing
  • order.success
  • order.failed
  • order.refunded
POST /your/webhook
Lodacks-Event: order.success
Lodacks-Delivery: evt_4k2j…
Lodacks-Signature: t=1791191564,v1=5f2c…

{ "id": "evt_4k2j…", "type": "order.success", "created_at": "2026-10-05T09:13:20+00:00",
  "data": { "order": { …order object… } } }

Reply with any 2xx within 5 seconds. Otherwise we retry after 1, 5, 15 and 60 minutes, then 3, 6 and 12 hours. Each event is sent at least once and may arrive more than once or out of order: use the event id to ignore repeats and the order's status as the truth (or call GET /orders/{id}).

Verifying webhooks

v1 is the hex HMAC-SHA256 of "{t}.{raw body}" using your signing secret (whsec_…, shown once in the portal). Reject the request if it doesn't match or if t is more than 5 minutes old.

// PHP
$header = $_SERVER['HTTP_LODACKS_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $sig);   // ['t' => …, 'v1' => …]
$body = file_get_contents('php://input');
$expected = hash_hmac('sha256', $sig['t'].'.'.$body, getenv('LODACKS_WEBHOOK_SECRET'));
if (! hash_equals($expected, $sig['v1'] ?? '') || abs(time() - (int) $sig['t']) > 300) {
    http_response_code(400); exit;
}
// Node.js (Express: use express.raw({ type: 'application/json' }) for this route)
const crypto = require('crypto');
const { t, v1 } = Object.fromEntries(req.get('Lodacks-Signature').split(',').map(p => p.split('=')));
const expected = crypto.createHmac('sha256', process.env.LODACKS_WEBHOOK_SECRET).update(`${t}.${req.body}`).digest('hex');
const ok = v1 && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)) && Math.abs(Date.now() / 1000 - t) <= 300;

Security checklist

  • Keep keys and the signing secret on your server only, never in an app, web page or repository.
  • Give each key only the permissions it needs, and add an IP allow-list for production servers.
  • Revoke a key in the portal the moment you think it leaked; replace the signing secret the same way.
  • Always verify webhook signatures, and send an Idempotency-Key with every order.
  • The portal's Logs page shows every request made with your keys.