For developers

Airtime API your engineers will actually like.

A REST API for sending Zimbabwean airtime, data, and ZESA tokens. Idempotency keys, HMAC-signed webhooks, multi-currency prepaid wallets, and a reconciler that resolves unknown states automatically. Built so you don't have to.

Get an API key → View status

Authentication

Every request carries an Authorization: Api-Key atk_… header. Get one from Settings → API keys in the dashboard.

Send a recharge

Post to POST /api/v1/orders. Pass agent_reference as your idempotency key — duplicate requests with the same value return the same order.

$ curl -X POST https://airtime.localhost.co.zw/api/v1/orders \
   -H "Authorization: Api-Key atk_…" \
   -H "Content-Type: application/json" \
   -d '{
        "product_id": 100,
        "target": "0772279099",
        "amount": 5,
        "agent_reference": "ord_2026-05-07_12345",
        "custom_sms": "Your monthly comms allowance from ACME."
      }'
import requests, uuid

resp = requests.post(
    "https://airtime.localhost.co.zw/api/v1/orders",
    headers={"Authorization": f"Api-Key {API_KEY}"},
    json={
        "product_id": 100,
        "target": "0772279099",
        "amount": 5,
        "agent_reference": "ord_" + uuid.uuid4().hex,
    },
    timeout=30,
)
resp.raise_for_status()
order = resp.json()
print(order["status"])  # "success" | "failed" | "unknown"
const resp = await fetch("https://airtime.localhost.co.zw/api/v1/orders", {
  method: "POST",
  headers: {
    "Authorization": `Api-Key ${process.env.API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    product_id: 100,
    target: "0772279099",
    amount: 5,
    agent_reference: crypto.randomUUID(),
  }),
});
const order = await resp.json();

Endpoints

GET/api/v1/products — catalogue (network, kind, currency, min/max).
POST/api/v1/orders — send a recharge. Idempotent on agent_reference.
GET/api/v1/orders/<agent_reference> — order status.
GET/api/v1/wallet — wallet balances.
POST/api/v1/wallet/topup — start a Pesepay top-up; returns redirect URL.
GET/api/v1/wallet/ledger — append-only ledger entries.
POST/api/v1/bulk/preview — validate a CSV before dispatch.
POST/api/v1/bulk — create + run a batch.
POST/api/v1/campaigns/<id>/payouts — survey-completion payout (HMAC-signed).

Webhook callbacks

When an order finalizes (success / failed / unknown), we POST a JSON payload to your tenant webhook URL with an HMAC-SHA256 signature in X-Airtime-Signature.

# Verify in Python:
import hmac, hashlib

raw = request.body
sig_header = request.headers["X-Airtime-Signature"]
expected = hmac.new(WEBHOOK_SECRET.encode(), raw, hashlib.sha256).hexdigest()
assert hmac.compare_digest(sig_header.replace("sha256=", ""), expected)

Failure semantics (read this once)

success

Provider confirmed. Wallet debited. provider_ref populated. Webhook fired.

failed → refunded

Provider rejected the recharge. Wallet auto-refunded. Status moves to refunded.

unknown

Network timeout / upstream hiccup. No refund. The reconciler runs every 2 minutes, queries the provider, and resolves to success or refunded failed. Plan retries around agent_reference, never re-issue from the client side.

Test mode

In DEBUG=True, every Pesepay top-up is forced to charge $0.01 so you can iterate without burning USD. Recharges still hit the live provider and consume wallet balance.

Create an API key → Building a survey reward flow?