External MFA factor
Users enrol their face once. When an IBM Verify access policy asks for a second factor, they confirm with a face scan. Faces and face keys stay with Facenition. IBM receives a status, nothing more.
/v1/ibm/enrollments. Your face factor is listed./v1/ibm/initiate. You receive a one-time link by email./v1/ibm/result, receives SUCCESS, and lets you in.Every form and face scan on this site has an Advanced tab. It shows each API call that was made and the database rows exactly as they were saved, so you can follow the integration end to end.
Documentation
Facenition as an IBM Verify external MFA provider, using the init_then_poll pattern.
ibm-verify.facenition.com (this site) is the only place a camera is used. It talks to api.ibm-verify.facenition.com and never calls api.facenition.com directly, so the Facenition API token stays on the server.
fcn. The factor appears in policies as fcn:face.https://api.ibm-verify.facenition.com. Resources: /v1/ibm/enrollments, /v1/ibm/initiate, /v1/ibm/result.X-Verify-Secret, value from config.php, marked sensitive.face, pattern init_then_poll.fcn:face allowed, and attach it to a test application.Base URL https://api.ibm-verify.facenition.com. JSON in and out. Errors always look like {"error":{"code","message","request_id"}}. Every response carries X-Request-Id.
/v1/ibm/enrollmentsLists the user's face factor. Empty array when the user has not enrolled.
// request
{"username": "jane@example.com"}
// 200
[{"id": "fcn_3f9a0c1b2d4e5f60", "capability": "face",
"pattern": "init_then_poll", "label": "Facenition face verification"}]
/v1/ibm/initiateOpens a challenge (3 minutes) and emails the user a one-time link. Older pending challenges for the user are closed.
// request
{"capability": "face", "id": "fcn_3f9a0c1b2d4e5f60", "username": "jane@example.com"}
// 200
{"status": "PENDING", "transactionId": "9c1e...a7"}
// 200 when it cannot start
{"status": "FAILED", "message": "No active face enrolment for this user."}
/v1/ibm/resultPolled by Verify until the status is final.
// request
{"capability": "face", "id": "fcn_...", "transactionId": "9c1e...a7", "username": "jane@example.com"}
// 200
{"status": "PENDING" | "SUCCESS" | "FAILED"}
/v1/enrollmentsEmails an enrolment link to the address. The response is the same whether or not the address is already enrolled.
// request
{"email": "jane@example.com"}
// 202
{"status": "sent", "expires_in": 900}
/v1/enrollments/{token}// 200
{"email": "j***@example.com", "status": "pending", "factor_id": null,
"link_expires_at": "2026-10-11T07:05:00Z", "face_key_expires_at": null, "attempts_left": 3}
/v1/enrollments/{token}/captureRuns liveness, then generates the face key with Facenition and activates the factor.
// request
{"image": "data:image/jpeg;base64,..."}
// 201
{"result": "enrolled", "status": "active", "factor_id": "fcn_...", ...}
// 200 on a failed liveness check
{"result": "liveness_failed", "status": "pending", "attempts_left": 2, "message": "..."}
/v1/challenges/{token}// 200
{"email": "j***@example.com", "status": "pending", "reason": null,
"expires_at": "2026-10-11T07:03:00Z", "attempts_left": 3}
/v1/challenges/{token}/captureRuns liveness and authenticates the photo against the stored face key.
// request
{"image": "data:image/jpeg;base64,..."}
// 200
{"result": "match" | "no_match" | "liveness_failed",
"status": "success" | "pending" | "failed", "attempts_left": 2, ...}
/v1/enrollments/{token}/trace, /v1/challenges/{token}/trace, /v1/traces/{id}The data behind the Advanced tab: inbound calls, outbound calls and database rows as saved, with images removed and secrets masked. A single request's trace id comes back in the X-Trace-Id header.
/v1/health// 200
{"status": "ok", "time": "2026-10-11T06:50:00Z"}
| Table | Holds |
|---|---|
enrollments | Email, factor id, face key, sealed per-enrolment Facenition password, expiry, link token hash. |
challenges | IBM transaction id, status, attempts, poll count, link token hash. |
trace_events | The Advanced tab's record, already redacted. Kept 7 days. |
No table holds an image. Link tokens are stored only as SHA-256.