KDA Turbo
API Docs
AI Resources

llms.txt

Prompt-friendly specification for Large Language Models.

Are you an LLM crawler? Download the raw specs directly at https://app.kdadatasub.name.ng/llms.txt (concise) and https://app.kdadatasub.name.ng/llms-full.txt (complete).

llms.txt for KDA API

The KDA API is an enterprise-grade Virtual Top-Up (VTU) and bill payment infrastructure. This page is the styled human-readable version — machines should fetch the raw text spec above.

Base URLs

All API requests must be made over HTTPS to:https://app.kdadatasub.name.ng
https://app.kdadatasub.name.ng/api/services
https://app.kdadatasub.name.ng/api/profile

HTTPS only, JSON in/out. Every request carries Authorization: Bearer <API_KEY>. There is no separate sandbox host — the key prefix routes the environment.

Authentication

Test Key

Prefix: kds_test_ — sandbox simulator, simulated success, no debit. Use during development.

Live Key

Prefix: kds_live_ — production networks, real wallet debit.

Using your key

curl -X GET https://app.kdadatasub.name.ng/api/profile/balance \
  -H "Authorization: Bearer kds_test_YOUR_API_KEY"
const response = await fetch("https://app.kdadatasub.name.ng/api/profile/balance", {
  headers: { "Authorization": "Bearer kds_test_YOUR_API_KEY" }
});
const data = await response.json();
import requests

response = requests.get(
    "https://app.kdadatasub.name.ng/api/profile/balance",
    headers={"Authorization": "Bearer kds_test_YOUR_API_KEY"},
)
print(response.json())
<?php
$ch = curl_init("https://app.kdadatasub.name.ng/api/profile/balance");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ["Authorization: Bearer kds_test_YOUR_API_KEY"],
    CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);

Scopes

One scope per key — create two keys if you need both:

  • Services scope → /api/services/* (plans, purchases, verify, test).
  • Profile scope → /api/profile/* (balance, transactions, withdraw, test).
  • Wrong scope → 403 FORBIDDEN with keyStatus: "FORBIDDEN".
  • GET /api/services/test and GET /api/profile/test accept either scope.

Key status

keyStatusMeaning
INVALIDKey does not exist, is malformed, or failed verification.
NOT_FOUNDKey passed verification but no matching record exists.
DISABLEDKey exists but is disabled. Re-enable it in the Developer Console.
FORBIDDENKey is valid but lacks the required scope for this endpoint.

Missing/invalid/disabled key → 401 UNAUTHORIZED with keyStatus. No header at all → 401 without it.

Rate limits

Key typeLimitWindow
LIVE10,000 requests1 hour
SANDBOX500 requests1 hour

Exhausted → 429 RATE_LIMITED with retryAfterMs plus Retry-After / X-Retry-After headers. Wait before retrying.

  • Never expose your live key in client-side code, mobile apps, or public repos.
  • Store keys in environment variables or a secrets manager and rotate periodically.

Quickstart

Verify your key

curl -X GET https://app.kdadatasub.name.ng/api/services/test \
  -H "Authorization: Bearer kds_test_YOUR_API_KEY"

Expect "environment": "TEST" and check key.scope matches the endpoints you plan to call.

Fetch plans

curl -X GET https://app.kdadatasub.name.ng/api/services/data/plans \
  -H "Authorization: Bearer kds_test_YOUR_API_KEY"

Pick a kds-* id — the -v1- / -v2- / -v3- segment selects the provider route. ?id= fetches one plan; ?type= filters data/tv categories.

Purchase

curl -X POST https://app.kdadatasub.name.ng/api/services/data \
  -H "Authorization: Bearer kds_test_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "planId": "kds-data-mtn-sme-v1-003",
    "number": "08012345678",
    "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
    "isPorted": false
  }'

New UUID per intent; reuse the same key only for retries. Test keys return simulated success.

Go live

Create a kds_live_* key, fund your wallet, re-call /test (expect "LIVE"), then run one low-value live purchase and reconcile balance, transaction history, and the PURCHASE_SUCCESS webhook. See Going live.

Service endpoints

Data/tv without filters group as [{ type, plans[] }] with a count total; filtered/single returns flat rows with type per row.

Purchase payloads

Data — POST /api/services/data

Prop

Type

Airtime — POST /api/services/airtime

Prop

Type

TV — POST /api/services/tv (verify first)

Prop

Type

Verify: POST /api/services/verify/tv with { smartCardNumber, providerId } → { valid, name }.

Electricity — POST /api/services/electricity (verify first)

Prop

Type

Purchase meterType is UPPERCASE (PREPAID / POSTPAID), but verify meterType is lowercase (prepaid / postpaid). Verify via POST /api/services/verify/meter with { discoSlug, meterNumber, meterType }. Response carries electricityToken (prepaid) plus conditional electricityUnits.

Exam — POST /api/services/exam

Prop

Type

Response pin may join PIN and serial with <=>.

Data / Recharge cards — POST /api/services/data-card, POST /api/services/recharge-card

Prop

Type

pin / serial return comma-separated lists matching quantity.

Account endpoints

Prop

Type

Withdraw body: { amount (>= 1000), bankCode, accountNumber (10-digit), pin (4-digit), narration? }. Requires Allow Withdrawals on the key, else 412. Accepted requests return PENDING and settle via WITHDRAWAL_SUCCESS / WITHDRAWAL_FAILED.

Pricing and routes

Data / TV / Exam / Cards

salePrice — final ₦ debit.

Airtime / Recharge

salePercentage — % off face value (3 → ₦100 costs ₦97).

Electricity

saleFee — flat ₦ fee added per transaction.

-v1- primary, -v2- secondary, -v3- aggregator (rolling out for Airtime, TV, and Electricity). v2 is usually cheaper for Data/Airtime; v1 for Electricity/NECO. Re-fetch plans — never cache prices long-term. See Plan versions.

Reliability

Safe retries: idempotencyKey is required on every purchase. Duplicate → 409 CONFLICT with the original transactionId. No double charge.

Pending: most purchases settle in under 3 seconds, but vendor delays can leave PENDING. Never retry with a new key — wait for the webhook or query transaction history.

Webhooks: subscribe per category (Purchase → PURCHASE_SUCCESS / PURCHASE_FAILED; Withdrawal → WITHDRAWAL_SUCCESS / WITHDRAWAL_FAILED). Verify the X-Webhook-Signature HMAC-SHA256 against the raw body before parsing, reject skew over 5 minutes, respond 200 in under 10 seconds. No automatic retries. See Webhooks.

Errors: 400 VALIDATION_ERROR (+ details), 401, 402 INSUFFICIENT_BALANCE, 403, 404, 405, 409 CONFLICT, 412 PRECONDITION_FAILED, 422 TRANSACTION_FAILED (refunded), 429 RATE_LIMITED, 500. See Error handling.

Guide index

The canonical machine specs live at https://app.kdadatasub.name.ng/llms.txt and https://app.kdadatasub.name.ng/llms-full.txt. They are validated against app/api/services/[...path]/schemas.ts by pnpm llms:check — update them whenever endpoints or fields change.