nin and bvn (just swap the path segment and body field name). Enrollment (Epic 3) is not a separate endpoint — it’s a side effect of a passing verify-face + link, see Architecture.
POST /identity/{nin|bvn}/validate
string
11 digits.
bvn field name for the BVN route.200 OK — new validation
matchedName is masked (first letter of each word kept, rest starred) even though the full name is known internally. The photo stays server-side, cached against validationToken, until a passing verify-face.
200 OK — already linked to a different account
404 Not Found
400 Bad Request (registry mismatch)
502 Bad Gateway
POST /identity/{nin|bvn}/verify-face
multipart/form-data.
string
required
file
Required unless
reuseCapture=true.string
"true" or "false" (string, not boolean — multipart fields are always strings). Reuses the most recent passing capture from a NIN verify-face within the capture-reuse window when linking BVN right after NIN — comparison still always runs fresh against the new registry photo, only the re-capture step is skipped.validationToken (a brute-force-identity-theft control).
200 OK — match
422 — liveness failed
422 — face doesn't match the registry photo
422 — face doesn't match this account's OTHER registry
cross_registry_mismatch only triggers when linking a second registry (BVN after NIN, or vice versa) — the live face is checked against the account’s existing canonical embedding, not just the registry photo just fetched. Logged at elevated severity; policy (hard-block vs. flag-and-continue) is CROSS_REGISTRY_MISMATCH_POLICY in src/config/constants.ts.match, the returned linkToken is single-use and expires in LINK_TOKEN_TTL_SECONDS (default 5 min) — it’s the API’s proof that this exact comparison actually happened.
POST /identity/{nin|bvn}/link
string
required
200 OK
NinLink/BvnLink row, and — the enrollment side effect — upserts the live-capture embedding (carried in the linkToken payload, never re-fetched or re-computed) into canonical_embeddings. Replaying a linkToken (already consumed, expired, or never issued) returns 404 not_found.