Sandbox¶
Every sandbox endpoint is a simulation. It never reaches a provider, never charges a wallet, and never moves real money — but the request validation, the response shapes and the error semantics are the same as the live API, so you can build the whole integration against it.
The sandbox is where the developer portal runs its interactive playground.
Authentication¶
Sandbox endpoints use the sandbox key — a separate credential from your
production sk_test_ key. It does not interoperate with the production key
store, and it only authenticates the /api/v1/sandbox/* routes.
Both header styles work:
The sandbox key is bound to your agent identity. If the linked account is
suspended or its email becomes unverified, sandbox calls start returning
403.
The scenario field¶
Every sandbox purchase accepts an optional scenario field that forces a
specific outcome. This is how you test your error handling without waiting for
a real provider to fail.
scenario |
Result | Available on |
|---|---|---|
success (default) |
HTTP 200, code: 200 |
All |
failure |
HTTP 400, code: 400 |
All |
pending |
HTTP 300, code: 300 |
Airtime only |
invalid_meter |
HTTP 400, invalid meter error |
Electricity only |
curl -X POST https://sandbox.instapaybill.com/api/v1/sandbox/airtime \
-H "Authorization: Bearer $IPB_SANDBOX_KEY" \
-H "Content-Type: application/json" \
-d '{"phone":"08012345678","amount":500,"network":"MTN","scenario":"failure"}'
{
"code": 400,
"reference": "SBTEST-ABCD1234EF",
"error": [
{
"path": "airtime-sandbox",
"msg": "Transaction failed: phone number not eligible for this network"
}
]
}
Airtime¶
curl -X POST https://sandbox.instapaybill.com/api/v1/sandbox/airtime \
-H "Authorization: Bearer $IPB_SANDBOX_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "08012345678",
"amount": 500,
"network": "MTN",
"scenario": "success"
}'
| Field | Type | Required |
|---|---|---|
phone |
string | ✅ |
amount |
number | ✅ |
network |
string | ✅ |
scenario |
string | — |
{
"msg": "MTN Airtime Purchase of ₦500 for 08012345678 was successful",
"data": {
"code": 200,
"reference": "SBTEST-ABCD1234EF",
"description": "MTN Airtime Purchase",
"message": "Top-up of ₦500 to 08012345678 delivered successfully.",
"type": "airtime"
},
"code": 200
}
Data¶
curl -X POST https://sandbox.instapaybill.com/api/v1/sandbox/data \
-H "Authorization: Bearer $IPB_SANDBOX_KEY" \
-H "Content-Type: application/json" \
-d '{"phone":"08012345678","planId":"MTN-1.5GB-30D","network":"MTN"}'
| Field | Type | Required |
|---|---|---|
phone |
string | ✅ |
planId |
string | ✅ |
network |
string | ✅ |
scenario |
string | — |
{
"msg": "MTN Data Purchase for 08012345678 was successful",
"data": {
"code": 200,
"reference": "SBTEST-ABCD1234EF",
"description": "MTN Data Purchase",
"message": "Data bundle activated on 08012345678. Valid for 30 days.",
"type": "data"
},
"code": 200
}
Electricity¶
Validate¶
curl -X POST https://sandbox.instapaybill.com/api/v1/sandbox/electricity/validate \
-H "Authorization: Bearer $IPB_SANDBOX_KEY" \
-H "Content-Type: application/json" \
-d '{"meterNo":"09012345678","disco":"EEDC","type":"prepaid"}'
| Field | Type | Required |
|---|---|---|
meterNo |
string | ✅ |
disco |
string | ✅ |
type |
string | — |
scenario |
string | — |
{
"msg": "Meter validated successfully",
"data": {
"customerName": "JOHN SANDBOX",
"customerAddress": "12 Test Street, Lagos",
"accountStatus": "active",
"meterNo": "09012345678",
"disco": "EEDC",
"type": "prepaid"
}
}
With scenario: "failure" the endpoint returns HTTP 400 with
"Meter number not found on this disco".
Buy¶
curl -X POST https://sandbox.instapaybill.com/api/v1/sandbox/electricity \
-H "Authorization: Bearer $IPB_SANDBOX_KEY" \
-H "Content-Type: application/json" \
-d '{"meterNo":"09012345678","disco":"EEDC","type":"prepaid","amount":5000,"phoneNumber":"08012345678"}'
| Field | Type | Required |
|---|---|---|
meterNo |
string | ✅ |
disco |
string | ✅ |
type |
string | — |
amount |
number | ✅ |
phoneNumber |
string | ✅ |
scenario |
string | — |
{
"msg": "Electricity purchase of ₦5,000 successful",
"data": {
"code": 200,
"reference": "SBTEST-ABCD1234EF",
"description": "EEDC prepaid Electricity Purchase",
"message": "Units: 66.67 kWh\nToken: 1234-5678-9012\nCustomer Name: JOHN SANDBOX\nCustomer Address: 12 Test Street, Lagos",
"token": "1234-5678-9012",
"units": "66.67",
"customerName": "JOHN SANDBOX",
"type": "electricity"
},
"code": 200
}
units here, unit in the live API
The sandbox returns data.units; the live API returns data.unit. The
sandbox webhook mirrors the sandbox shape.
Read both fields if your client shares code between the two.
With scenario: "invalid_meter":
{
"code": 400,
"error": [
{ "path": "meterNo", "msg": "Invalid meter number for selected DISCO" }
]
}
Cable TV¶
Validate¶
curl -X POST https://sandbox.instapaybill.com/api/v1/sandbox/cables/validate \
-H "Authorization: Bearer $IPB_SANDBOX_KEY" \
-H "Content-Type: application/json" \
-d '{"smartCardNo":"1234567890","type":"DSTV"}'
{
"msg": "Smart card validated successfully",
"data": {
"customerName": "ADEBAYO SANDBOX",
"accountStatus": "active",
"currentBouquet": "DSTV Compact",
"iucNumber": "1234567890",
"cableType": "DSTV",
"minimumPayable": 9000,
"dueDate": "2026-11-18"
}
}
Buy¶
curl -X POST https://sandbox.instapaybill.com/api/v1/sandbox/cables \
-H "Authorization: Bearer $IPB_SANDBOX_KEY" \
-H "Content-Type: application/json" \
-d '{"smartCardNo":"1234567890","type":"DSTV"}'
{
"msg": "DSTV subscription renewed successfully",
"data": {
"code": 200,
"reference": "SBTEST-ABCD1234EF",
"description": "DSTV Cable Subscription",
"message": "DSTV plan renewed on 1234567890. Customer: ADEBAYO SANDBOX.",
"type": "cables"
},
"code": 200
}
Education¶
curl -X POST https://sandbox.instapaybill.com/api/v1/sandbox/education \
-H "Authorization: Bearer $IPB_SANDBOX_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"WAEC","numberOfPin":2}'
| Field | Type | Required |
|---|---|---|
type |
string | ✅ |
numberOfPin |
number | — (defaults to 1, capped at 10) |
scenario |
string | — |
{
"msg": "WAEC Result Checker PIN(s) generated successfully",
"data": {
"code": 200,
"reference": "SBTEST-ABCD1234EF",
"description": "WAEC Result Checker - 2 PIN",
"message": "1. Serial: WAE1234567890 / PIN: 1234-5678-9012\n2. Serial: WAE9876543210 / PIN: 9876-5432-1098",
"pins": [
{ "serial": "WAE1234567890", "pin": "1234-5678-9012" },
{ "serial": "WAE9876543210", "pin": "9876-5432-1098" }
],
"type": "education"
},
"code": 200
}
The sandbox pins array is the shape to code against
The live API's data.data_pin comes straight from the provider and its
shape varies between providers. The sandbox normalises it to a pins
array of { serial, pin }. Write your client against that shape and make
the live path tolerant.
Startimes¶
curl -X POST https://sandbox.instapaybill.com/api/v1/sandbox/startimes \
-H "Authorization: Bearer $IPB_SANDBOX_KEY" \
-H "Content-Type: application/json" \
-d '{"smartCardNo":"1234567890","planId":"bouquet-1","phone":"08012345678"}'
| Field | Type | Required |
|---|---|---|
smartCardNo |
string | ✅ |
planId |
string | ✅ |
phone |
string | ✅ |
scenario |
string | — |
{
"msg": "Startimes subscription purchased successfully",
"data": {
"code": 200,
"reference": "SBTEST-ABCD1234EF",
"description": "Startimes Bouquet-1 Subscription",
"message": "Startimes bouquet-1 plan activated on 1234567890. Customer: SANDBOX USER.",
"type": "startimes"
},
"code": 200
}
Sandbox transactions¶
The sandbox records every simulated transaction. These two routes are
session-protected — they require the developer portal cookie and the exact
portal Origin header, not the sandbox key.
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/sandbox/transactions |
List. page (≥1), limit (≤50). |
GET |
/api/v1/sandbox/transactions/{id} |
Single transaction by Mongo id. |
Sandbox key and stats¶
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/sandbox/keys |
The sandbox key, webhook URL, delivery status and counters |
GET |
/api/v1/sandbox/stats |
Request counters |
POST |
/api/v1/sandbox/keys/regenerate |
Rotate the sandbox key |
POST |
/api/v1/sandbox/keys/webhook |
Set the sandbox webhook URL |
{
"msg": "Keys retrieved",
"data": {
"sk_test": "sk_test_…",
"webhookUrl": "https://your-server.com/webhooks/instapaybill",
"webhookLastDeliveredAt": "2026-10-19T12:00:00.000Z",
"webhookLastStatus": 200,
"stats": {
"totalRequests": 42,
"successful": 38,
"failed": 4,
"walletBalance": 100000
}
}
}
The wallet starts with ₦100,000 of virtual funds. Each successful sandbox
API purchase reduces walletBalance; failure and pending outcomes leave it
unchanged. Purchases above the remaining balance fail with 400 and
Insufficient sandbox wallet balance. Balances persist across sessions and
include previously saved successful purchases. No real wallet is charged.
Successful purchase responses include data.amount and data.walletBalance.
Transaction records include amount, amountMinor (the deduction in kobo),
balanceBeforeMinor and balanceAfterMinor. Failed and pending records have
amountMinor: 0. The ledger and balance deduction are saved in one atomic
insert, so concurrent requests cannot overspend or lose a debit.
| Service | Simulated price |
|---|---|
| Airtime | Requested amount |
| Electricity | Requested amount |
| Data | ₦500 per request |
| Cable TV | ₦9,000 per request |
| Education | ₦3,700 per PIN (1–10 PINs) |
| Startimes | ₦1,500 per request |
These fixed simulator prices do not use the production plan catalogue. Amounts must be positive, with at most two decimal places. A request's plan ID does not change these simulated prices.
The playground defaults to Sandbox API call, which saves transactions and updates the virtual balance. Simulated (instant) is a browser-only response preview; it does not save a transaction or change the wallet.
Sandbox vs live — what differs¶
| Sandbox | Live | |
|---|---|---|
| Base path | /api/v1/sandbox/* |
/api/v1/* |
| Key | Sandbox key (sk_test_ + 64 hex) |
Live key (sk_live_ + 24 alnum) |
| Providers | Never contacted | Real providers |
| Wallet | Starts at ₦100,000; successful purchases deduct virtual funds | Four real buckets |
| Response shape | Normalised per service | Provider-dependent in places |
| Forced outcomes | scenario field |
Not available |
| Webhook signature | None | x-webhook-signature (HMAC-SHA512) |
| Webhook header | X-InstaPayBill-Sandbox: 1 |
none |
| KYC / account guards | Active, verified agent required; production KYC not simulated | Applied |
| Duplicate guard | Not applied | Applied |
Build in the sandbox, verify in production
The sandbox checks virtual wallet funds but does not run production KYC, biller health or duplicate-purchase guards. Agent eligibility is checked for sandbox access. Those are the failures your production integration will actually hit, so test them against production with a small live amount before going live.
Related¶
- Quickstart — first call
- Webhooks — sandbox event format
- Errors — the error contract