PlatformXeDocs
Get API Key

Identity Verification

Per-kind KYC endpoints (BVN, NIN, liveness, face-match, account-name) with audit trail + envelope-encrypted PII.

PlatformXe ships per-kind identity verification endpoints that wrap the underlying Identity Resolution Service with audit-grade persistence, envelope-encrypted PII, and scoped events. Five capabilities are exposed:

EndpointWhat it verifies
POST /v1/identity/verify-bvnBank Verification Number against a name.
POST /v1/identity/verify-ninNational Identification Number against a name.
POST /v1/identity/livenessLive person, not a printed photo / video / mask.
POST /v1/identity/face-matchSelfie similarity to an ID-document reference photo.
POST /v1/identity/verify-accountBank account name resolution (NIBSS-equivalent).
PropertyValue
Scopeidentity:verify
Plan gateDetection Pack addon
Rate limit1,000 requests / hour per API key (per endpoint)
IdempotencyAll endpoints honour Idempotency-Key

Each call is persisted to identity_verifications with the raw provider response envelope-encrypted under your tenant's KMS key. The metadata summary stays in the clear for fast querying; the encrypted payload is only ever decrypted under explicit reviewer authorisation.

Country support today: The per-kind verification endpoints on this page (BVN, NIN, account-name, liveness, face-match) currently dispatch through Nigeria's provider chain. The country-pluggable engine that backs them — NG, KE, GH, and ZA plugins, per-(country, provider) circuit breakers, latency observations, and a dead-letter queue — is live as of Phase 6F.5b. See Identity Resilience for the supported countries, capability matrix, and provider health surface.

For multi-country use today, call POST /api/v1/identity/resolve or /verify (the generic endpoints) with the country-appropriate identifier kind — NIN maps to each country's primary national identifier (Huduma Number in KE, Ghana Card in GH, SAID in ZA). The per-kind endpoints below will accept an explicit country parameter in a follow-up release.

Verify BVN / NIN

curl -X POST https://api.platformxe.com/api/v1/identity/verify-bvn \
  -H "Content-Type: application/json" \
  -H "x-api-key: pxk_live_..." \
  -d '{
    "subjectId": "usr_001",
    "bvn": "12345678901",
    "matchAgainst": {
      "firstName": "Adebayo",
      "lastName": "Ogunde",
      "dateOfBirth": "1980-04-15"
    }
  }'

POST /v1/identity/verify-nin is identical — substitute nin for bvn.

FieldTypeRequiredDescription
subjectIdstringYesTenant-defined user / merchant id. Persisted on the verification row for audit linkage.
bvn / ninstringYes11-digit identifier. Server validates the format.
matchAgainst.firstName / lastNamestringYesExpected name to compare against the resolved profile.
matchAgainst.dateOfBirthstringNoISO-8601. Used as a soft signal when the upstream returns DOB.

Response:

{
  "success": true,
  "data": {
    "verificationId": "iv_2j7yhq8wcs9...",
    "kind": "bvn",
    "status": "verified",
    "matchScore": 95,
    "profile": { "firstName": "Adebayo", "lastName": "Ogunde", "dateOfBirth": "1980-04-15", "...": "..." }
  }
}

Statuses: verified (match), failed (no match or upstream returned verified=false), provider_down (every provider in the chain unavailable).

When the chain returns provider_down, the request is also captured to the dead-letter queue so operators can replay it later. See Identity Resilience → Dead-letter queue.

Liveness

curl -X POST https://api.platformxe.com/api/v1/identity/liveness \
  -H "Content-Type: application/json" \
  -H "x-api-key: pxk_live_..." \
  -d '{
    "subjectId": "usr_001",
    "image": { "url": "https://your-storage.com/selfie.jpg" }
  }'
FieldTypeRequiredDescription
subjectIdstringYesTenant-defined user id.
image.urlstring (https)Either url or base64HTTPS-only URL to the captured selfie.
image.base64stringEither url or base64Raw base64 (no data: prefix). Mutually exclusive with url.
image.contentType"image/jpeg" | "image/png" | "image/webp"When base64 is suppliedMIME hint.

Response:

{
  "success": true,
  "data": {
    "verificationId": "iv_2j7yhq8...",
    "kind": "liveness",
    "status": "verified",
    "score": 92,
    "provider": "primary"
  }
}

When liveness fails, status: "failed" ships with an optional reason (spoof, occluded, low_quality, …). The provider field is the internal provider label — never a vendor name.

Face match

curl -X POST https://api.platformxe.com/api/v1/identity/face-match \
  -H "Content-Type: application/json" \
  -H "x-api-key: pxk_live_..." \
  -d '{
    "subjectId": "usr_001",
    "selfie":    { "url": "https://your-storage.com/selfie.jpg" },
    "reference": { "url": "https://your-storage.com/id-photo.jpg" },
    "threshold": 70
  }'
FieldTypeRequiredDescription
subjectIdstringYes
selfieBiometricImageYesThe "live" capture.
referenceBiometricImageYesThe reference image — typically the photo extracted from an ID document.
thresholdnumber (0–100)NoSimilarity threshold for verified. Defaults to 65, clamped to [0, 100].

Response:

{
  "success": true,
  "data": {
    "verificationId": "iv_2j7y...",
    "kind": "face_match",
    "status": "verified",
    "similarity": 78,
    "provider": "primary"
  }
}

Verify account (NIBSS-equivalent)

curl -X POST https://api.platformxe.com/api/v1/identity/verify-account \
  -H "Content-Type: application/json" \
  -H "x-api-key: pxk_live_..." \
  -d '{
    "subjectId": "usr_001",
    "accountNumber": "1234567890",
    "bankCode": "044",
    "expectedName": { "firstName": "Adebayo", "lastName": "Ogunde" },
    "threshold": 70
  }'
FieldTypeRequiredDescription
subjectIdstringYes
accountNumberstringYesAccount number in the tenant's bank. Format depends on the country.
bankCodestringYesBank code (NIBSS sort code in NG).
expectedName.firstName / lastNamestringYesExpected name to match against the resolved account name.
thresholdnumber (0–100)NoToken-overlap match threshold. Defaults to 70.

Response:

{
  "success": true,
  "data": {
    "verificationId": "iv_2j7y...",
    "kind": "account",
    "status": "verified",
    "matchScore": 100,
    "resolvedName": "ADEBAYO OGUNDE",
    "provider": "primary"
  }
}

Match scoring uses the same canonicalisation pipeline as screening (lowercase, diacritic-stripped, honorifics removed, whitespace collapsed) — Adébáyò Ógúndé resolves to the same canonical form as Adebayo Ogunde, so honest spelling differences don't sink the match.

Privacy + storage

  • Every successful or failed call writes one append-only row to identity_verifications keyed by (orgId, subjectId, kind, verifiedAt).
  • The raw provider response is envelope-encrypted: per-row 32-byte DEK, AES-256-GCM payload encryption, KEK held by the configured KMS provider. Wire format is <keyId>|<wrappedDEK>|<iv>|<authTag>|<ciphertext> (base64).
  • metadata is non-PII (scores, thresholds, bank code, matched-fields list). The PII (full name, DOB, address, photos) lives only inside the encrypted payload.
  • Decryption requires explicit reviewer authorisation. A dedicated audit-decryption endpoint that surfaces raw payloads under that authorisation ships in a follow-up release; encryption itself is live.
  • When the KMS provider is unconfigured (the documented null adapter), persistence still writes the row but rawResponseEncrypted stays null. Audit reproducibility is partial in that case but the verification trail isn't lost.

Events

EventFires when
IDENTITY_BVN_VERIFIEDBVN match against the expected name.
IDENTITY_NIN_VERIFIEDNIN match.
IDENTITY_LIVENESS_PASSEDLiveness check returned passed: true.
IDENTITY_LIVENESS_FAILEDLiveness check returned passed: false. Carries reason.
IDENTITY_FACE_MATCHEDSimilarity ≥ threshold.
IDENTITY_ACCOUNT_VERIFIEDAccount name match ≥ threshold.
IDENTITY_VERIFICATION_FAILEDAny kind failed. Carries kind, optional reason.

All events carry the verificationId so you can drill into the audit trail.

Errors

HTTPCodeCause
400BAD_REQUESTValidation failure — non-11-digit BVN/NIN, missing subjectId / expectedName, malformed image (both url+base64, http: URL), invalid threshold.
401UNAUTHORIZEDMissing or invalid API key.
402DETECTION_PACK_REQUIREDDetection Pack addon not enabled.
403FORBIDDENAPI key has no organisation context, or wrong scope.
429RATE_LIMITED1,000/hr per endpoint exceeded.
500INTERNAL_ERRORUnexpected service failure. (Provider unavailability is a documented status: "provider_down" response, NOT a 500.)