Skip to main content

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).

TypeIdentifierRegistryTypical use
bvn11-digit BVNNIBSSBank-account verification, high-value transaction checks
nin11-digit NINNIMCNational identity verification, tier upgrades
dlDriver's licence numberFRSCSupplementary KYC, driver onboarding
cacCompany name or RC numberCACKYB (business verification), director UBO resolution
tinTax identification numberFIRSEnhanced 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"
}
  • status is the lifecycle state: PENDINGCOMPLETED | FAILED | EXPIRED | CANCELLED.
  • result is the verdict once completed: PASS (identifier resolved), FAIL (not found / invalid), REVIEW (manual check required).
  • data holds the provider-normalised fields. Shape varies by identifier type — see per-type sections below.
  • id is the verification UUID. Store it alongside the customer record if you want a pointer back to the full audit row (retrievable via GET /api/v1/verifications/{id}).

Type-specific data shapes

BVN — NIBSS record:

FieldExample
firstName, lastName, middleNamestrings
dateOfBirth1962-06-24
genderMale / Female
mobileE.164
enrollmentBranch, enrollmentInstitutionstring
imagedata:image/jpg;base64,…
statusfound / not found

NIN — NIMC record:

FieldExample
firstName, lastNamestrings
dateOfBirth1962-06-24
genderMale / Female
mobileE.164
address{ addressLine, lga, state, town }
religion, nokStatestrings
imagedata:image/jpg;base64,…

DL — FRSC record:

FieldExample
firstName, lastNamestrings
dateOfBirth1985-03-15
issueDate, expiryDateISO date
licenseNumberstring

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):

FieldExample
taxpayerNamestring
tinstring
rcNumberoptional, for business TINs
taxOfficestring

Error responses

All errors use the standard envelope:

{ "code": "…", "message": "…" }
HTTPcodeWhen
400VALIDATION_ERRORMissing/invalid subject_id or identifier_value; unrecognised country
401UNAUTHENTICATEDMissing/invalid Bearer token
403FORBIDDENAPI key doesn't carry the verification:write scope, or your plan doesn't include this identifier type
404SUBJECT_NOT_FOUNDsubject_id doesn't exist for your tenant
429RATE_LIMITEDPer-key rate limit hit (quota lives in your tenant's rate-limit tier)
500INTERNAL_ERRORcengine-side failure — check message for the upstream reason
502UPSTREAM_ERRORInterswitch 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; PROFESSIONAL is 300 req/min; ENTERPRISE is negotiated. Exceeding the limit returns 429 with Retry-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 at GET /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_id used
  • 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-sdk bumped to include verifyTIN.
  • 2026-04-21 — CAC endpoint normalised to always return an array in data.matches (also exposed primary_match for convenience).
  • 2026-04-18 — Driver's Licence (dl) and CAC (cac) endpoints exposed via the Marketplace Routing provider.

See also