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.
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.
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.
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
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.
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
{}
Example Response
{
"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
{
"checks": ["nin"],
"nin": "39506861494",
"first_name": "Adewale",
"last_name": "Jamiu",
"reference": "user_8827"
}
Example Response
{
"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
{
"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
{
"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
{
"amount": 10000,
"callback_url": "https://yourdomain.com/wallet/funded"
}
Example Response
{
"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
{
"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
{
"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
{
"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
{
"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
{ "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