KDA API Logo
API Docs
Services

Exam PINs

Generate PINs for WAEC, NECO, and NABTEB exams.

Endpoints

All API requests must be made over HTTPS to: https://kda-turbo-web.vercel.app

Supported Providers

KDA supports exam PIN vending for the following examining bodies. Each provider is accessible via two routes where available:

Currently we only support result checker PINs. Registration PINs are not yet supported but will be available soon.

  • -v1- — Primary Infrastructure.
  • -v2- — Secondary Infrastructure.
WAEC

WAEC

Label: WAEC
Full Name: West African Examination Council
Provider IDs: kds-exam-waec-v1, kds-exam-waec-v2

NECO

NECO

Label: NECO
Full Name: National Examination Council
Provider IDs: kds-exam-neco-v1, kds-exam-neco-v2

NABTEB

NABTEB

Label: NABTEB
Full Name: National Business and Technical Examination Board
Provider ID: kds-exam-nabteb-v1

Fetch Providers

curl -X GET https://kda-turbo-web.vercel.app/api/services/exam/plans \
  -H "Authorization: Bearer kds_test_YOUR_API_KEY"
{
  "success": true,
  "count": 5,
  "data": [
    {
      "id": "kds-exam-waec-v1",
      "label": "WAEC",
      "examName": "West African Examination Council",
      "salePrice": "5150"
    },
    {
      "id": "kds-exam-neco-v1",
      "label": "NECO",
      "examName": "National Examination Council",
      "salePrice": "2120"
    },
    {
      "id": "kds-exam-nabteb-v1",
      "label": "NABTEB",
      "examName": "National Business and Technical Examination Board",
      "salePrice": "860"
    },
    {
      "id": "kds-exam-waec-v2",
      "label": "WAEC",
      "examName": "West African Examination Council",
      "salePrice": "3400"
    },
    {
      "id": "kds-exam-neco-v2",
      "label": "NECO",
      "examName": "National Examination Council",
      "salePrice": "1150"
    }
  ]
}

Fetching without any filter returns all plans. Pass ?id=<planId> to fetch a single plan.

Fetch a single provider by its id:

curl -X GET "https://kda-turbo-web.vercel.app/api/services/exam/plans?id=kds-exam-waec-v1" \
  -H "Authorization: Bearer kds_test_YOUR_API_KEY"

Pricing varies by route — v2 (secondary infrastructure) offers lower sale prices than v1 (primary infrastructure). Not all providers have a v2 route; NABTEB is currently only available on v1.

Anatomy of an Exam Plan

Each plan object represents an exam provider and route combination:

{
  "id": "kds-exam-waec-v1",
  "label": "WAEC",
  "examName": "West African Examination Council",
  "salePrice": "5150"
}

Prop

Type

Purchase Flow

Purchase Request

Request Body

Prop

Type

Example

curl -X POST https://kda-turbo-web.vercel.app/api/services/exam \
  -H "Authorization: Bearer kds_test_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "providerId": "kds-exam-waec-v1",
    "quantity": 1,
    "cardName": "Customer",
    "idempotencyKey": "681227cc-2638-49fe-bbe3-d2500cf54767"
  }'
const response = await fetch("https://kda-turbo-web.vercel.app/api/services/exam", {
  method: "POST",
  headers: {
    "Authorization": "Bearer kds_test_YOUR_API_KEY",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    providerId: "kds-exam-waec-v1",
    quantity: 1,
    cardName: "Customer",
    idempotencyKey: crypto.randomUUID()
  })
});

const data = await response.json();
import requests
import uuid

response = requests.post(
    "https://kda-turbo-web.vercel.app/api/services/exam",
    headers={
        "Authorization": "Bearer kds_test_YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json={
        "providerId": "kds-exam-waec-v1",
        "quantity": 1,
        "subscriberName": "Customer",
        "idempotencyKey": str(uuid.uuid4()),
    },
)

data = response.json()
<?php

$ch = curl_init("https://kda-turbo-web.vercel.app/api/services/exam");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer kds_test_YOUR_API_KEY",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "providerId" => "kds-exam-waec-v1",
        "quantity" => 1,
        "cardName" => "Customer",
        "idempotencyKey" => bin2hex(random_bytes(16)),
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);

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

$data = json_decode($response, true);
print_r($data);

Success Response

The response returns the pin field directly in the data object. Note that some upstream providers merge the PIN and serial number into this single field with a <=> separator.

{
  "success": true,
  "message": "Exam Card purchase successful!",
  "transactionId": "KDS-EPC-94DPPAWY-SN",
  "data": {
    "amount": 5150,
    "providerId": "kds-exam-waec-v1",
    "provider": "WAEC",
    "status": "SUCCESS",
    "description": "Exam Card purchase successful!",
    "transactionId": "KDS-EPC-94DPPAWY-SN",
    "createdAt": "2026-06-17T14:21:51.313Z",
    "quantity": "1",
    "pin": "PIN-1234567890<=>SERIAL-WEC9876543",
    "cardName": "Customer",
    "type": "EXAM_PIN",
    "idempotencyKey": "9b9be1d8-dd98-4ea4-b6e8-7df1c4db031b"
  }
}

Best Practices & Next Steps

PIN Security & Processing: Exam PINs and serial numbers are delivered as comma-separated values in the response object based on the requested quantity. Securely store and display these PINs to users as they cannot be regenerated.

Safe Retries with Idempotency: Always attach a unique idempotencyKey (UUID v4) to your request body. If your connection drops or times out, you can safely resubmit the exact same request without risk of debiting your wallet twice.

Handling Pending Statuses: While most purchases complete in under 3 seconds, vendor delays can occasionally leave a transaction in a PENDING state. Do not retry with a new idempotency key. Instead, listen for real-time status updates via Webhooks.