Identifier Lookups
Number-based identity checks against Nigerian government registries (NIBSS, NIMC, FRSC, CAC, FIRS). A single call returns the authoritative record — name, DOB, gender, photo, etc. — for a given identifier. Results are persisted for audit and billed per successful call (compliance tier) or bundled into your plan (core banking, digital banking, Kora IDV, KYB).
| Type | Identifier | Registry | Typical use |
|---|---|---|---|
bvn | 11-digit BVN | NIBSS | Bank-account verification, high-value transaction checks |
nin | 11-digit NIN | NIMC | National identity verification, tier upgrades |
dl | Driver's licence number | FRSC | Supplementary KYC, driver onboarding |
cac | Company name or RC number | CAC | KYB (business verification), director UBO resolution |
tin | Tax identification number | FIRS | Enhanced due diligence, tax compliance |
All five endpoints share the same request envelope, auth scheme, and response envelope — so once you've integrated one, the rest are one-line additions.
Authentication
Every request carries two headers:
Authorization: Bearer ck_live_<your-api-key>
X-Tenant-ID: <uuid issued when your platform was onboarded>
API keys are issued per tenant via POST /api/v1/api-keys on our platform-service. See Authentication for the key-issuance flow; one-time bootstrap is covered in the Onboarding guide.
- Live keys (
ck_live_…) hit production registries — real PII is returned, real billing applies. - Sandbox keys (
ck_test_…,sk_sandbox_…) route to a stubbed IDV service that returns synthetic data.
Subject model
Every verification is attributed to a subject — a per-tenant record representing the person (or business) being checked. You create subjects once with an external_id you control (usually your customer's UUID), then reuse them for every future lookup against that same person. This gives you a single audit trail per customer and prevents duplicate subject rows.
POST /api/v1/subjects
Authorization: Bearer ck_live_...
X-Tenant-ID: <tenant-uuid>
Content-Type: application/json
{
"subject_type": "INDIVIDUAL", // or "BUSINESS" for CAC
"primary_name": "Adebisi Adedokun",
"external_id": "cust_9f2a…"
}
Response:
{ "id": "f7441462-0215-46bd-8d87-df9b6490a0e5", ... }
Re-running the same external_id is idempotent via GET /api/v1/subjects/by-external-id/{external_id} before the POST. The server SDKs handle this automatically.
Request envelope
All five lookup endpoints share the same shape:
POST /api/v1/identity/verify/{type}
Authorization: Bearer ck_live_...
X-Tenant-ID: <tenant-uuid>
Content-Type: application/json
{
"subject_id": "<subject uuid>",
"identifier_value": "22518987723",
"country": "NG"
}
{type} is one of bvn, nin, dl, cac, tin. country defaults to NG if omitted.
Response envelope
{
"id": "c001713e-0293-4572-a792-8f55f51558b7",
"tenant_id": "290c0e1e-5aa3-4d11-a37c-2eab55e28a2d",
"subject_id": "f7441462-0215-46bd-8d87-df9b6490a0e5",
"verification_type": "BVN",
"status": "COMPLETED",
"result": "PASS",
"data": {
"firstName": "ADEBISI",
"lastName": "ADEDOKUN",
"dateOfBirth": "1962-06-24",
"gender": "Male",
"mobile": "+2348012345678",
"idNumber": "22518987723",
"allValidationPassed": true,
"status": "found",
...
},
"created_at": "2026-04-22T19:17:21.345743465Z"
}
statusis the lifecycle state:PENDING→COMPLETED|FAILED|EXPIRED|CANCELLED.resultis the verdict once completed:PASS(identifier resolved),FAIL(not found / invalid),REVIEW(manual check required).dataholds the provider-normalised fields. Shape varies by identifier type — see per-type sections below.idis the verification UUID. Store it alongside the customer record if you want a pointer back to the full audit row (retrievable viaGET /api/v1/verifications/{id}).
Type-specific data shapes
BVN — NIBSS record:
| Field | Example |
|---|---|
firstName, lastName, middleName | strings |
dateOfBirth | 1962-06-24 |
gender | Male / Female |
mobile | E.164 |
enrollmentBranch, enrollmentInstitution | string |
image | data:image/jpg;base64,… |
status | found / not found |
NIN — NIMC record:
| Field | Example |
|---|---|
firstName, lastName | strings |
dateOfBirth | 1962-06-24 |
gender | Male / Female |
mobile | E.164 |
address | { addressLine, lga, state, town } |
religion, nokState | strings |
image | data:image/jpg;base64,… |
DL — FRSC record:
| Field | Example |
|---|---|
firstName, lastName | strings |
dateOfBirth | 1985-03-15 |
issueDate, expiryDate | ISO date |
licenseNumber | string |
CAC — registry lookup returns an array of matches (company name queries may match more than one):
"data": {
"matches": [ { "companyId": "…", "companyName": "…", "rcNumber": "…" }, ... ],
"match_count": 3,
"primary_match": { ... first item as convenience ... }
}
When querying by exact RC number you'll typically get match_count: 1.
TIN — FIRS record (field set provisional; confirm against the latest Interswitch Marketplace docs before relying on specific fields):
| Field | Example |
|---|---|
taxpayerName | string |
tin | string |
rcNumber | optional, for business TINs |
taxOffice | string |
Error responses
All errors use the standard envelope:
{ "code": "…", "message": "…" }
| HTTP | code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Missing/invalid subject_id or identifier_value; unrecognised country |
| 401 | UNAUTHENTICATED | Missing/invalid Bearer token |
| 403 | FORBIDDEN | API key doesn't carry the verification:write scope, or your plan doesn't include this identifier type |
| 404 | SUBJECT_NOT_FOUND | subject_id doesn't exist for your tenant |
| 429 | RATE_LIMITED | Per-key rate limit hit (quota lives in your tenant's rate-limit tier) |
| 500 | INTERNAL_ERROR | cengine-side failure — check message for the upstream reason |
| 502 | UPSTREAM_ERROR | Interswitch returned a 5xx or timed out |
Interswitch cold calls can take up to 60 seconds on first invocation (OAuth token fetch). Subsequent calls are sub-second while the token is cached.
Code examples
Node.js / TypeScript (via @kora/idv-sdk)
import { KoraIDV } from "@kora/idv-sdk";
const idv = new KoraIDV({
apiKey: process.env.KORAIDV_API_KEY!, // ck_live_…
tenantId: process.env.KORAIDV_TENANT_ID!, // your tenant UUID
});
const bvn = await idv.verifyBVN({
subjectId: customerSubjectId, // or the SDK will resolve/create
identifierValue: "22518987723",
});
if (bvn.isValid) {
await db.customers.update({
kycStatus: "verified",
kycVerifiedName: `${bvn.extractedData.firstName} ${bvn.extractedData.lastName}`,
cengineVerificationId: bvn.id,
});
}
The SDK exposes verifyBVN, verifyNIN, verifyDriversLicense, verifyCAC, verifyTIN with the same shape.
Go (raw HTTP)
req := map[string]string{
"subject_id": subjectID,
"identifier_value": "22518987723",
"country": "NG",
}
body, _ := json.Marshal(req)
r, _ := http.NewRequest(http.MethodPost,
"https://api.korastratum.com/api/v1/identity/verify/bvn",
bytes.NewReader(body))
r.Header.Set("Authorization", "Bearer "+apiKey)
r.Header.Set("X-Tenant-ID", tenantID)
r.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(r)
// parse resp.Body into your struct
curl (test call)
curl -sS -X POST https://api.korastratum.com/api/v1/identity/verify/bvn \
-H "Authorization: Bearer $KORA_API_KEY" \
-H "X-Tenant-ID: $KORA_TENANT_ID" \
-H "Content-Type: application/json" \
-d '{"subject_id":"f7441462-…","identifier_value":"22518987723","country":"NG"}' \
| jq '.status, .result, (.data.firstName + " " + .data.lastName)'
Rate limits & billing
- Rate limit is per API key. Default tier is 60 req/min;
PROFESSIONALis 300 req/min;ENTERPRISEis negotiated. Exceeding the limit returns 429 withRetry-After. - Billing depends on your plan:
- Core banking, digital banking, Kora IDV, KYB plans — BVN / NIN / CAC calls are bundled.
- Compliance / enhanced due-diligence tier — billed per successful call for BVN / NIN / DL / TIN. Failed calls (before the Interswitch hop) are not billed. Failed Interswitch responses (4xx/5xx from their side) are billed at 50%.
- Your current-period usage is available at
GET /api/v1/usage/current; historical usage atGET /api/v1/usage.
Persistence & audit
Every verification persists to our compliance_engine database permanently — we never purge identity records. You always have an audit row keyed by the returned verification id.
On your side, the minimum we recommend is storing:
- the returned
id(verification UUID) - the
subject_idused - your own outcome (
kyc_status,kyc_verified_at, etc.)
If you need to inspect the full stored payload later, hit GET /api/v1/verifications/{id}.
Changelog
- 2026-04-22 — TIN endpoint added.
@kora/idv-sdkbumped to includeverifyTIN. - 2026-04-21 — CAC endpoint normalised to always return an array in
data.matches(also exposedprimary_matchfor convenience). - 2026-04-18 — Driver's Licence (
dl) and CAC (cac) endpoints exposed via the Marketplace Routing provider.
See also
- Authentication — how to obtain and rotate API keys.
- Server integration guide — webhook handling and error-retry patterns.
- Error codes reference — complete list.
- Rate limits — per-tier quotas.