Skip to content

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.

export IPB_SANDBOX_KEY="sk_test_..."

Both header styles work:

-H "Authorization: Bearer $IPB_SANDBOX_KEY"
-H "X-API-Key: $IPB_SANDBOX_KEY"

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

POST/api/v1/sandbox/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

POST/api/v1/sandbox/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

POST/api/v1/sandbox/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

POST/api/v1/sandbox/electricity
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

POST/api/v1/sandbox/cables/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

POST/api/v1/sandbox/cables
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

POST/api/v1/sandbox/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

POST/api/v1/sandbox/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.