VerifyNG API

Nigerian Identity Verification — v1

VerifyNG is a unified Nigerian identity verification API. One integration connects your application to NIMC (NIN), CBN (BVN), FRSC (Driver's Licence), INEC (Voter's Card), NIS (Passport) and CAC (Business Registration) — with intelligent caching that makes repeat lookups free.

Base URL

https://api.verifyn.ng/api/v1

Production

Sandbox URL

https://sandbox.verifyn.ng/api/v1

Testing

Auth Method

HMAC-SHA256 + JWT

Per request

Authentication

VerifyNG uses a two-step authentication process. Every request is signed with HMAC-SHA256 using your api_secret, and then a short-lived JWT is used to authorise API calls. This prevents replay attacks and keeps your secret key off the wire.

1

Sign your request with HMAC-SHA256

Build a signing string from your client_key, timestamp, HTTP method, URL path and SHA-256 hash of the request body. Sign it with your api_secret using HMAC-SHA256.

2

Exchange for a JWT

POST to /auth/token with your HMAC headers. The API returns a JWT valid for 60 minutes. Cache this token — do not request a new one on every API call.

3

Call any endpoint

Include both the HMAC headers AND the JWT Bearer token in every subsequent request. The API verifies both the signature and the token.

Required headers on every request

Request Headers
X-Client-Key: ck_live_xxxxxxxxxxxx
X-Timestamp:  1783440000          # Unix timestamp (seconds)
X-Signature:  a3f8c2...            # HMAC-SHA256 signature
Authorization: Bearer eyJhbGci...  # JWT (after /auth/token)
Content-Type: application/json
Accept:       application/json

Request Signing (HMAC-SHA256)

The signing string is a pipe-delimited string of 5 components. Build it exactly as shown and sign with HMAC-SHA256 using your api_secret.

Signing String Format
client_key | unix_timestamp | HTTP_METHOD | /full/path | sha256(body)

# Example signing string:
ck_live_abc123|1783440000|POST|/api/v1/auth/token|4f53cda18c2b...

# Sign it:
signature = HMAC-SHA256(signing_string, api_secret)

# Notes:
# - HTTP_METHOD must be UPPERCASE (POST, GET, DELETE)
# - /full/path includes /api/v1 prefix
# - body = empty string "" for GET/DELETE requests
# - sha256(body) = hash of the raw JSON body string
# - Timestamp must be within 5 minutes of server time
// VerifyNG HMAC signing — PHP

function buildHmacHeaders(string $clientKey, string $apiSecret,
string $method, string $path, string $body): array {

$timestamp     = time();
$bodyHash      = hash('sha256', $body);
$signingString = implode('|', [
$clientKey, $timestamp,
strtoupper($method),
'/api/v1' . $path,
$bodyHash,
]);

$signature = hash_hmac('sha256', $signingString, $apiSecret);

return [
'X-Client-Key'  => $clientKey,
'X-Timestamp'   => $timestamp,
'X-Signature'   => $signature,
'Content-Type'  => 'application/json',
'Accept'        => 'application/json',
];
}

// Step 1: Get JWT token
$body    = '{}';
$headers = buildHmacHeaders($clientKey, $apiSecret, 'POST', '/auth/token', $body);

$response = Http::withHeaders($headers)
->withBody($body, 'application/json')
->post('https://api.verifyn.ng/api/v1/auth/token');

$jwt = $response->json('data.access_token');

// Step 2: Verify a NIN
$payload = json_encode([
'checks'     => ['nin'],
'nin'        => '12345678901',
'first_name' => 'Adaeze',
'last_name'  => 'Okonkwo',
]);

$headers = buildHmacHeaders($clientKey, $apiSecret, 'POST', '/verify', $payload);
$headers['Authorization'] = "Bearer {$jwt}";

$result = Http::withHeaders($headers)
->withBody($payload, 'application/json')
->post('https://api.verifyn.ng/api/v1/verify')
->json('data');

if ($result['outcome'] === 'approved') {
// User identity verified ✅
}
// VerifyNG HMAC signing — Node.js
const crypto = require('crypto');
const axios  = require('axios');

function buildHmacHeaders(clientKey, apiSecret, method, path, body) {
const timestamp     = Math.floor(Date.now() / 1000);
const bodyHash      = crypto.createHash('sha256').update(body).digest('hex');
const signingString = [clientKey, timestamp, method.toUpperCase(),
            `/api/v1${path}`, bodyHash].join('|');
const signature     = crypto.createHmac('sha256', apiSecret)
            .update(signingString).digest('hex');

return { 'X-Client-Key': clientKey,
'X-Timestamp':  timestamp,
'X-Signature':  signature,
'Content-Type': 'application/json',
'Accept':        'application/json' };
}

async function verifyNIN(nin, firstName, lastName) {
// Get JWT
const tokenRes = await axios.post(
'https://api.verifyn.ng/api/v1/auth/token',
{}, { headers: buildHmacHeaders(CLIENT_KEY, API_SECRET, 'POST', '/auth/token', '{}') }
);
const jwt = tokenRes.data.data.access_token;

// Verify NIN
const body = JSON.stringify({ checks: ['nin'], nin, first_name: firstName, last_name: lastName });
const headers = { ...buildHmacHeaders(CLIENT_KEY, API_SECRET, 'POST', '/verify', body),
  Authorization: `Bearer ${jwt}` };

const res = await axios.post('https://api.verifyn.ng/api/v1/verify', JSON.parse(body), { headers });
return res.data.data;
}
# VerifyNG HMAC signing — Python
import hmac, hashlib, time, json, requests

def build_hmac_headers(client_key, api_secret, method, path, body):
timestamp      = int(time.time())
body_hash      = hashlib.sha256(body.encode()).hexdigest()
signing_string = "|".join([client_key, str(timestamp),
     method.upper(), f"/api/v1{path}", body_hash])
signature      = hmac.new(api_secret.encode(), signing_string.encode(),
          hashlib.sha256).hexdigest()
return { "X-Client-Key": client_key, "X-Timestamp": str(timestamp),
"X-Signature": signature, "Content-Type": "application/json",
"Accept": "application/json" }

# Get JWT
headers = build_hmac_headers(CLIENT_KEY, API_SECRET, "POST", "/auth/token", "{}")
jwt = requests.post("https://api.verifyn.ng/api/v1/auth/token",
data="{}", headers=headers).json()["data"]["access_token"]

# Verify NIN
payload = json.dumps({"checks": ["nin"], "nin": "12345678901",
   "first_name": "Adaeze", "last_name": "Okonkwo"})
headers = build_hmac_headers(CLIENT_KEY, API_SECRET, "POST", "/verify", payload)
headers["Authorization"] = f"Bearer {jwt}"
result = requests.post("https://api.verifyn.ng/api/v1/verify",
   data=payload, headers=headers).json()["data"]
# Step 1 — Get JWT (replace values with your credentials)
# First compute the signature (see signing string docs)

curl -X POST https://api.verifyn.ng/api/v1/auth/token \
-H "X-Client-Key: ck_live_xxxx" \
-H "X-Timestamp: 1783440000" \
-H "X-Signature: your_hmac_signature" \
-H "Content-Type: application/json" \
-d '{}'

# Response:
{
"success": true,
"data": {
"access_token": "eyJhbGci...",
"expires_in": 3600
}
}

# Step 2 — Verify a NIN
curl -X POST https://api.verifyn.ng/api/v1/verify \
-H "X-Client-Key: ck_live_xxxx" \
-H "X-Timestamp: 1783440001" \
-H "X-Signature: new_hmac_for_this_body" \
-H "Authorization: Bearer eyJhbGci..." \
-H "Content-Type: application/json" \
-d '{"checks":["nin"],"nin":"12345678901","first_name":"Adaeze","last_name":"Okonkwo"}'
// VerifyNG HMAC signing — Go
package main

import (
"crypto/hmac"; "crypto/sha256"; "encoding/hex"
"fmt"; "strings"; "time"; "bytes"; "net/http"
)

func buildHmacHeaders(clientKey, apiSecret, method, path, body string) map[string]string {
timestamp  := fmt.Sprintf("%d", time.Now().Unix())
h          := sha256.Sum256([]byte(body))
bodyHash   := hex.EncodeToString(h[:])
signing    := strings.Join([]string{clientKey, timestamp,
 strings.ToUpper(method), "/api/v1"+path, bodyHash}, "|")

mac := hmac.New(sha256.New, []byte(apiSecret))
mac.Write([]byte(signing))
sig := hex.EncodeToString(mac.Sum(nil))

return map[string]string{
"X-Client-Key": clientKey, "X-Timestamp":  timestamp,
"X-Signature":  sig,         "Content-Type": "application/json",
}
}

API Endpoints

Every endpoint starts with https://api.verifyn.ng/api/v1. Each entry below shows the full expected payload, which fields are required vs optional, and a real example response.

No body fields needed. The HMAC headers identify you.

Request Body Fields

Field Required Notes
optional Empty JSON body required: {}

Example Request Body

JSON POST /api/v1/auth/token
{}

Example Response

200 OK
{
  "success": true,
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "token_type":   "bearer",
    "expires_in":   3600
  }
}

Only the ID field for your selected check type is required. All personal details (first_name, last_name, etc.) are optional — providing them improves the confidence score.

Request Body Fields

Field Required Notes
checks required At least one check type: nin, bvn, drivers_license, voters_card, passport, cac
nin if checks includes nin 11 digits, no spaces. Example: 39506861494
bvn if checks includes bvn 11 digits. Example: 22369534677
license_number if checks includes drivers_license FRSC licence number. Example: AAA00000AA00
voter_id if checks includes voters_card INEC voter ID number from PVC
passport_number if checks includes passport Letter + 8 digits. Example: A12345678
rc_number if checks includes cac CAC registration number. Example: RC1234567
first_name optional Optional. Cross-validates against government record — raises confidence score.
last_name optional Optional. Cross-validates against government record.
middle_name optional Optional.
date_of_birth required for passport Format: YYYY-MM-DD. Required for passport. Optional for NIN/BVN (raises confidence).
gender optional Optional. Values: male, female
phone optional Optional. Your applicant's phone number.
email optional Optional. Your applicant's email address.
reference optional Optional. Your internal ID for this applicant. Stored for correlation.

Example Request Body

JSON POST /api/v1/verify
{
  "checks":     ["nin"],
  "nin":        "39506861494",
  "first_name": "Adewale",
  "last_name":  "Jamiu",
  "reference":  "user_8827"
}

Example Response

200 OK
{
  "success": true,
  "data": {
    "session_id":         "01KWY41Z...",
    "outcome":            "approved",
    "reference":          "user_8827",
    "verified_at":        "2026-07-08T10:00:00.000Z",
    "overall_confidence": 95,
    "checks": {
      "nin": {
        "status":        "passed",
        "source":        "provider",
        "provider":      "dojah",
        "matched_name":  "ADEWALE JAMIU",
        "first_name":    "ADEWALE",
        "last_name":     "JAMIU",
        "middle_name":   "ADESHOLA",
        "date_of_birth": "1990-04-31",
        "gender":        "male",
        "phone_number":  "07012345678",
        "state_of_origin":"Kwara",
        "photo":         "/9j/4AAQSkZJRg...",
        "confidence":    95,
        "cost":          200,
        "wallet_balance_after": 4800
      }
    }
  }
}

GET request — no body. No query parameters needed.

Example Response

200 OK
{
  "success": true,
  "data": {
    "wallet": {
      "balance":          4800.00,
      "currency":         "NGN",
      "cost_per_nin":     200,
      "cost_per_bvn":     150,
      "cost_per_dl":      150,
      "cost_per_pvc":     150,
      "cost_per_passport":200,
      "cost_per_cac":     250,
      "low_balance_threshold": 2000
    }
  }
}

All query parameters are optional.

Query Parameters

Field Required Notes
page optional Page number. Default: 1
per_page optional Results per page. Default: 25. Max: 100
type optional Filter: debit or credit
check_type optional Filter by check: nin, bvn, etc.
from optional Start date. Format: YYYY-MM-DD
to optional End date. Format: YYYY-MM-DD

Example Response

200 OK
{
  "success": true,
  "data": {
    "data": [
      {
        "ulid":       "01KWY41Z...",
        "type":       "debit",
        "amount":     200,
        "balance_after": 4800,
        "description":"NIN verification — ref: 01KWA12...",
        "created_at": "2026-07-08T10:00:00.000Z"
      }
    ],
    "meta": { "total": 42, "per_page": 25, "current_page": 1, "last_page": 2 }
  }
}

Amount is in Naira (not kobo). Minimum ₦1,000.

Request Body Fields

Field Required Notes
amount required Amount in Naira (NGN). Minimum: 1000. Maximum: 10,000,000
callback_url required Your URL that Paystack redirects to after payment. Must be HTTPS in production.

Example Request Body

JSON POST /api/v1/wallet/fund/initialize
{
  "amount":       10000,
  "callback_url": "https://yourdomain.com/wallet/funded"
}

Example Response

200 OK
{
  "success": true,
  "data": {
    "reference":         "VNG-ABC123DEF456",
    "authorization_url": "https://checkout.paystack.com/...",
    "access_code":       "abc123...",
    "amount":            10000
  }
}

Pass the Paystack reference as a URL segment. No body needed.

Query Parameters

Field Required Notes
{reference} required URL parameter. The Paystack reference from the callback URL query string.

Example Response

200 OK
{
  "success": true,
  "data": {
    "status":      "success",
    "amount":      10000,
    "new_balance": 14800,
    "message":     "₦10,000.00 has been added to your wallet."
  }
}

All query parameters are optional.

Query Parameters

Field Required Notes
page optional Page number. Default: 1
check_type optional Filter by type: nin, bvn, drivers_license, voters_card, passport, cac
source optional Filter by source: provider, database, redis

Example Response

200 OK
{
  "success": true,
  "data": {
    "data": [
      {
        "ulid":          "01KWY41Z...",
        "check_type":    "nin",
        "source":        "provider",
        "cost_charged":  200,
        "looked_up_at":  "2026-07-08T10:00:00.000Z",
        "data": {
          "first_name":  "DAUDA",
          "last_name":   "JAMIU",
          "gender":      "male",
          "date_of_birth":"2000-06-01"
        }
      }
    ],
    "stats": {
      "total_lookups":  42,
      "provider_calls": 8,
      "cache_hits":     34,
      "total_spent":    1600
    }
  }
}

All query parameters optional.

Query Parameters

Field Required Notes
page optional Page number. Default: 1
search optional Search by name or reference
kyc_status optional Filter: approved, rejected, pending

Example Response

200 OK
{
  "success": true,
  "data": {
    "data": [
      {
        "ulid":       "01KWY41Z...",
        "reference":  "user_8827",
        "kyc_status": "approved",
        "created_at": "2026-07-08T10:00:00.000Z"
      }
    ]
  }
}

All query parameters optional.

Query Parameters

Field Required Notes
page optional Default: 1
outcome optional Filter: approved, rejected, manual_review, inconclusive
reference optional Your internal reference to find a specific session

Example Response

200 OK
{
  "success": true,
  "data": {
    "data": [
      {
        "session_id":  "01KWY41Z...",
        "outcome":     "approved",
        "reference":   "user_8827",
        "verified_at": "2026-07-08T10:00:00.000Z",
        "checks":      ["nin"]
      }
    ]
  }
}

Managed by Paystack. Do not call manually.

Request Body Fields

Field Required Notes
(Paystack payload) N/A Sent automatically by Paystack. Verified using HMAC-SHA512 against your Paystack secret key.

Example Response

200 OK
{ "message": "Webhook processed" }

Check Types

All checks go through POST /api/v1/verify. The checks array tells the API which lookups to run.

Example value

12345678901

Format

11 digits

Request body

{"checks":["nin"],"nin":"12345678901","first_name":"Adaeze","last_name":"Okonkwo"}

Example value

12345678901

Format

11 digits

Request body

{"checks":["bvn"],"bvn":"12345678901"}

Example value

AAA00000AA00

Format

Alpha-numeric

Request body

{"checks":["drivers_license"],"license_number":"AAA00000AA00"}

Example value

VIN00000000

Format

Alpha-numeric

Request body

{"checks":["voters_card"],"voter_id":"VIN00000000"}

Example value

A00000000

Format

Letter + 8 digits

Request body

{"checks":["passport"],"passport_number":"A00000000","date_of_birth":"1990-01-15"}

Example value

RC1234567

Format

RC + digits

Request body

{"checks":["cac"],"rc_number":"RC1234567"}

Response Format

All responses follow the same envelope structure.

Successful verification

{
"success": true,
"data": {
"session_id":          "01KWY41Z...",
"outcome":             "approved",  // approved | rejected | manual_review | inconclusive
"reference":           "your-user-id-123",
"verified_at":         "2026-07-08T10:00:00.000Z",
"overall_confidence":  95,
"checks": {
"nin": {
"status":        "passed",      // passed | failed | skipped | unavailable
"source":        "provider",    // provider | database | redis
"provider":      "dojah",
"matched_name":   "ADEWALE JAMIU",
"first_name":     "ADEWALE",
"last_name":      "JAMIU",
"middle_name":    "ADESHOLA",
"date_of_birth":  "1990-04-31",
"gender":         "male",
"phone_number":   "07012345678",
"state_of_origin":"Kwara",
"photo":          "/9j/4AAQSkZJRg...", // base64 NIMC photo
"confidence":     95,
"cost":           200,            // ₦200 (0 if from cache)
"wallet_balance_after": 4800
}
}
}
}

Error response

{
"success": false,
"error": {
"code":    "INSUFFICIENT_BALANCE",
"message": "Wallet balance is too low. Please top up.",
"balance": 50,
"cost":    200
}
}

Error Codes

Code HTTP Meaning & Fix
MISSING_HEADERS 401 HMAC headers missing. Add X-Client-Key, X-Timestamp, X-Signature to every request.
INVALID_CLIENT_KEY 401 client_key not found or account suspended. Check your credentials.
INVALID_SIGNATURE 401 HMAC signature mismatch. Check signing string order and api_secret.
TIMESTAMP_DRIFT 401 Request timestamp too old (>5 min). Ensure your server clock is synced (NTP).
INSUFFICIENT_BALANCE 402 Wallet has insufficient funds. Top up at /wallet/fund/initialize.
QUOTA_EXCEEDED 429 Monthly verification quota exceeded. Contact support to increase.
TOO_MANY_ATTEMPTS 429 Rate limit hit. Slow down requests — max 60/min per client.
INVALID_CREDENTIALS 401 Wrong email or password (portal login).
ACCOUNT_SUSPENDED 403 Account suspended. Contact VerifyNG support.
INVALID_RESPONSE 500 Upstream provider returned unexpected data. Non-billable — retry.
CONNECTION_ERROR 502 Could not reach government database. Non-billable — retry in 30s.

Webhooks

VerifyNG fires webhooks to your registered webhook_url when important events occur. Verify the payload using your api_secret.

wallet.low_balance

Fired when wallet drops below your configured threshold. Act immediately to avoid verification failures.

{
"event":       "wallet.low_balance",
"client_id":   "01KWY41Z...",
"balance":     1800,
"threshold":   2000,
"cost_per_nin":200,
"checks_left": 9,
"timestamp":   "2026-07-08T10:00:00.000Z"
}

Pricing & Caching

Cache-first saves money

The same NIN, BVN or any ID verified before is stored permanently in VerifyNG's database. Repeat lookups — even from different client projects — return instantly from cache at ₦0. Only the very first lookup hits the government database and costs money.

Check Type Provider Cost (first lookup) Cache Hit Cost Source Database
NIN ₦200 ₦0 ⚡ NIMC
BVN ₦150 ₦0 ⚡ CBN
Driver's Licence ₦150 ₦0 ⚡ FRSC
Voter's Card ₦150 ₦0 ⚡ INEC
International Passport ₦200 ₦0 ⚡ NIS
CAC Business ₦250 ₦0 ⚡ CAC

Prices shown are defaults. Your account may have custom pricing set by the VerifyNG administrator. Check your wallet summary (GET /wallet) for your current per-check costs.

Rate Limits

/auth/token

10 requests/minute

Per client. Prevents token hammering.

/verify

60 requests/minute

Per client. For high volume contact us.

All endpoints

300 requests/minute

Per IP address across all endpoints.

Portal login

5 attempts/minute

Per IP. Blocks brute force attempts.

Rate limit headers are returned on every response: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.

NDPR Compliance

No NIN stored in plain text

Every NIN, BVN and sensitive ID is stored as an irreversible SHA-256 hash. The actual ID is AES-256 encrypted and only accessible to authorised VerifyNG administrators.

Consent via payment

The public ID search feature requires explicit payment before returning any identity data — satisfying NDPR's consent requirement.

Right to erasure

Contact support to request deletion of any data VerifyNG holds. Cache entries can be invalidated on request.

Data minimisation

Only data directly returned by the government database is stored. VerifyNG never stores additional profiling data.

Immutable audit trail

Every access to identity data is logged with timestamp, client ID, and source. Logs cannot be modified.

Data residency

All VerifyNG data is stored in Nigeria. No data is transferred outside Nigerian jurisdiction.

Frequently Asked Questions

No. The JWT is valid for 60 minutes. Cache it and reuse it. Only request a new token when you receive a 401 response or when the token expires.

VerifyNG uses multi-provider fallback (Prembly → Smile Identity → Dojah). If all providers fail, you receive a CONNECTION_ERROR — no charge is applied. Retry after 30 seconds.

Yes. Pass multiple values in the checks array: {"checks":["nin","bvn"],"nin":"...","bvn":"..."}. Each check is independent — partial success is possible.

Yes. The photo field contains a base64-encoded JPEG image from NIMC. On cache hits, the photo is returned from the VerifyNG database at no additional cost.

"provider" means we called the government database this time and charged your wallet. "database" means we served from our permanent cache — free. "redis" means ultra-fast in-memory cache — also free.

Catch the 402 status code and show a user-friendly message like "Verification temporarily unavailable" — do not expose the wallet error to your end users. Alert your admin to top up.

Yes. Use https://sandbox.verifyn.ng/api/v1 with sandbox credentials. Test NIN: 70123456789 (returns John Doe Adamu). Test BVN: 12345678901.

Any language that can make HTTP requests and compute HMAC-SHA256. We provide examples for PHP, Node.js, Python, Go and cURL. The signing algorithm is industry-standard.

No. Your api_secret must NEVER be exposed in frontend code. Always call VerifyNG from your backend server. Your backend then returns the verification result to your frontend.

Ready to integrate?

Register your project and get sandbox credentials in under 2 minutes.

Get API Access — Free Sandbox