U-verify

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.

Two speeds

Need a single yes/no answer with no record-keeping on your side? Quick Verify is one call. Building an onboarding flow with subjects, multiple sessions, and document/facial capture? Use the verification session flow below.

Base URL

Every path below is relative to 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.

POST/verify
Runs every check in checks[] synchronously and returns a combined outcome in one response.
FieldTypeNotes
checksrequiredarrayOne or more of nin, bvn, drivers_license, voters_card, passport, cac, credit_bureau.
nin / bvn / license_number / voter_id / passport_number / rc_numberstringRequired for whichever check types you requested. credit_bureau is looked up by bvn too, no separate field.
bvn_selfie_imagestring (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, genderstringOptional; improves match confidence, doesn't block the check if omitted.
company_namestringRequired if checks includes cac.
referencestringYour 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

Quick Verify is one call, start to finish: 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_birth and the response includes match_confidence ; a per-field { confidence_value, status } breakdown for bvn, first_name, and last_name, straight from the registry, not a guess on our side.
  • Send bvn_selfie_image and the response includes selfie_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.

POST/subjects
FieldTypeNotes
first_name, last_namerequiredstring
external_refstringYour own user/customer ID, makes lookups on your side trivial.
email, phone, date_of_birth, gender, state_of_origin, lga_of_originstringAll optional.
  • GET /subjects : list yours, filterable by kyc_status, is_flagged, or q (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

POST/verifications
FieldTypeNotes
subject_idstringAn existing subject's ulid; provide this OR subject below, not both.
subjectobject{first_name, last_name, ...} ; creates a new subject inline.
required_checksrequiredarrayAny of nin, bvn, document, facial.
document_typestringRequired 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.

POST/verifications/{ulid}/nin
Body: { "nin": "12345678901" }. Returns 202 Accepted with a poll_url : this runs async even though it's usually fast.
GET/verifications/{ulid}/nin/status
Poll this every 2–3 seconds until status is passed or failed ; or skip polling entirely and use a webhook instead.
POST/verifications/{ulid}/bvn
Body: { "bvn": "12345678901" }. Same shape as the NIN step above; 202 Accepted with its own poll_url.
GET/verifications/{ulid}/bvn/status
Same polling pattern as /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.

POST/verifications/{ulid}/documents/upload-url
Body: { "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.
POST/verifications/{ulid}/documents
Confirms the upload finished. Body: { "media_upload_id", "document_type", "file_size_bytes", "content_hash" }.
GET/verifications/{ulid}/documents/{docUlid}/status

3c. Facial liveness step

Camera capture only

A liveness score is only meaningful if the selfie was captured live, letting a user upload an arbitrary photo defeats the entire check. If you build your own capture UI, don't add a file-picker fallback.
POST/verifications/{ulid}/facial/upload-url
Body: { "media_type": "selfie", "mime_type": "image/jpeg" }.
POST/verifications/{ulid}/facial
Body: { "selfie_upload_id", "liveness_actions": ["blink","turn_left"], "file_size_bytes", "content_hash" }.
GET/verifications/{ulid}/facial/status

4. Read the final results

GET/verifications/{ulid}/results
Returns the combined outcome once every requested check has finished, 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

POST/face-collections
Body: { "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

POST/face-collections/{ulid}/enroll
Body: { "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.
DELETE/face-collections/{ulid}/faces/{enrollmentUlid}

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.

POST/face-collections/{ulid}/authenticate
Body: { "subject_id", "image_base64" } : confirms the selfie matches one specific, claimed subject.
POST/face-collections/{ulid}/search
Body: { "image_base64", "threshold": 0.8 } : finds whoever the selfie matches across everyone in the collection; threshold is optional.
FieldTypeNotes
passedbooleanThe identity match result.
confidence, threshold_usednumber
liveness_passedbooleanThe server-side anti-spoofing result; separate from passed above.
liveness_probabilitynumber0–100.
matched_subject_idstringsearch only, which subject's ulid it resolved to.

Build your own active-liveness UX

The API only performs a passive (single-frame) liveness check, there's no endpoint for a "turn your head" motion challenge, because that part has to run in the browser. If you want that fintech-onboarding feel, track face landmarks client-side (we use MediaPipe in our own dashboard) as a free pre-filter, then submit the final frame here, the same pattern the async facial step above uses.

LIVENESS_FAILED

If the passive check doesn't pass, authenticate/search return 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.

POST/face-document-compare
Body: { "photoid_image", "selfie_image", "first_name"?, "last_name"? } , both images base64, name fields optional.
FieldTypeNotes
matchboolean
confidence_valuenumber0–100.
card_type, age_rangestring
photoid_glare, photoid_image_blurry, selfie_glare, selfie_image_blurry, sunglassesbooleanQuality 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 by check_type or source.
  • 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.

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.

POST/public/search
Body: { "check_type", "id_value", "callback_url" }. Returns either the cached public-safe data, or a payment_required response with an authorization_url.
POST/public/fetch
Body: { "payment_reference" }. Call this from your Paystack callback page to complete the lookup.

Rate limits

FieldTypeNotes
POST /auth/token10/minPer IP.
POST /auth/token/refresh10/minPer IP, shares the auth bucket above.
Authenticated endpoints60/minPer client (your client_key), flat, not currently tiered by plan.
POST /hosted/validate30/minPer IP.
POST /hosted/verify10/minPer IP.
POST /public/search20/minPer IP.
POST /public/fetch5/minPer IP.