Documentation
Authentication
Two layers protect every request that touches real identity data: an HMAC signature proving the request came from you and wasn't altered in transit, and a short-lived JWT access token proving you're currently logged in. The Hosted Verification and Public Search endpoints skip both; see Hosted Verification if that's all you need.
The three signature headers
Every signed request carries these three headers:
| Field | Type | Notes |
|---|---|---|
X-Client-Keyrequired | string | Your project's public identifier. |
X-Timestamprequired | unix seconds | Must be within 300 seconds of the server's clock, in either direction. |
X-Signaturerequired | hex string | HMAC-SHA256 of the signing string below, keyed with your api_secret. |
Building the signing string
Join five parts with a pipe character, in this exact order:
client_key|timestamp|METHOD|path|sha256(body)METHODis uppercase :GET,POST,PATCH, orDELETE.pathis the full request path including/api/v1, and excluding any query string; e.g./api/v1/verifications/01ABC.../nin, not the full URL and not?status=pending.sha256(body)is the hex-encoded SHA-256 hash of the exact raw request body you're about to send, an empty string for aGETrequest with no body, not{}.
Then compute HMAC-SHA256(signing_string, api_secret), hex-encoded, that's your X-Signature.
Worked example
Submitting a NIN inside an existing verification session, a signed, JWT-authenticated POST with a body:
BODY='{"nin":"12345678901"}'
TS=$(date +%s)
HASH=$(printf '%s' "$BODY" | shasum -a 256 | cut -d' ' -f1)
# SESSION_ULID is NOT a literal string, substitute the real session_id
# returned by POST /verifications (see "Start a verification session").
# Sign and send the EXACT same path, or the signature will never match.
PATH="/api/v1/verifications/$SESSION_ULID/nin"
SIGN="$CLIENT_KEY|$TS|POST|$PATH|$HASH"
SIG=$(printf '%s' "$SIGN" | openssl dgst -sha256 -hmac "$API_SECRET" | cut -d' ' -f2)
curl -X POST "https://api.uverify.uselodger.com$PATH" \
-H "X-Client-Key: $CLIENT_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "$BODY"Getting and using the access token
"client_key and api_secret go in the JSON body"
client_key and api_secret never travel in the request at all; they're only inputs to the X-Client-Key and X-Signature headers from the section above. Posting { "client_key": "...", "api_secret": "..." } as JSON; the natural shape if you've used a typical OAuth client-credentials exchange before, gets you 401 MISSING_HEADERS: the server never reads the body on this endpoint, it's checking for three headers and finding none.So the full request is: an empty body, the three signature headers, signed with sha256("") as the body-hash component, since there's nothing to hash, and no Authorization header yet, because you don't have a token until this call returns one:
curl -X POST https://api.uverify.uselodger.com/api/v1/auth/token \
-H "X-Client-Key: $CLIENT_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG" \
-H "Content-Type: application/json"Full signing code for this exact call, in four languages, is on the quickstart page. A successful response:
{
"success": true,
"data": {
"access_token": "eyJ0eXAiOiJKV1Qi...",
"token_type": "bearer",
"expires_in": 3600,
"scope": "client"
}
}Attach access_token as Authorization: Bearer <token> on every subsequent request in addition to the three signature headers, both layers are checked on every authenticated endpoint.
Tokens last expires_in seconds (currently one hour). The simplest pattern: cache the token and its expiry when you get it, and proactively call POST /auth/token/refresh (also signed, using your current token) somewhere around the 50–55 minute mark, before it lapses. /auth/token/refresh also works for up to two weeks after a token has expired, so if you do hit a 401 TOKEN_EXPIRED on some other call because a refresh was missed, retry with /auth/token/refresh first rather than jumping straight back to a full /auth/token exchange. Only fall back to /auth/token from scratch if that refresh itself fails, meaning the token is more than two weeks stale, or was explicitly logged out.
Either way, you only need one of these calls per hour of traffic, not one per verification. Every individual verification request still needs its own fresh HMAC signature (timestamp changes every call), it just reuses whatever access token is already cached.
Test this before writing any code
Common mistakes
"The keys are the bearer token"
client_key/api_secret are signing material for the HMAC headers above; pasting api_secret directly into Authorization: Bearer fails, both because the JWT guard rejects a non-JWT value and because the request is still missing the three signature headers every endpoint requires, including /auth/token itself. The bearer value only ever comes from calling /auth/token first.Signature mismatch on every request
path, lower-casing the HTTP method, or on any endpoint with a {ulid} segment signing a hardcoded placeholder instead of the real id you're actually sending, so the path you signed and the path you requested silently diverge. That last one fails every single time, with no other symptom, which makes it easy to mistake for a broken secret or a broken signing implementation when the real request path is the only thing wrong. Log your signing string server-side and compare it character-by-character against what you'd expect."Timestamp drift" errors
Never sign from the browser or a mobile app
api_secret ships inside client-side JavaScript or an app binary, anyone can extract it. Sign requests from your own backend, or use Hosted Verification instead, which needs no secret on the client at all.