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.
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
/api/v1/products — catalogue (network, kind, currency, min/max)./api/v1/orders — send a recharge. Idempotent on agent_reference./api/v1/orders/<agent_reference> — order status./api/v1/wallet — wallet balances./api/v1/wallet/topup — start a Pesepay top-up; returns redirect URL./api/v1/wallet/ledger — append-only ledger entries./api/v1/bulk/preview — validate a CSV before dispatch./api/v1/bulk — create + run a batch./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.