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.
fee is 0 and total equals amount. If the purchase fails, the full amount is returned to your balance automatically.Endpoint
/v1/billsAuthenticate with your secret Payments key, exactly as on collections. The action field selects the operation; the mode (test or live) comes from the key.
| Field | Type | Description |
|---|---|---|
catalog | action | Networks we support and the billers you can pay. |
bundles | action | Data bundles for one network, with ids and prices. |
lookup | action | Resolve the account name behind a bill account number. |
purchase | action | Buy airtime, a data bundle or pay a bill. Requires a write key. |
status | action | Refresh one purchase from the provider by id or reference. |
list | action | Page through your purchase history, newest first. Default action. |
Request fields
| Field | Type | Description |
|---|---|---|
action | enum · required | catalog · bundles · lookup · purchase · status · list. |
kind | enum | airtime, data or bill. Required for action=purchase. |
amount | number | Cedis. Required for action=purchase and must be greater than zero. |
account_number | string | Mobile number for airtime and data; the bill account number for kind=bill. |
network | enum | MTN · AT · TELECEL. Required for airtime, data and action=bundles. telco is accepted as an alias. |
data_id | string | Bundle id taken from action=bundles. Required for kind=data. |
biller_slug | string | Biller identifier from action=catalog. Required for kind=bill and action=lookup. |
account_name | string | Optional label. Use the value returned by action=lookup where you have it. |
narration | string | Up to 100 characters, stored on the purchase and shown in your history. |
id · reference | string | For action=status. Pass either the purchase id or the reference we returned. |
status · kind | filter | For action=list. Also accepts page (default 1) and limit (1–100, default 25). |
Buy airtime
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
}'{
"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.
# 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.
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
pending. The network confirms delivery seconds to minutes later. Never treat the create response as proof that airtime landed — wait for approved.| Field | Type | Description |
|---|---|---|
pending | status | Accepted and debited from your balance. Awaiting the network. |
approved | status | Delivered. completed_at is set and bill.completed fires. |
failed | status | Not 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:
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
| Field | Type | Description |
|---|---|---|
400 invalid_request | error | Missing or unusable kind, amount, network, data_id or biller_slug. |
402 insufficient_balance | error | Your settled balance does not cover the purchase. Nothing was bought. |
403 insufficient_scope | error | Read-only key, or the business is not approved for live mode. |
404 not_found | error | Unknown id or reference on action=status. |
409 idempotency_conflict | error | Idempotency-Key reused with a different body, or still in flight. |
429 rate_limited | error | Too many requests. Back off and retry — see Rate limits. |
502 provider_error | error | The 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
| Field | Type | Description |
|---|---|---|
bill.completed | event | The purchase was delivered. Payload carries the full bill object. |
bill.failed | event | The purchase failed and your balance was refunded in full. |
Both events are signed and retried like every other event — see Webhooks.
