U-verify

Documentation

Testing in Postman

The signing scheme has enough moving parts — a timestamp, a body hash, an HMAC signature, a separate bearer token — that hand-building a request to check it works is slow and error-prone. This page sets up one Postman collection-level script that signs every request automatically, so you can confirm the whole flow works in a few minutes before writing any backend code.

1. Create an environment

In Postman, create a new Environment with four variables. Leave access_token blank — it gets filled in automatically once you run the token request in step 4.

  • base_url — https://api.uverify.uselodger.com/api/v1 (use your sandbox client against the same URL — sandbox vs. production is determined by which client_key you sign with, not a different host).
  • client_key — from your dashboard.
  • api_secret — same place. Mark this variable type as secret in Postman so it doesn't show up in plain text or get synced to a shared workspace.
  • access_token — empty for now.

2. Add the signing script

Open your collection's settings → Scripts tab → Before request (this tab was called Pre-request Script in older Postman versions — same place, new name). This runs before every request in the collection, so you only write it once:

const clientKey = pm.environment.get("client_key");
const apiSecret = pm.environment.get("api_secret");
const timestamp = Math.floor(Date.now() / 1000).toString();

const method = pm.request.method.toUpperCase();

// pm.request.url.getPath() does NOT resolve {{variables}} — if base_url
// itself contains a path (e.g. /api/v1, as in step 1 above), that prefix
// silently disappears and the signed path stops matching what the server
// receives. Resolve the full URL first, then strip the scheme and host.
const resolvedUrl = pm.variables.replaceIn(pm.request.url.toString());
const path = "/" + resolvedUrl
  .replace(/^[a-z]+:\/\/[^/]+/i, "")
  .split("?")[0]
  .replace(/^\/+/, "");

const rawBody = pm.request.body && pm.request.body.mode === "raw" ? pm.request.body.raw : "";
const bodyHash = CryptoJS.SHA256(rawBody).toString(CryptoJS.enc.Hex);

const signingString = [clientKey, timestamp, method, path, bodyHash].join("|");
const signature = CryptoJS.HmacSHA256(signingString, apiSecret).toString(CryptoJS.enc.Hex);

pm.request.headers.upsert({ key: "X-Client-Key", value: clientKey });
pm.request.headers.upsert({ key: "X-Timestamp", value: timestamp });
pm.request.headers.upsert({ key: "X-Signature", value: signature });

// Attach the cached bearer token to every call except the token exchange itself
const token = pm.environment.get("access_token");
if (token && !path.endsWith("/auth/token")) {
  pm.request.headers.upsert({ key: "Authorization", value: `Bearer ${token}` });
}

CryptoJS is available globally inside Postman's sandbox — no import needed.

Why the script resolves the URL before reading the path

pm.request.url.getPath() on its own only sees the literal part of the request URL — it doesn't resolve {{variables}}. Since base_url already ends in /api/v1, reading the path that way would sign /auth/token while the server checks the signature against /api/v1/auth/token — every request then fails with 401 INVALID_SIGNATURE (never MISSING_HEADERS, since the headers are present and just signed over the wrong string). pm.variables.replaceIn() resolves base_url to its real value first, so the signed path always matches what's actually sent.

3. Cache the token automatically

Create a request for POST {{base_url}}/auth/token with an empty body. Open that request's own Scripts tab → After response (called Tests in older Postman versions) and add:

const body = pm.response.json();
if (body.success) {
  pm.environment.set("access_token", body.data.access_token);
}

Copy this same script onto a POST {{base_url}}/auth/token/refresh request too (also empty body) — running it later refreshes the cached token the same way.

4. Run it

  • Run the token request first. Send POST /auth/token. You should get back { "success": true, "data": { "access_token": "...", "expires_in": 3600 } }, and your access_token environment variable now has a value — check the eye icon next to the environment selector to confirm.
  • Then try a protected call. Create POST {{base_url}}/verify with JSON body { "checks": ["nin"], "nin": "12345678901" } (use a real sandbox test ID from your dashboard). Send it — no headers to fill in by hand, the pre-request script already attached all four.
  • Confirm the signature is actually doing something. Temporarily edit one character of api_secret in your environment, resend, and confirm you get back 401 INVALID_SIGNATURE — then put the correct value back. A quick way to be sure you're not accidentally bypassing the check.

Postman-specific pitfalls

INVALID_SIGNATURE even though all three headers are present

Almost always a path mismatch, not a bad secret. If your base_url environment variable includes a path segment — it ends in /api/v1, per step 1 — pm.request.url.getPath() alone won't see it, since it only resolves the literal part of the URL, not variables inside it. The script in step 2 already works around this with pm.variables.replaceIn(); if you're running a hand-modified version and hit this, that's the first thing to check. To confirm, add console.log(path) right after the path line and check Postman's Console (bottom-left) — it should print the full path including /api/v1.

GET requests must use body mode “none”, not an empty raw body

The script falls back to an empty-string body hash whenever pm.request.body.mode isn't "raw" — which is what you want for any GET call. If you instead leave the body tab on raw with nothing typed, that still works today, but don't mix the two across requests or a stray space will produce a body hash the server doesn't expect.

Folder-level scripts run in addition to the collection one

If you organize requests into folders and also attach a pre-request script at the folder level, both run — collection script first, then folder, then request. headers.upsert() is safe to call twice (it replaces, not appends), but keep the signing logic in exactly one place to avoid confusing drift between copies.

Don't commit api_secret into a shared collection

If you export this collection to share with teammates or commit it to a repo, export without current values, or scrub the environment file first. Mark api_secret as a secret-type variable so Postman excludes it from exports by default.

Once this works

You've now proven the signing scheme end-to-end against a real client. Porting the pre-request script's logic into your own backend language is the entire integration — see the Authentication page for worked examples in cURL, Node.js, PHP, and Python, or Full API reference for every endpoint available once you're authenticated.