Documentation
Full API reference
This is the fully custom path, you build the verification screens yourself and call these endpoints directly from your backend. Every endpoint here needs the signed headers from Authentication plus a bearer access token, unless noted otherwise. If you'd rather not build a UI at all, see Hosted Verification instead, it covers the same ID-number checks with far less code.
Base URL
https://api.uverify.uselodger.com/api/v1. For example, POST /verify means POST https://api.uverify.uselodger.com/api/v1/verify.Quick Verify; one call, one answer
For when you just need to know if a set of IDs check out, with nothing to poll and nothing to look up later. Document and facial checks aren't available here, those need the async flow further down.
checks[] synchronously and returns a combined outcome in one response.| Field | Type | Notes |
|---|---|---|
checksrequired | array | One or more of nin, bvn, drivers_license, voters_card, passport, cac, credit_bureau. |
nin / bvn / license_number / voter_id / passport_number / rc_number | string | Required for whichever check types you requested. credit_bureau is looked up by bvn too, no separate field. |
bvn_selfie_image | string (base64) | Optional, only with checks including bvn; adds a face-match score against the BVN record (see the bvn response below). |
first_name, last_name, date_of_birth, gender | string | Optional; improves match confidence, doesn't block the check if omitted. |
company_name | string | Required if checks includes cac. |
reference | string | Your own identifier, echoed back in the response. |
Response: data:
{
"session_id": "8c37ef2e-017b-4cf6-8882-c1b39c4495fe",
"outcome": "approved",
"reference": "order-42",
"verified_at": "2026-01-01T12:00:00Z",
"overall_confidence": 96,
"checks": {
"nin": {
"status": "passed",
"confidence": 96,
"matched_name": "Ada Okafor",
"cost": 200,
"wallet_balance_after": 48800,
"history_ulid": "01J..."
}
}
}This session_id isn't a resource; you can't call back into it
session_id here only tags this request's logs and billing, it's a plain UUID, not a VerificationSession, and there's nothing to fetch it back from or add more checks to later. In particular, it is not interchangeable with the session ulid from POST /verifications below, posting this value to /verifications/{this session_id}/bvn (or /nin) returns 404, because no session with that ulid was ever created. Need another check on the same person afterwards? Call POST /verify again, every call stands alone, or use a verification session instead if you want one persistent record spanning several checks. Each check's own history_ulid above is independently fetchable, via GET /verifications/history/{ulid}.BVN; richer than a plain lookup
A bvn check pulls the full holder profile (watch_listed, enrollment_bank, enrollment_branch, level_of_account, email, title, marital_status, state/LGA of origin and residence) in every response. Two things layer on top of that, each independently optional:
- Send
first_name/last_name/date_of_birthand the response includesmatch_confidence; a per-field{ confidence_value, status }breakdown forbvn,first_name, andlast_name, straight from the registry, not a guess on our side. - Send
bvn_selfie_imageand the response includesselfie_match:{ confidence_value, match }, confirming the selfie belongs to the BVN holder.
Either extra costs a bit more than a bare lookup (each is a separate check behind the scenes), but the price you're charged is still the one flat bvn rate from GET /wallet ; no surprise line items.
Credit Bureau
A credit_bureau check is looked up by bvn and returns a score object; loan counts, active/closed loans, and other metrics, each broken out per bureau source (e.g. [{ "source": "CRC", "value": 3 }, { "source": "FIRST_CENTRAL", "value": 1 }] rather than one blended number). Unlike every other check type, this one is never served from cache, credit standing changes, so it's always a fresh pull and always fully billed.
Verification sessions, the full onboarding flow
Use this when you need document upload and/or facial liveness, or when you want a persistent record you can look up later (a subject applying for a loan, a tenant onboarding, etc).
1. Create a subject (optional but recommended)
A subject is the person being verified. Reuse the same subject across multiple verification sessions instead of re-entering their details each time.
| Field | Type | Notes |
|---|---|---|
first_name, last_namerequired | string | |
external_ref | string | Your own user/customer ID, makes lookups on your side trivial. |
email, phone, date_of_birth, gender, state_of_origin, lga_of_origin | string | All optional. |
GET /subjects: list yours, filterable bykyc_status,is_flagged, orq(search).GET /subjects/{ulid}·PATCH /subjects/{ulid}; read or update one.GET /subjects/{ulid}/sessions: every verification session run against them.
2. Start a verification session
| Field | Type | Notes |
|---|---|---|
subject_id | string | An existing subject's ulid; provide this OR subject below, not both. |
subject | object | {first_name, last_name, ...} ; creates a new subject inline. |
required_checksrequired | array | Any of nin, bvn, document, facial. |
document_type | string | Required if required_checks includes document, see config for the allowed list (national_id, drivers_license, etc). |
The response includes next_steps , a map of which endpoint to call for each pending check, so you don't have to hardcode the URL shape yourself.
3a. ID-number step, NIN and BVN are separate endpoints
If required_checks includes both, submit to both paths below, there's no single generic endpoint that takes either id type. Call whichever of the two your session actually requires.
{ "nin": "12345678901" }. Returns 202 Accepted with a poll_url : this runs async even though it's usually fast.status is passed or failed ; or skip polling entirely and use a webhook instead.{ "bvn": "12345678901" }. Same shape as the NIN step above; 202 Accepted with its own poll_url./nin/status above.Both calls need the same {ulid} from step 2
{ulid} here is the session_id this session's POST /verifications call returned, never a session_id from Quick Verify. Submitting a check to a {ulid} that isn't a real, still-pending session returns 404.3b. Document upload step
Documents upload directly to encrypted storage, your server never proxies the file bytes.
{ "document_type": "national_id", "mime_type": "image/jpeg" }. Returns a presigned upload_url (valid 15 minutes), upload the raw file with a PUT straight to that URL.{ "media_upload_id", "document_type", "file_size_bytes", "content_hash" }.3c. Facial liveness step
Camera capture only
{ "media_type": "selfie", "mime_type": "image/jpeg" }.{ "selfie_upload_id", "liveness_actions": ["blink","turn_left"], "file_size_bytes", "content_hash" }.4. Read the final results
approved, rejected, manual_review, or inconclusive.GET /verifications; list your sessions.GET /verifications/{ulid}; status summary (which checks are pending/completed/failed).DELETE /verifications/{ulid}; cancel a session that hasn't completed yet.
Face Collections; 1:N biometric login
A different shape of problem from the sessions above: instead of a one-shot "does this selfie match this document," a collection lets you enroll a face once and then re-authenticate that same person on every later visit; biometric login, not a one-time onboarding check. A collection persists across as many verification sessions as you need.
Create and manage a collection
{ "name": "Branch A tellers" }.GET /face-collections: list yours.GET /face-collections/{ulid}·DELETE /face-collections/{ulid}GET /face-collections/{ulid}/faces: everyone currently enrolled.
Enroll a face
{ "subject_id", "image_base64" }. One clear, front-facing photo, no liveness challenge here, since this is a deliberate setup action (often done once, sometimes by your own staff), not the "prove a live person is here right now" moment.Authenticate (1:1) and Search (1:N)
These are the re-verification moment, and both go through the same two-step check server-side before you get a result: a passive liveness/anti-spoofing pass first, then the identity match, a non-live selfie never reaches the match step, so you're not billed for comparing a photo of a photo against your collection.
{ "subject_id", "image_base64" } : confirms the selfie matches one specific, claimed subject.{ "image_base64", "threshold": 0.8 } : finds whoever the selfie matches across everyone in the collection; threshold is optional.| Field | Type | Notes |
|---|---|---|
passed | boolean | The identity match result. |
confidence, threshold_used | number | |
liveness_passed | boolean | The server-side anti-spoofing result; separate from passed above. |
liveness_probability | number | 0–100. |
matched_subject_id | string | search only, which subject's ulid it resolved to. |
Build your own active-liveness UX
LIVENESS_FAILED
422 with error.code = "LIVENESS_FAILED" ; see Errors.Face ↔ Document Compare
A stateless one-shot compare, no enrollment, nothing persisted on either side, for confirming a selfie matches the photo on an ID document image. Use this instead of Face Collections when there's no ongoing relationship to set up, just a single "is this the same person" check.
{ "photoid_image", "selfie_image", "first_name"?, "last_name"? } , both images base64, name fields optional.| Field | Type | Notes |
|---|---|---|
match | boolean | |
confidence_value | number | 0–100. |
card_type, age_range | string | |
photoid_glare, photoid_image_blurry, selfie_glare, selfie_image_blurry, sunglasses | boolean | Quality flags; worth surfacing to the user so they can retake a bad capture. |
Verification history
Every ID you've successfully looked up (via any method) is queryable later, useful for an audit trail without you having to build your own database of results.
GET /verifications/history; filter bycheck_typeorsource.GET /verifications/history/{ulid}; one record in full.
Phone number verification
A standalone OTP check, independent of the session flow above.
POST /phone/verify:{ "phone": "0801..." }, sends an SMS code.POST /phone/verify/confirm:{ "reference", "otp" }.GET /phone/verify/status/{reference}
Wallet
GET /wallet: current balance and per-check pricing.GET /wallet/transactions; full ledger.POST /wallet/fund/initialize:{ "amount", "callback_url" }, returns a Paystack checkout URL.GET /wallet/fund/verify/{reference}: confirm a top-up after the Paystack redirect.
Public ID search, no auth required
A separate, fully unauthenticated pair of endpoints; no client_key needed at all. This is what powers the public Search an ID page: free if the record is already cached, otherwise a small Paystack charge before a fresh lookup.
{ "check_type", "id_value", "callback_url" }. Returns either the cached public-safe data, or a payment_required response with an authorization_url.{ "payment_reference" }. Call this from your Paystack callback page to complete the lookup.Rate limits
| Field | Type | Notes |
|---|---|---|
POST /auth/token | 10/min | Per IP. |
POST /auth/token/refresh | 10/min | Per IP, shares the auth bucket above. |
Authenticated endpoints | 60/min | Per client (your client_key), flat, not currently tiered by plan. |
POST /hosted/validate | 30/min | Per IP. |
POST /hosted/verify | 10/min | Per IP. |
POST /public/search | 20/min | Per IP. |
POST /public/fetch | 5/min | Per IP. |