Buying airtime and data bundles
Buy airtime with one POST to /purchases. For a data bundle, list the bundles first and buy one by its id. The response says whether the purchase succeeded, failed or is pending.
You need
- An API key with the Airtime API turned on under Subscribed APIs, on Settings › Developers › API Keys. See API keys and base URLs.
- Money on your balance: your test balance in the sandbox, or funds you added in live.
Buy airtime
curl -X POST $PONITOR_BASE_URL/airtime/v1/purchases \
-H "apiKey: $PONITOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"msisdn": "231880000000",
"amount": 5,
"externalId": "order-8842"
}'
| Field | What to send |
|---|---|
msisdn | An Orange or Lonestar Cell MTN number: 231880000000 or 0880000000. |
amount | The airtime the number gets, in US dollars. At least 1, and at most your team's limit per recipient ($50 unless agreed otherwise). |
externalId | Your own reference, unique within your team. See Retries and externalId. |
Or buy a data bundle
List the bundles on sale for the number's network:
curl "$PONITOR_BASE_URL/airtime/v1/products?carrier=LONESTAR" \
-H "apiKey: $PONITOR_API_KEY"
Then send the bundle's id as productId, instead of amount:
{
"msisdn": "231880000000",
"productId": "prod_p4nzx9xp335h",
"externalId": "order-8843"
}
You can send the bundle's code as productCode instead. See List data bundles.
Read the response
201 Created means Ponitor accepted the purchase. Check its status: it can still be FAILED.
{
"id": "6f1c2a3e-8b4d-4c1a-9e2f-3a4b5c6d7e8f",
"externalId": "order-8842",
"type": "AIRTIME",
"msisdn": "231880000000",
"carrier": "LONESTAR",
"product": null,
"amount": 5,
"cost": 5,
"status": "SUCCESS",
"failureReason": null,
"instructions": null,
"createdAt": "2026-10-20T19:40:00.000Z"
}
Check status. A FAILED purchase also returns 201, because the request was valid but the network declined it. Its failureReason says why, and the cost is refunded. 200 OK instead means you already used this externalId, and the body is the original purchase.
What happens to a purchase
| Status | Meaning | Your account |
|---|---|---|
SUCCESS | The airtime or bundle reached the number. | Charged the cost |
PENDING | The network accepted it but hasn't confirmed it. Ponitor checks for up to an hour. | cost held |
FAILED | The purchase didn't happen. failureReason says why. | Refunded in full |
To learn when a PENDING purchase settles, use a team webhook. It gets airtime.purchase.delivered or airtime.purchase.failed. Or call Get a purchase every 30 to 60 seconds. If a purchase is still PENDING after an hour, contact support with its id.
Why a purchase failed
failureReason tells you what went wrong, in words: a failed purchase has no error code. The three reasons that end in "Refunded." are Ponitor's and don't change. The network's wording can change, so don't match on it exactly.
| Problem with | failureReason | What to do |
|---|---|---|
| The number | The network's wording, such as Destination is barred, Destination is inactive, Daily limit exceeded or Invalid destination | Fix the number or amount. Send a new request with a new externalId. |
| The number already has the bundle | The network's wording, such as Already purchased | Don't retry. Choose a different bundle, or try again after the current bundle runs out. |
| The bundle | Product not available right now. Refunded. | The network refused the bundle. It's off sale while we check it. List the bundles again and choose another. |
| The network | Network temporarily unavailable. Refunded. | Try again in a few minutes. |
| Ponitor | Service temporarily unavailable. Refunded. | Try again later. We're alerted automatically. |
Ponitor refunds the cost of every failed purchase: in its answer, or when a PENDING purchase settles as FAILED.
When a request is refused
Ponitor refuses a request before charging anything or creating a purchase:
| Status | Why | Fix |
|---|---|---|
400 | It has amount and a bundle, or neither. Or productId and productCode name different bundles. | Send amount for airtime, or one bundle. |
400 | The bundle is unknown, withdrawn, or for the other network. | List the bundles again, and pick one for the number's network. |
400 | The amount or the bundle's price is over your team's limit per recipient. | Buy less, or ask us to raise the limit. |
400 | Your balance can't cover the cost. | Add funds under Billing. In the sandbox, set your test balance. |
401 | The key is wrong, or it doesn't match the base URL. | See API keys and base URLs. |
403 | The Airtime API isn't turned on, or your team isn't approved. | Check Subscribed APIs and Settings › Compliance. |
500 | Something failed on our side. Nothing was charged. | Send it again with the same externalId. |
Buy airtime or data lists each message.
Try it in the sandbox
With a sandbox key, every purchase is simulated and paid from your test balance:
| Buy for | What happens |
|---|---|
A number ending 0001, such as 231770000001 | It ends as FAILED, and the cost is refunded. |
A number ending 0002, such as 231770000002 | It stays PENDING. |
| Any other number | It ends as SUCCESS. |