Web Rabbit MediaDocs
Bills

Bills & Airtime

Currently unavailable — airtime, data bundles and bill payments are switched off.

!Currently unavailable
Airtime, data bundles and bill payments are switched off. Requests to POST /v1/bills return 410. This page is kept for reference in case the service returns.

Bills buys airtime, data bundles and utility bill payments straight from your settled Web Rabbit balance — the same balance you withdraw from. One endpoint covers the whole flow: list what is available, resolve an account name, buy, then poll or listen for the result.

iPaid from your settled balance
A purchase debits your settled balance at the moment it is accepted, so money already committed to a bill can never be withdrawn at the same time. We charge no platform fee on bills — fee is 0 and total equals amount. If the purchase fails, the full amount is returned to your balance automatically.

Endpoint

POST/v1/bills

Authenticate with your secret Payments key, exactly as on collections. The action field selects the operation; the mode (test or live) comes from the key.

FieldTypeDescription
catalogactionNetworks we support and the billers you can pay.
bundlesactionData bundles for one network, with ids and prices.
lookupactionResolve the account name behind a bill account number.
purchaseactionBuy airtime, a data bundle or pay a bill. Requires a write key.
statusactionRefresh one purchase from the provider by id or reference.
listactionPage through your purchase history, newest first. Default action.

Request fields

FieldTypeDescription
actionenum · requiredcatalog · bundles · lookup · purchase · status · list.
kindenumairtime, data or bill. Required for action=purchase.
amountnumberCedis. Required for action=purchase and must be greater than zero.
account_numberstringMobile number for airtime and data; the bill account number for kind=bill.
networkenumMTN · AT · TELECEL. Required for airtime, data and action=bundles. telco is accepted as an alias.
data_idstringBundle id taken from action=bundles. Required for kind=data.
biller_slugstringBiller identifier from action=catalog. Required for kind=bill and action=lookup.
account_namestringOptional label. Use the value returned by action=lookup where you have it.
narrationstringUp to 100 characters, stored on the purchase and shown in your history.
id · referencestringFor action=status. Pass either the purchase id or the reference we returned.
status · kindfilterFor action=list. Also accepts page (default 1) and limit (1–100, default 25).

Buy airtime

shell
curl -X POST https://api.webrabbitmedia.com/v1/bills \
  -H "Authorization: Bearer wr_test_..." \
  -H "Idempotency-Key: airtime-0240000000-2026-09-19" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "purchase",
    "kind": "airtime",
    "network": "MTN",
    "account_number": "0240000000",
    "amount": 5
  }'
200 OK
{
  "bill": {
    "id": "9f2a5f0e-6b51-4a4b-9a4f-2f0c6e1b7d30",
    "reference": "air_8FQ2K7X1",
    "kind": "airtime",
    "status": "pending",
    "amount": 5,
    "fee": 0,
    "total": 5,
    "currency": "GHS",
    "biller_slug": "mtn-airtime",
    "biller_name": "MTN Airtime",
    "account_number": "0240000000",
    "account_name": null,
    "bundle_name": null,
    "data_id": null,
    "narration": null,
    "mode": "live",
    "message": null,
    "created_at": "2026-09-19T08:12:04.000Z",
    "completed_at": null
  }
}

Buy a data bundle

Bundles are priced by the network, so read the list first and send the data_id and its price back unchanged.

shell
# 1. list bundles
curl -X POST https://api.webrabbitmedia.com/v1/bills \
  -H "Authorization: Bearer wr_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "action": "bundles", "network": "MTN" }'

# 2. buy one
curl -X POST https://api.webrabbitmedia.com/v1/bills \
  -H "Authorization: Bearer wr_live_..." \
  -H "Idempotency-Key: data-0240000000-2026-09-19" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "purchase",
    "kind": "data",
    "network": "MTN",
    "account_number": "0240000000",
    "data_id": "mtn-1gb-30d",
    "bundle_name": "1GB · 30 days",
    "amount": 12
  }'

Pay a bill

Take the biller_slug from action: "catalog", confirm the customer with action: "lookup", then purchase. A lookup returns { "ok": false, "reason": "..." } with HTTP 200 when the account cannot be resolved — check ok, not the status code.

shell
curl -X POST https://api.webrabbitmedia.com/v1/bills \
  -H "Authorization: Bearer wr_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "action": "lookup", "biller_slug": "ecg-postpaid", "account_number": "0200123456" }'

curl -X POST https://api.webrabbitmedia.com/v1/bills \
  -H "Authorization: Bearer wr_live_..." \
  -H "Idempotency-Key: ecg-0200123456-sep" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "purchase",
    "kind": "bill",
    "biller_slug": "ecg-postpaid",
    "account_number": "0200123456",
    "account_name": "Kwame Mensah",
    "phone_number": "0240000000",
    "amount": 150,
    "narration": "September electricity"
  }'

Statuses and settlement

!200 means accepted, not delivered
Every purchase starts pending. The network confirms delivery seconds to minutes later. Never treat the create response as proof that airtime landed — wait for approved.
FieldTypeDescription
pendingstatusAccepted and debited from your balance. Awaiting the network.
approvedstatusDelivered. completed_at is set and bill.completed fires.
failedstatusNot delivered. The full amount is refunded to your balance and bill.failed fires.

Refresh a single purchase at any time — this pulls a fresh answer from the provider rather than reading a cache:

shell
curl -X POST https://api.webrabbitmedia.com/v1/bills \
  -H "Authorization: Bearer wr_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "action": "status", "reference": "air_8FQ2K7X1" }'

History

action: "list" returns bills, total, page and limit, newest first, scoped to the key's mode. Filter with kind and status. The same history is in your dashboard under Bills → History, with CSV export.

Idempotency

Send an Idempotency-Key header on every purchase. Replaying the same key with the same body returns the original purchase with idempotent-replayed: true; reusing it with a different body returns 409 idempotency_conflict. See Idempotency.

Errors

FieldTypeDescription
400 invalid_requesterrorMissing or unusable kind, amount, network, data_id or biller_slug.
402 insufficient_balanceerrorYour settled balance does not cover the purchase. Nothing was bought.
403 insufficient_scopeerrorRead-only key, or the business is not approved for live mode.
404 not_founderrorUnknown id or reference on action=status.
409 idempotency_conflicterrorIdempotency-Key reused with a different body, or still in flight.
429 rate_limitederrorToo many requests. Back off and retry — see Rate limits.
502 provider_errorerrorThe provider rejected the purchase. Your balance is refunded in full.

Test mode

A test key runs entirely on our simulator: no provider is contacted, no airtime is sent and no real money moves. Send GHS 2.00 to force a failed purchase and a refund; any other positive amount settles as pending and then approved. See Test data.

Webhook events

FieldTypeDescription
bill.completedeventThe purchase was delivered. Payload carries the full bill object.
bill.failedeventThe purchase failed and your balance was refunded in full.

Both events are signed and retried like every other event — see Webhooks.