API Keys
Generate a key below to authenticate API requests. Keys start with ink_live_ and are shown only once.

Developers

Inkoom API documentation

A JSON REST API for agents, super agents and dealers. Sell data and airtime from your own site, bot or POS — priced at your reseller tier, charged to your Inkoom wallet.

Base URL

https://inkoomgh.com/api/public/v1

Auth

Bearer ink_live_…

Rate limit

120 req / min / key

Quickstart

Three calls get you selling. Everything is JSON over HTTPS.

  1. Create a key in your dashboard under API Keys (Agent tier or above). It is shown once.
  2. Fund your Inkoom wallet — every purchase is debited from it at your tier price.
  3. List products, then POST a purchase with your own unique reference.
# 1. check your wallet
curl https://inkoomgh.com/api/public/v1/balance -H "Authorization: Bearer ink_live_xxx"

# 2. list MTN bundles at your tier price
curl "https://inkoomgh.com/api/public/v1/products?network=mtn" -H "Authorization: Bearer ink_live_xxx"

# 3. buy one
curl -X POST https://inkoomgh.com/api/public/v1/purchase \
  -H "Authorization: Bearer ink_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{"product_id":"<uuid>","recipient":"0241234567","reference":"order-00123"}'
bash

Authentication

Send your key on every request. Either header works — pick one and stick to it. Keys start with ink_live_, are stored hashed on our side, and are shown to you only once at creation.

Authorization: Bearer ink_live_xxxxxxxxxxxxxxxx
# or
x-api-key: ink_live_xxxxxxxxxxxxxxxx
http

A missing, revoked or banned key returns 401 with { "success": false, "error": "Invalid or missing API key", "code": "unauthorized" }. Never expose a key in browser code — call the API from your server.

Conventions

  • Every response is { "success": boolean, "data": … } or { "success": false, "error": string, "code": string }.
  • All amounts are in Ghana Cedis (GHS) as numbers, e.g. 15.5.
  • Phone numbers accept 0241234567, 233241234567 or +233241234567 — we normalise them.
  • Product prices are resolved per key: your tier (agent, super agent, dealer) is applied automatically.

Unified Action API (POST /api/v1)

POST/api/v1 (or /api/public/v1)

Inkoom provides a single-endpoint Action API identical to Bundle Portal. Send a JSON body with an action property, authenticated with your x-api-key or Bearer token.

Drop-in Bundle Portal Compatible

If you have an existing integration built for Bundle Portal, you can switch your base URL to https://inkoomgh.com/api/v1 without changing your request payload format!

Supported Actions

FieldTypeDescription
check_balanceNo extra paramsReturns spendable wallet, commission balance, and role.
verify_numbernetwork, recipientChecks number eligibility with operator before placing orders.
get_bundlesnetwork (optional)Lists active bundle packages and your tier price.
place_ordernetwork, package_size, recipient (or product_id)Places an order with automatic carrier waterfall.
check_statusorder_reference or order_idPolls live fulfillment status of an order.
order_airtimenetwork, recipient, amountInstant mobile airtime recharge (GHS 1 - 500).
get_result_checker_typesNo extra paramsLists WAEC and BECE result checker voucher types.
order_result_checkerchecker_type, quantity (opt)Instantly purchases WAEC / BECE result checker PIN and Serial.
get_transactionslimit, page (optional)Returns your wallet ledger history.
get_special_offersNo extra paramsLists active promo data bundles.
share_datafrom_phone, to_phone, network, volume_gbInitiates data volume transfer.

Example: Place Order via Action API

curl -X POST https://inkoomgh.com/api/v1 \
  -H "x-api-key: ink_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "place_order",
    "network": "mtn",
    "recipient": "0241234567",
    "package_size": 5,
    "order_id": "client-ref-1001"
  }'
bash
{
  "success": true,
  "data": {
    "order_id": "…uuid…",
    "reference": "client-ref-1001",
    "recipient": "0241234567",
    "network": "mtn",
    "package_size": 5,
    "amount": 24.00,
    "status": "success"
  }
}
json

Wallet balance

GET/api/public/v1/balance

Returns your spendable wallet, commission balance and reseller role.

{
  "success": true,
  "data": {
    "currency": "GHS",
    "wallet_balance": 482.50,
    "commission_balance": 26.10,
    "role": "agent"
  }
}
json

Networks

GET/api/public/v1/networks

The active networks you can sell on. Use code to filter products.

{
  "success": true,
  "data": [
    { "id": "…uuid…", "code": "mtn", "name": "MTN Ghana" },
    { "id": "…uuid…", "code": "telecel", "name": "Telecel Ghana" },
    { "id": "…uuid…", "code": "at", "name": "AT (iShare & BigTime)" }
  ]
}
json

Products (bundles)

GET/api/public/v1/products?network=mtn&service=data

Cache this list and refresh it hourly — id values are stable, prices can change when the operator updates the catalogue.

FieldTypeDescription
networkquery, optionalNetwork code: mtn, telecel, at.
servicequery, optionaldata | airtime | afa | result_checker | combo.
{
  "success": true,
  "data": [
    {
      "id": "b1f0…-…-…",
      "name": "5GB",
      "service": "data",
      "network": "mtn",
      "volume_mb": 5120,
      "validity_days": 90,
      "price": 24.00
    }
  ]
}
json

price is what your wallet is debited — mark it up however you like.

Place an order

POST/api/public/v1/purchase
FieldTypeDescription
product_idstring (uuid), requiredFrom GET /products.
recipientstring, requiredGhana MSISDN receiving the bundle.
referencestring, requiredYour unique order id, 8–128 chars. Makes the call idempotent.
curl -X POST https://inkoomgh.com/api/public/v1/purchase \
  -H "Authorization: Bearer ink_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "product_id": "b1f0…-…-…",
    "recipient": "0241234567",
    "reference": "shop-2026-00123"
  }'
bash
{
  "success": true,
  "data": {
    "order_id": "…uuid…",
    "reference": "shop-2026-00123",
    "recipient": "0241234567",
    "amount": 24.00,
    "status": "success",
    "reason": null
  }
}
json

Order lifecycle

  • success — delivered. Wallet already debited.
  • processing — accepted by an upstream carrier partner, not yet confirmed. Poll the order endpoint; do not re-send.
  • failed — every route failed. Your wallet is refunded automatically and reason explains why.

Behind one call we run a supplier waterfall: if the first carrier partner rejects or times out, we fall through to the next one before failing.

Airtime top-up

POST/api/v1/airtime (or /api/public/v1/airtime)

Top up any MTN, Telecel or AT Ghana number with instant airtime between GHS 1 and GHS 500.

FieldTypeDescription
recipientstring, required10-digit Ghana phone number.
networkstring, requiredmtn | telecel | at.
amountnumber, requiredRecharge amount in GHS (1 to 500).
order_idstring, optionalIdempotency key for this recharge.
curl -X POST https://inkoomgh.com/api/v1/airtime \
  -H "x-api-key: ink_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "recipient": "0541234567",
    "network": "mtn",
    "amount": 10.00
  }'
bash
{
  "success": true,
  "data": {
    "order_id": "…uuid…",
    "reference": "ink_air_…",
    "recipient": "0541234567",
    "network": "mtn",
    "amount": 10,
    "status": "completed"
  }
}
json

Result Checkers

POST/api/v1/checkers (or action: order_result_checker)

Purchase WAEC WASSCE and BECE result checker voucher cards. The response delivers PIN and Serial numbers immediately upon wallet debit.

FieldTypeDescription
checker_typestring, requiredwaec | bece | novdec.
quantitynumber, optionalQuantity of scratch cards to purchase (default 1).
recipient_phonestring, optionalPhone number for SMS voucher notification.
curl -X POST https://inkoomgh.com/api/v1/checkers \
  -H "x-api-key: ink_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "checker_type": "waec",
    "quantity": 1
  }'
bash
{
  "success": true,
  "data": {
    "order_id": "…uuid…",
    "checker_type": "waec",
    "quantity": 1,
    "checkers": [
      {
        "pin": "123456789012",
        "serial": "WEC202612345",
        "type": "waec"
      }
    ]
  }
}
json

Wallet Transactions

GET/api/v1/transactions (or action: get_transactions)

Retrieve full ledger audit trail of deposits, order debits, refunds, and commissions.

FieldTypeDescription
limitquery, optionalPage size, 1 to 100 (default 20).
pagequery, optionalPage number starting at 1.
{
  "success": true,
  "data": {
    "transactions": [
      {
        "id": "…uuid…",
        "amount": 24.00,
        "type": "debit",
        "purpose": "purchase",
        "reference": "shop-00123",
        "balance_before": 100.00,
        "balance_after": 76.00,
        "status": "completed",
        "created_at": "2026-09-17T12:00:00Z"
      }
    ],
    "total": 142,
    "page": 1,
    "limit": 20
  }
}
json

Special Offers

GET/api/v1/special-offers (or action: get_special_offers)

Returns active discounted promotional bundles and supplier specials.

Share Data

POST/api/v1/share-data (or action: share_data)

Transfers data balance from a sender number to a recipient number.

FieldTypeDescription
from_phonestring, requiredSender Ghana phone number.
to_phonestring, requiredRecipient Ghana phone number.
networkstring, requiredNetwork code (e.g. mtn, telecel, at).
volume_gbnumber, requiredData volume in GB to transfer.

Check an order

GET/api/public/v1/orders/{order_id_or_reference}

Accepts either our order_id or your own reference. Poll every 10–20 seconds while the status is processing; most orders settle within a minute.

{
  "success": true,
  "data": {
    "order_id": "…uuid…",
    "reference": "shop-2026-00123",
    "recipient": "0241234567",
    "amount": 24.00,
    "status": "processing",
    "refunded": false,
    "reason": null
  }
}
json

Idempotency & retries

reference is your idempotency key. Re-sending a purchase with the same reference returns the original order instead of charging twice — so on a timeout or a 500, retry with the exact same reference.

  • Generate the reference before the first attempt and persist it.
  • Never reuse a reference for a different recipient or product.
  • Alternatively send the header Idempotency-Key instead of the body field.

Rate limits

120 requests per minute per API key. Every response carries the current window state:

x-ratelimit-limit: 120
x-ratelimit-remaining: 118
x-ratelimit-reset: 1767225600
http

Over the limit you get 429 with retry_after in seconds. Back off exponentially; dealers can request a higher ceiling.

Error codes

Errors are never thrown as HTML. Switch on code, not on the message text.

FieldTypeDescription
unauthorized401API key missing, revoked, or the account is banned.
rate_limited429More than 120 requests in a minute. Check retry_after.
bad_request400Body was not valid JSON.
invalid_product400product_id missing or not a UUID.
invalid_recipient400Recipient is not a valid Ghana MSISDN.
invalid_reference400reference missing or not 8–128 characters.
order_rejected422Business rejection: insufficient balance, blacklisted number, inactive product.
not_found404No order matches that id or reference.
server_error500Unexpected failure. Safe to retry with the same reference.
{
  "success": false,
  "error": "Insufficient wallet balance",
  "code": "order_rejected"
}
json

Code samples

const API = "https://inkoomgh.com/api/public/v1";
const KEY = process.env.INKOOM_API_KEY;

async function inkoom(path, init = {}) {
  const res = await fetch(API + path, {
    ...init,
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json",
      ...(init.headers || {}),
    },
  });
  const body = await res.json();
  if (!body.success) throw new Error(`${body.code}: ${body.error}`);
  return body.data;
}

export async function sellBundle(productId, recipient, reference) {
  const order = await inkoom("/purchase", {
    method: "POST",
    body: JSON.stringify({ product_id: productId, recipient, reference }),
  });

  if (order.status !== "processing") return order;

  // poll until terminal
  for (let i = 0; i < 20; i++) {
    await new Promise((r) => setTimeout(r, 10_000));
    const check = await inkoom(`/orders/${reference}`);
    if (check.status !== "processing") return check;
  }
  return order;
}
javascript

Go-live checklist

  • Store the API key as a server-side environment variable, never in the client.
  • Persist your reference before calling /purchase.
  • Treat processing as an open order in your own system, and reconcile by polling /orders/{reference}.
  • Handle 429 with exponential backoff.
  • Alert yourself when the wallet balance drops below a day of sales.
  • Rotate keys from the dashboard if one leaks — revoking is instant.