Documentation
Hosted Verification
The fastest way to add identity checks to your product: send your user's browser to a page we host, and get a signed result back at a URL of your choosing. You never see a raw NIN or BVN unless you explicitly fetch the full record afterward. No frontend code to write.
Who this is for
The flow, end to end
- 1. You redirect out. Send the browser to our hosted page with your
client_key, which checks to run, and where to send them back. - 2. Your user verifies. They enter their ID on our page. We run the check, debit your wallet, and build a signed result.
- 3. We redirect back. The browser lands on your
redirect_urlwith the outcome and a signature appended as query parameters. - 4. You verify the signature server-side before trusting the result, then let the user continue.
Step 1 — send the user to us
Redirect (a plain link or 302 works — no signing needed for this hop) to:
https://uverify.uselodger.com/verify/hosted
?client_key=ck_live_xxxx
&checks[]=nin
&checks[]=bvn
&reference=order-42
&redirect_url=https://yourapp.com/kyc/callback| Field | Type | Notes |
|---|---|---|
client_keyrequired | string | Your project's public identifier. |
checks[]required | array | One or more of nin, bvn, drivers_license, voters_card, passport, cac. |
reference | string | Your own order/user ID — echoed back so you can match the result to your record. |
redirect_urlrequired | url | Must be on a domain you've registered against this client_key, or the hosted page rejects the session. |
Register your domain first
redirect_url is checked against a project_url you register on your account — including your staging domain, if you test there. An unregistered domain fails with INVALID_REDIRECT_URL. Update it from your dashboard's Profile page, or ask support to add it for you.Step 2 — the redirect back
Your redirect_url receives:
| Field | Type | Notes |
|---|---|---|
outcome | string | approved, rejected, manual_review, or inconclusive. |
reference | string | Whatever you passed in step 1. |
session_id | string | This hosted session's unique ID. |
verified_at | ISO 8601 | When the check completed. |
sig | hex string | HMAC-SHA256 proving this result came from U-verify unmodified. |
Step 3 — verify the signature
Recompute the signature over client_key|session_id|reference|outcome|verified_at using your api_secret, and compare it to sig with a constant-time comparison — never a plain === or ==, which leaks timing information an attacker can use to forge a valid signature byte-by-byte.
import crypto from "node:crypto";
function isValidCallback({ clientKey, apiSecret, sessionId, reference, outcome, verifiedAt, sig }) {
const signingString = [clientKey, sessionId, reference, outcome, verifiedAt].join("|");
const expected = crypto.createHmac("sha256", apiSecret).update(signingString).digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(sig, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// In your callback route handler, reading the query string U-verify redirected back with:
const url = new URL(request.url);
const ok = isValidCallback({
clientKey: process.env.VERIFYNG_CLIENT_KEY,
apiSecret: process.env.VERIFYNG_API_SECRET,
sessionId: url.searchParams.get("session_id"),
reference: url.searchParams.get("reference"),
outcome: url.searchParams.get("outcome"),
verifiedAt: url.searchParams.get("verified_at"),
sig: url.searchParams.get("sig"),
});A valid signature is not a login
sig matched.Fetch the full record
The redirect query string only carries the outcome — not the actual name, date of birth, address, or photo. Every production integration we've shipped fetches the full record from the backend as the very next step of the callback handler, before doing anything with the result. There are two ways to do this; pick based on what your callback route already has available.
Recommended — signed, no login required
Call this straight from your callback handler with the same client_key/api_secret pair you already use to sign every other request. No JWT, no prior login — just an HMAC over client_key|session_id, exactly like the callback signature above but with the session ID standing in for the full field list.
data shape you'd get from a Quick Verify call — outcome, verified_at, and a checks object keyed by check type, each with the full provider payload (name, DOB, gender, address, photo where available).| Field | Type | Notes |
|---|---|---|
client_keyrequired | string | Your project's identifier. |
session_idrequired | string | From the callback query string. |
sigrequired | hex string | hash_hmac('sha256', client_key + '|' + session_id, api_secret). |
/hosted/result-by-reference is the same endpoint shape, keyed on the reference you originally passed in Step 1 instead of session_id (signed the same way, with reference in place of the session ID). Use it to build a "refresh verification status" action that re-checks a user's latest result on demand, without sending them through a fresh hosted redirect.
import crypto from "node:crypto";
async function fetchResultBySession(sessionId) {
const clientKey = process.env.VERIFYNG_CLIENT_KEY;
const apiSecret = process.env.VERIFYNG_API_SECRET;
const sig = crypto.createHmac("sha256", apiSecret)
.update(`${clientKey}|${sessionId}`)
.digest("hex");
const res = await fetch("https://api.uverify.uselodger.com/api/v1/hosted/result", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ client_key: clientKey, session_id: sessionId, sig }),
});
const body = await res.json();
return body.success ? body.data : null; // data.checks.nin, data.outcome, ...
}Re-fetch before you trust it — always
outcome in the redirect query string is for your UI only. Before you flip any verified flag in your own database, call /hosted/result and use that response's outcome as the source of truth. A signature proves the query string wasn't tampered with in transit, but re-fetching server-to-server closes the separate case where the browser never reaches your callback at all (closed tab, dropped connection, a user who edits the URL bar) — you don't want a "refresh status" retry silently trusting stale or absent query params.Alternative — from an authenticated admin session
If the code path already has a logged-in client JWT handy (an admin/staff panel reviewing a specific verification, for instance) and doesn't want to touch the raw api_secret, this does the same lookup under your existing session instead:
Session lifetime
- The session token from step 1 is single-use and expires after 30 minutes.
- Rate limits: 30/min on validate, 10/min on verify, per IP address.
- Treat an expired session as a normal "please try again" state, not an error worth alarming your user over.