v2.0.0Latest Release

API Documentation

Embed Topteller wallet-powered recharge and bill-payment services into your website, app, ERP, or merchant portal.

Open Dashboard https://vtu.paradigmlimited.org/topteller/api/v1

Authentication

Authenticate requests using your API key pair or a JWT token for first-party flows.

🔐

API keys

Recommended for server-to-server integrations.

HeaderRequiredDescription
x-api-key Required Your public integration key.
x-secret-key Required Your secret integration key. Never expose this in client apps.
idempotency-key or x-idempotency-key Optional Strongly recommended for all purchase and payment-initiation requests to prevent duplicate debits.
X-Transaction-Pin or X-Pin Optional Required for JWT purchases and only required for API-key purchases when the merchant PIN policy is enforced.
đŸĒ™

Platform payment keys

Used by approved merchant websites to create hosted Topteller Pay checkout requests.

HeaderRequiredDescription
x-platform-api-key Required Your merchant platform public key. x-api-key is also accepted for compatibility.
x-platform-api-secret Optional Your merchant platform secret when secret enforcement is enabled for your account.
idempotency-key or x-idempotency-key Optional Recommended when initiating payment requests.
đŸĒ™

JWT

Useful for first-party flows like API key management inside the app.

HeaderRequiredDescription
Authorization Required Bearer <token>
âš ī¸ Never expose your x-secret-key in client-side code, browser JavaScript, or mobile app bundles. Store it server-side only.

Developer Eligibility

API access is gated to ensure wallet integrity and responsible vending.

Live API credentials are available to Diamond plan members inside the Topteller app.

Required Plan 💎 Diamond

Quickstart

Follow these steps in order. Skipping service discovery is the most common cause of vending failures.

1

Upgrade the account to Diamond inside the Topteller app.

2

Open API Integration in-app, generate your credentials, and store the secret key securely.

3

Call the discovery endpoints first to fetch live service IDs and plan IDs.

4

Fund your Topteller wallet, then send purchase requests with an idempotency key.

5

Handle synchronous responses and optional webhook notifications for status updates.

Important Notes

Read these before going to production — they encode the most common integration mistakes.

💡 Never ship your secret key in frontend apps, browser JavaScript, or mobile APKs.
💡 Service IDs and plan IDs can change as providers update their catalogue. Always fetch them dynamically.
💡 Use serviceId from /api/services for airtime, electricity, exam PINs, and airtime PINs. Use planId from /api/services/:id/plans for data and TV.
💡 For API-key purchases, transaction PIN may be optional depending on the merchant policy. If required, send pin in the JSON body or X-Transaction-Pin/X-Pin header.
💡 Amounts may be sent as JSON numbers or numeric strings. Invalid or missing required fields return a 400 response before provider processing.
💡 Transactions can return pending while provider verification continues in the background. Requery when needed.
💡 Wallet, discovery, and transaction monitoring endpoints are stable. Purchase endpoints reflect the current developer-controller behavior and should be validated end-to-end in your environment before full rollout.

Credentials and Wallet

Manage API credentials and inspect the wallet funding source for your integration.

POST /api-keys/generate
JWT Live

Create a new API key pair for the signed-in Diamond user.

GET /api-keys
JWT Live

Fetch the active credential and metadata.

PUT /api-keys/:id
JWT Live

Update webhook URL, name, or rate limit.

POST /api-keys/:id/regenerate
JWT Live

Rotate your public and secret keys.

POST /api-keys/:id/revoke
JWT Live

Deactivate the current key pair.

GET /api-keys/balance
API key or JWT Live

Check available wallet balance in NGN.

GET /api-keys/wallet-info
API key or JWT Live

Get balance, user level, and reserved account details.

Service Discovery

Use these before vending so you always work with live IDs from your current provider catalogue.

GET /api/services
Public Live

Return the active services that can be integrated.

GET /api/services/categories
Public Live

Return categories for grouping service UI.

GET /api/services/:id
Optional auth Live

Return a service and current user-aware pricing where available.

GET /api/services/:id/plans
Public Live

Return live plan IDs and provider plan codes for data/TV services.

Topteller Pay Checkout

Create hosted checkout requests so a Topteller user can securely approve payment on Topteller-owned screens.

POST /external-payment/initiate
Platform payment key Live

Create a payment request and return checkoutUrl, checkoutToken, sdkUrl, fee, and totalAmount.

body:payerPhoneamountdescriptionlistingReference (optional)
GET /external-payment/status/:reference
Platform payment key Live

Fetch a payment request by reference for the authenticated merchant platform.

GET /pay/topteller-pay.js
Public Live

Browser SDK for opening the hosted Topteller checkout URL.

POST /external-payment/approve-offline
Platform payment key Live

Deprecated endpoint. Always returns 410 because PIN collection must happen only on Topteller.

Purchases

Debit the integration wallet to vend services for your customers.

POST /purchase/airtime
API key or JWT Beta

Top up airtime to a Nigerian phone number.

body:serviceIdphoneNumberamountpin (conditional)
POST /purchase/airtime-pin
API key or JWT Beta

Buy airtime PINs from a live PIN service.

body:serviceIdamountquantitypin (conditional)
POST /purchase/data
API key or JWT Beta

Vend a data plan using a live plan ID.

body:planIdphoneNumberpin (conditional)
POST /purchase/tv
API key or JWT Beta

Subscribe a decoder using a live plan ID.

body:planIdsmartCardNumberphoneNumberpin (conditional)
POST /purchase/electricity
API key or JWT Beta

Vend prepaid or postpaid electricity units.

body:serviceIdmeterNumbertypeamountphoneNumberpin (conditional)
POST /purchase/epin
API key or JWT Beta

Buy WAEC/NECO/JAMB-style exam PINs.

body:serviceIdquantityemailpin (conditional)
POST /purchase/sms/bulk
API key or JWT Beta

Send bulk SMS after wallet debit.

body:recipientsmessagesenderIdpin (conditional)

Verification and Monitoring

Verify customer accounts before debit and monitor delivery after purchase.

POST /purchase/tv/verify
API key or JWT Beta

Validate a decoder account before subscribing.

body:smartCardNumberserviceType
POST /purchase/electricity/verify
API key or JWT Beta

Validate meter details before purchase.

body:serviceIdmeterNumbertype
GET /api/transactions
API key or JWT Live

Fetch integration transaction history.

GET /api/transactions/:id
API key or JWT Live

Inspect a single transaction.

POST /api/transactions/:id/requery
API key or JWT Live

Trigger a fresh provider verification for a pending transaction.

GET /api/transactions/stats/summary
API key or JWT Live

Get aggregate transaction statistics.

Webhooks

Set a webhook URL on your API key to receive purchase status notifications from Topteller.

â„šī¸ Webhooks are delivered via POST with application/json. Your endpoint must respond within 5s or the delivery will be retried.

Supported Events

airtime_purchaseairtime_pin_purchasedata_purchasetv_subscriptionelectricity_purchaseepin_purchasebulk_smspayment.completedpayment.failedpayment.expiredpayment.rejected
Example Webhook Payload
{
  "event": "data_purchase",
  "reference": "DATA-123456789",
  "status": "completed",
  "phoneNumber": "08012345678",
  "plan": "MTN 2GB SME",
  "amount": 1200,
  "finalPrice": 1188,
  "timestamp": "2026-08-01T17:38:13.937Z"
}

Response Format

All endpoints return JSON. Check the success boolean first, then branch on message for error details.

✓ Success

200 OK
{
  "success": true,
  "message": "Request processed successfully",
  "data": {}
}

✕ Error

4xx / 5xx
{
  "success": false,
  "message": "Readable error description"
}

Code Samples

Ready-to-use snippets for the most common integration paths. Replace placeholder keys with your real credentials.

Check wallet balance

Confirm that your integration wallet is funded before making debits.

cURL
curl --request GET "https://vtu.paradigmlimited.org/topteller/api/v1/api-keys/balance" \
  --header "x-api-key: topt_live_xxxxxxxxxxxxxxxx" \
  --header "x-secret-key: tsk_live_xxxxxxxxxxxxxxxx"
JavaScript
import axios from "axios";

const response = await axios.get("https://vtu.paradigmlimited.org/topteller/api/v1/api-keys/balance", {
  headers: {
    "x-api-key": "topt_live_xxxxxxxxxxxxxxxx",
    "x-secret-key": "tsk_live_xxxxxxxxxxxxxxxx"
  }
});

console.log(response.data);
Python
import requests

response = requests.get(
    "https://vtu.paradigmlimited.org/topteller/api/v1/api-keys/balance",
    headers={
        "x-api-key": "topt_live_xxxxxxxxxxxxxxxx",
        "x-secret-key": "tsk_live_xxxxxxxxxxxxxxxx"
    },
    timeout=30,
)

print(response.json())
PHP
<?php
$curl = curl_init("https://vtu.paradigmlimited.org/topteller/api/v1/api-keys/balance");

curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "x-api-key: topt_live_xxxxxxxxxxxxxxxx",
        "x-secret-key: tsk_live_xxxxxxxxxxxxxxxx"
    ],
]);

$response = curl_exec($curl);
curl_close($curl);

echo $response;
Kotlin
val request = Request.Builder()
    .url("https://vtu.paradigmlimited.org/topteller/api/v1/api-keys/balance")
    .get()
    .addHeader("x-api-key", "topt_live_xxxxxxxxxxxxxxxx")
    .addHeader("x-secret-key", "tsk_live_xxxxxxxxxxxxxxxx")
    .build()

OkHttpClient().newCall(request).execute().use { response ->
    println(response.body?.string())
}

Discover services and plans

Fetch live service IDs and plan IDs before vending.

cURL
curl --request GET "https://vtu.paradigmlimited.org/topteller/api/v1/api/services"
JavaScript
const services = await axios.get("https://vtu.paradigmlimited.org/topteller/api/v1/api/services");
const plans = await axios.get("https://vtu.paradigmlimited.org/topteller/api/v1/api/services/10/plans");

console.log({ services: services.data, plans: plans.data });
Python
services = requests.get("https://vtu.paradigmlimited.org/topteller/api/v1/api/services", timeout=30).json()
plans = requests.get("https://vtu.paradigmlimited.org/topteller/api/v1/api/services/10/plans", timeout=30).json()

print(services)
print(plans)
PHP
<?php
$services = file_get_contents("https://vtu.paradigmlimited.org/topteller/api/v1/api/services");
$plans = file_get_contents("https://vtu.paradigmlimited.org/topteller/api/v1/api/services/10/plans");

echo $services;
echo $plans;
Kotlin
suspend fun fetchServices(client: OkHttpClient) {
    val servicesRequest = Request.Builder()
        .url("https://vtu.paradigmlimited.org/topteller/api/v1/api/services")
        .build()

    val plansRequest = Request.Builder()
        .url("https://vtu.paradigmlimited.org/topteller/api/v1/api/services/10/plans")
        .build()

    client.newCall(servicesRequest).execute().use { println(it.body?.string()) }
    client.newCall(plansRequest).execute().use { println(it.body?.string()) }
}

Purchase airtime

Send a debit request from your funded Topteller wallet. Always use an idempotency key.

cURL
curl --request POST "https://vtu.paradigmlimited.org/topteller/api/v1/purchase/airtime" \
  --header "Content-Type: application/json" \
  --header "x-api-key: topt_live_xxxxxxxxxxxxxxxx" \
  --header "x-secret-key: tsk_live_xxxxxxxxxxxxxxxx" \
  --header "x-idempotency-key: airtime-order-10001" \
  --data '{
    "serviceId": 1,
    "phoneNumber": "08012345678",
    "amount": 1000,
    "pin": "1234"
  }'
JavaScript
const response = await axios.post(
  "https://vtu.paradigmlimited.org/topteller/api/v1/purchase/airtime",
  {
    serviceId: 1,
    phoneNumber: "08012345678",
    amount: 1000,
    pin: "1234"
  },
  {
    headers: {
      "Content-Type": "application/json",
      "x-api-key": "topt_live_xxxxxxxxxxxxxxxx",
      "x-secret-key": "tsk_live_xxxxxxxxxxxxxxxx",
      "x-idempotency-key": "airtime-order-10001"
    }
  }
);

console.log(response.data);
Python
payload = {
    "serviceId": 1,
    "phoneNumber": "08012345678",
    "amount": 1000,
    "pin": "1234",
}

response = requests.post(
    "https://vtu.paradigmlimited.org/topteller/api/v1/purchase/airtime",
    json=payload,
    headers={
        "x-api-key": "topt_live_xxxxxxxxxxxxxxxx",
        "x-secret-key": "tsk_live_xxxxxxxxxxxxxxxx",
        "x-idempotency-key": "airtime-order-10001",
    },
    timeout=30,
)

print(response.json())
PHP
<?php
$payload = json_encode([
    "serviceId" => 1,
    "phoneNumber" => "08012345678",
    "amount" => 1000,
    "pin" => "1234",
]);

$curl = curl_init("https://vtu.paradigmlimited.org/topteller/api/v1/purchase/airtime");
curl_setopt_array($curl, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "x-api-key: topt_live_xxxxxxxxxxxxxxxx",
        "x-secret-key: tsk_live_xxxxxxxxxxxxxxxx",
        "x-idempotency-key: airtime-order-10001",
    ],
]);

$response = curl_exec($curl);
curl_close($curl);

echo $response;
Kotlin
val payload = """
    {
      "serviceId": 1,
      "phoneNumber": "08012345678",
      "amount": 1000,
      "pin": "1234"
    }
""".trimIndent()

val request = Request.Builder()
    .url("https://vtu.paradigmlimited.org/topteller/api/v1/purchase/airtime")
    .post(payload.toRequestBody("application/json".toMediaType()))
    .addHeader("x-api-key", "topt_live_xxxxxxxxxxxxxxxx")
    .addHeader("x-secret-key", "tsk_live_xxxxxxxxxxxxxxxx")
    .addHeader("x-idempotency-key", "airtime-order-10001")
    .build()

OkHttpClient().newCall(request).execute().use { response ->
    println(response.body?.string())
}

Requery a transaction

Use this when a purchase is still pending and you need a fresh provider check.

cURL
curl --request POST "https://vtu.paradigmlimited.org/topteller/api/v1/api/transactions/248/requery" \
  --header "x-api-key: topt_live_xxxxxxxxxxxxxxxx" \
  --header "x-secret-key: tsk_live_xxxxxxxxxxxxxxxx"
JavaScript
const response = await axios.post(
  "https://vtu.paradigmlimited.org/topteller/api/v1/api/transactions/248/requery",
  {},
  {
    headers: {
      "x-api-key": "topt_live_xxxxxxxxxxxxxxxx",
      "x-secret-key": "tsk_live_xxxxxxxxxxxxxxxx"
    }
  }
);

console.log(response.data);
Python
response = requests.post(
    "https://vtu.paradigmlimited.org/topteller/api/v1/api/transactions/248/requery",
    headers={
        "x-api-key": "topt_live_xxxxxxxxxxxxxxxx",
        "x-secret-key": "tsk_live_xxxxxxxxxxxxxxxx",
    },
    timeout=30,
)

print(response.json())
PHP
<?php
$curl = curl_init("https://vtu.paradigmlimited.org/topteller/api/v1/api/transactions/248/requery");

curl_setopt_array($curl, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "x-api-key: topt_live_xxxxxxxxxxxxxxxx",
        "x-secret-key: tsk_live_xxxxxxxxxxxxxxxx"
    ],
]);

$response = curl_exec($curl);
curl_close($curl);

echo $response;
Kotlin
val request = Request.Builder()
    .url("https://vtu.paradigmlimited.org/topteller/api/v1/api/transactions/248/requery")
    .post("{}".toRequestBody("application/json".toMediaType()))
    .addHeader("x-api-key", "topt_live_xxxxxxxxxxxxxxxx")
    .addHeader("x-secret-key", "tsk_live_xxxxxxxxxxxxxxxx")
    .build()

OkHttpClient().newCall(request).execute().use { response ->
    println(response.body?.string())
}

Initiate hosted checkout

Create a Topteller Pay request and send your customer to checkoutUrl for secure approval.

cURL
curl --request POST "https://vtu.paradigmlimited.org/topteller/api/v1/external-payment/initiate" \
  --header "Content-Type: application/json" \
  --header "x-platform-api-key: tk_live_xxxxxxxxxxxxxxxx" \
  --header "idempotency-key: checkout-order-10001" \
  --data '{
    "payerPhone": "08012345678",
    "amount": 2500,
    "description": "Order #10001",
    "listingReference": "LIST-88"
  }'
JavaScript
const response = await axios.post(
  "https://vtu.paradigmlimited.org/topteller/api/v1/external-payment/initiate",
  {
    payerPhone: "08012345678",
    amount: 2500,
    description: "Order #10001",
    listingReference: "LIST-88"
  },
  {
    headers: {
      "Content-Type": "application/json",
      "x-platform-api-key": "tk_live_xxxxxxxxxxxxxxxx",
      "idempotency-key": "checkout-order-10001"
    }
  }
);

console.log(response.data.data.checkoutUrl);
Python
response = requests.post(
    "https://vtu.paradigmlimited.org/topteller/api/v1/external-payment/initiate",
    json={
        "payerPhone": "08012345678",
        "amount": 2500,
        "description": "Order #10001",
        "listingReference": "LIST-88",
    },
    headers={
        "x-platform-api-key": "tk_live_xxxxxxxxxxxxxxxx",
        "idempotency-key": "checkout-order-10001",
    },
    timeout=30,
)

print(response.json())
PHP
<?php
$payload = json_encode([
    "payerPhone" => "08012345678",
    "amount" => 2500,
    "description" => "Order #10001",
    "listingReference" => "LIST-88",
]);

$curl = curl_init("https://vtu.paradigmlimited.org/topteller/api/v1/external-payment/initiate");
curl_setopt_array($curl, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "x-platform-api-key: tk_live_xxxxxxxxxxxxxxxx",
        "idempotency-key: checkout-order-10001",
    ],
]);

echo curl_exec($curl);
curl_close($curl);
Kotlin
val payload = """
    {
      "payerPhone": "08012345678",
      "amount": 2500,
      "description": "Order #10001",
      "listingReference": "LIST-88"
    }
""".trimIndent()

val request = Request.Builder()
    .url("https://vtu.paradigmlimited.org/topteller/api/v1/external-payment/initiate")
    .post(payload.toRequestBody("application/json".toMediaType()))
    .addHeader("x-platform-api-key", "tk_live_xxxxxxxxxxxxxxxx")
    .addHeader("idempotency-key", "checkout-order-10001")
    .build()

OkHttpClient().newCall(request).execute().use { response ->
    println(response.body?.string())
}