Quickstart
FaceLock lets Resellers and Clients initiate ad-hoc liveness verifications
against enrolled users (mobile Authenticator) or browser proof sessions
(liveness, enrollment, verification, or IDV via proof.facelock.live).
- Obtain a tenant API key from your FaceLock administrator (same key for ad-hoc and browser sessions).
- For ad-hoc: identify the target user via
tenantUserKeyor registered contactuserId(email / E.164 phone). - For browser sessions: POST with
operationand share the returned minimalhyperlink(?referenceId=only) with the applicant. - Poll the challenge
referenceIduntil status iscompletedorexpired.
Authentication
All integrator requests require two headers:
| Header | Description |
|---|---|
X-FaceLock-Tenant-Key | Your tenant identifier (Reseller or Client) |
Authorization: Bearer {api-key} | Server-to-server API key issued for your tenant |
curl -X POST "https://partner.facelock.id/api/v1/verifications/adhoc" \
-H "X-FaceLock-Tenant-Key: your-tenant-key" \
-H "Authorization: Bearer flk_live_..." \
-H "Content-Type: application/json" \
-d '{"userId":"jane@example.com","purpose":"Wire transfer approval"}'
Create ad-hoc verification
POST/api/v1/verifications/adhoc
Issues a push challenge to an enrolled Authenticator user for your tenant.
Identify the target with either tenantUserKey (FaceLock opaque key, e.g. tu-…)
or userId (the user's registered contact — normalized email or E.164 phone).
When both are sent, tenantUserKey is used.
Lookup is always scoped to the tenant on your API key (no cross-tenant search).
Request body
| Field | Type | Required | Description |
|---|---|---|---|
tenantUserKey | string | one of* | Opaque TenantUser key (e.g. tu-abc…) |
userId | string | one of* | Registered contact: email or E.164 phone (e.g. +15551234567) |
purpose | string | yes | Shown to the user in the Authenticator |
initiatorReference | string | no | Your system's reference (stored as initiator) |
expiresInMinutes | number | no | Default 5 |
* Provide tenantUserKey and/or userId. At least one is required.
Response (success)
{
"referenceId": "adhoc-7f3c...",
"tenantUserKey": "tu-abc...",
"userId": "jane@example.com",
"status": "pending",
"expiresAt": "2026-06-13T14:05:00Z"
}
The response always includes the resolved tenantUserKey (and contact userId when known)
so you can store the opaque key for later calls without looking it up again.
Identity & eligibility errors
tenantUserKey or userId is required— neither identity field was provided.Target user not found for this tenant— unknown key/contact, or not in your tenant (uniform message).Target user is not ready for ad-hoc verification— user exists but lacks MFA binding, face enrollment, or a registered device.- Invalid email/phone format returns
400with a validation message.
Create browser proof session
Use browser sessions when the user has no Authenticator app — for example,
university application pre-validation. The API returns a branded hyperlink to
proof.facelock.live where the FaceTec SDK runs in the browser.
POST/api/v1/verifications/sessions
Request body
| Field | Type | Required | Description |
|---|---|---|---|
operation | string | yes | liveness, enrollment, verification, or idv |
purpose | string | yes | Human-readable reason (audit) |
tenantUserKey | string | no* | Existing tenant user (required for verification if no userId) |
userId | string | no* | Email or external applicant id |
displayName | string | no | Applicant display name |
initiatorReference | string | no | Your system's reference |
expiresInMinutes | number | no | Default 30; min 5, max 10080 (7 days) |
callbackData | object | no | Optional arbitrary JSON object stored with the session. Returned on poll (GET /sessions/{referenceId}) and sent in completion webhooks. If omitted, callbackData will be null. |
redirectOnCompletion | boolean | no | Default false. When true, the proof portal navigates to redirectUrl after FaceTec finishes (instead of the static “Biometric complete” page). Requires redirectUrl. |
redirectUrl | string | no* | Absolute https URL template (max 2000 chars). http only allowed for localhost / 127.0.0.1. Supports placeholders (see below). Required when redirectOnCompletion is true; ignored when false. |
* For verification, provide tenantUserKey or userId of an enrolled user.
For idv/liveness/enrollment, omit both to auto-generate an anonymous applicant id.
Completion redirect
When redirectOnCompletion is true, the proof portal substitutes placeholders in redirectUrl
and navigates the browser after the FaceTec session ends (success, cancel, or error).
Placeholders support both {key} and {{key}}:
| Placeholder | Description | Example |
|---|---|---|
referenceId | Session / challenge key | a1b2c3... |
sdkStatus | FaceTec session status name | SessionCompleted, UserCancelledFaceScan |
sdkStatusCode | FaceTec status numeric code | 0, 2 |
operation | Session operation | idv |
status | Coarse status: completed, cancelled, or failed | completed |
externalDatabaseRefId | FaceTec external database ref used for the session | … |
On successful capture, results are posted to FaceLock first, then the browser redirects.
OIDC / Entra resume URLs (when present) take precedence over completion redirect.
Only trusted callers should set redirectUrl (open redirect to the destination you provide).
curl -X POST "https://partner.facelock.id/api/v1/verifications/sessions" \
-H "X-FaceLock-Tenant-Key: your-tenant-key" \
-H "Authorization: Bearer flk_live_..." \
-H "Content-Type: application/json" \
-d '{"operation":"idv","purpose":"University application pre-check","userId":"applicant@university.edu","redirectOnCompletion":true,"redirectUrl":"https://admissions.example.edu/idv/return?ref={{referenceId}}&status={{status}}&sdk={{sdkStatus}}"}'
Response (201)
{
"referenceId": "a1b2c3...",
"hyperlink": "https://proof.facelock.live/?referenceId=a1b2c3...",
"operation": "idv",
"status": "pending",
"expiresAt": "2026-06-17T18:30:00Z",
"callbackData": { "myAppId": "app-123", "priority": "high" }
}
Share hyperlink with the applicant. The URL contains only referenceId —
operation, FaceTec session ref, and tenant branding are loaded server-side when the proof portal opens
(see Session data flow below).
Session data flow
Integrator metadata is split across three surfaces:
| Surface | What you send | What you receive |
|---|---|---|
POST /sessions |
operation, purpose, initiatorReference, applicant ids, TTL |
referenceId, minimal hyperlink, operation, status, expiresAt |
| Applicant URL | — | Only ?referenceId=... (no operation, tenant key, or callback fields in the query string) |
| Proof portal bootstrap | — | Server-side challenge-context returns operation, externalDatabaseRefID, branding (logoUrl, colors), and purpose for the pending challenge |
GET /sessions/{referenceId} |
— | status, completedAt, hyperlink, callbackData (snapshotted at create time) |
| Completion webhook | Configure callback URL in Admin Portal | referenceId, operation, status, and callbackData echoing your POST metadata |
Webhook callbackData
Values from your POST body are snapshotted on the challenge and returned on completion — they are not appended to the applicant hyperlink:
{
"operation": "idv",
"purpose": "University application pre-check",
"initiatorReference": "app-2026-88421"
}
Completion webhooks
Configure a tenant callback URL and HMAC Webhook Secret in Admin Portal →
Integrator API Keys → Completion webhook (Production tab).
See the Events catalog for every event type, payload fields, and enums.
The same callbackData is also returned when polling via
GET /sessions/{referenceId}.
Use the Sandbox tab to replay a past referenceId with the real
payload shape. Sandbox deliveries include X-Sandbox-Webhook-Event: true and never
receive live challenge traffic until you promote them with Swap with Production.
Webhook HMAC-SHA256 signatures
When a webhook secret is configured, every delivery includes authenticity + integrity headers:
| Header | Value |
|---|---|
X-Webhook-Timestamp | Unix time in seconds (UTC) |
X-Webhook-Signature | sha256= + lowercase hex HMAC-SHA256 |
Content-Type | application/json |
The HMAC is computed over the exact string {timestamp}.{raw_body} using the webhook secret as the key.
The raw_body is the JSON request body as sent (compact, not pretty-printed).
Purpose: prove the request came from FaceLock and that the body was not modified in transit.
POST https://client.example.com/webhook
Content-Type: application/json
X-Webhook-Timestamp: 1720456800
X-Webhook-Signature: sha256=a1b2c3d4e5f6… (64 hex characters)
{"referenceId":"…","event":"challenge.completed",…}
Verify (Node.js)
const crypto = require('crypto');
function verifyFaceLockWebhook(secret, rawBody, timestampHeader, signatureHeader, maxSkewSec = 300) {
const ts = Number(timestampHeader);
if (!Number.isFinite(ts)) return false;
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - ts) > maxSkewSec) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${ts}.${rawBody}`, 'utf8')
.digest('hex');
const provided = String(signatureHeader || '').replace(/^sha256=/i, '');
try {
return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(provided, 'hex'));
} catch {
return false;
}
}
// Express: use express.raw({ type: 'application/json' }) so req.body is a Buffer
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8');
const ok = verifyFaceLockWebhook(
process.env.FACELOCK_WEBHOOK_SECRET,
raw,
req.get('X-Webhook-Timestamp'),
req.get('X-Webhook-Signature')
);
if (!ok) return res.status(401).send('invalid signature');
const event = JSON.parse(raw);
// handle event…
res.status(204).end();
});
Verify (Python)
import hmac, hashlib, time
def verify_facelock_webhook(secret: str, raw_body: bytes | str, timestamp: str, signature: str, max_skew=300) -> bool:
try:
ts = int(timestamp)
except (TypeError, ValueError):
return False
if abs(int(time.time()) - ts) > max_skew:
return False
body = raw_body if isinstance(raw_body, str) else raw_body.decode("utf-8")
signed = f"{ts}.{body}".encode("utf-8")
expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
provided = (signature or "").removeprefix("sha256=").removeprefix("SHA256=")
return hmac.compare_digest(expected, provided)
Poll browser session
GET/api/v1/verifications/sessions/{referenceId}
Poll the status of a browser proof session created via POST /sessions.
Retrieve the result the same way you would retrieve a Stripe object after an async action —
poll until the session reaches a terminal status.
| Field | Valid values | Meaning |
|---|---|---|
status |
pending · completed · expired · failed |
Lifecycle of the proof session. Redirect completion may also use cancelled in the browser URL; that value is not returned on poll. |
operation |
liveness · enrollment · verification · idv |
Proof type requested at create time. |
For operation=idv, when FaceTec / review data is available the response includes
idvDisposition and idvVerificationMetrics. Applicant biographics
(documentFullName, givenName/familyName,
firstName/lastName, dateOfBirth) are
null until learned from the scanned ID. Non-IDV operations return these fields as null.
The same IDV fields are included on completion webhooks.
curl -s "https://partner.facelock.id/api/v1/verifications/sessions/a1b2c3..." \ -H "X-FaceLock-Tenant-Key: your-tenant-key" \ -H "Authorization: Bearer your-api-key"
{
"referenceId": "a1b2c3...",
"tenantKey": "your-tenant-key",
"operation": "idv",
"status": "completed",
"purpose": "University application pre-check",
"initiatorReference": "app-2026-88421",
"completedAt": "2026-06-17T18:05:11Z",
"expiresAt": "2026-06-17T18:35:00Z",
"hyperlink": "https://proof.facelock.live/?referenceId=a1b2c3...",
"callbackData": { "adhocData": "idv", "purpose": "...", "extraData": "..." },
"idvDisposition": {
"disposition": "accepted",
"reason": "manual_acceptance",
"reasonLabel": "Accepted by reviewer",
"isAutomatic": false,
"isFinal": true,
"fraudFlag": false,
"matchLevel": 6,
"livenessProven": true,
"launchId": "launch-abc"
},
"idvVerificationMetrics": {
"matchLevel": 6,
"matchLevelLabel": "1:100,000",
"livenessProven": true,
"didMatchOcrTemplate": true,
"barcodeStatus": "Success",
"documentFullName": "Jane Doe",
"givenName": "Jane",
"familyName": "Doe",
"firstName": "Jane",
"lastName": "Doe",
"dateOfBirth": "1990-01-15",
"documentType": "DriversLicense"
}
}
IDV fields on poll & webhooks
| Field | Valid values / shape | Notes |
|---|---|---|
idvDisposition.disposition |
passed · needs_review · accepted · rejected |
passed normalizes to accepted for success checks. Not a poll/webhook status. |
idvDisposition.reason |
See disposition reasons | Machine code; reasonLabel is human-readable. |
idvVerificationMetrics.documentFullName |
string or null | Full name from barcode/OCR when learned |
givenName / familyName |
string or null | Also aliased as firstName / lastName |
dateOfBirth |
string or null | Prefer ISO YYYY-MM-DD when FaceTec provides it |
OpenAPI: /developer/openapi.json
Events
FaceLock posts snapshot events to your completion webhook when a challenge changes state. Configure which event types you receive under Integrator API Keys → Completion webhook → Production.
Event types
| Event | When it fires | Typical status |
success |
IDV fields |
|---|---|---|---|---|
challenge.completed |
Terminal success for verification, enrollment, or IDV | completed |
true |
Present when operation=idv |
challenge.failed |
Terminal failure (liveness/match/IDV failure) | failed |
false |
Present when operation=idv |
challenge.expired |
Session expired without completing proof | expired |
false |
Usually null / incomplete |
challenge.idv_completed |
IDV FaceTec result recorded (before or with terminal outcome) | completed |
true |
Yes — disposition + metrics |
challenge.disposition.updated |
Admin (or auto-policy) changes final IDV disposition | completed |
true if accepted/passed; else false |
Yes — updated disposition |
Webhook POST body
Deliveries are POST with Content-Type: application/json (camelCase).
Delivery secrets are not included in the body.
{
"referenceId": "a1b2c3...",
"tenantKey": "your-tenant-key",
"event": "challenge.completed",
"operation": "idv",
"status": "completed",
"success": true,
"completedAt": "2026-07-16T18:05:11Z",
"callbackData": { "purpose": "University application pre-check" },
"launchId": "launch-abc",
"idvDisposition": { "disposition": "accepted", "reason": "manual_acceptance", "...": "..." },
"idvVerificationMetrics": { "matchLevel": 6, "livenessProven": true, "...": "..." }
}
| Field | Type | Description |
|---|---|---|
referenceId | string | Challenge / session key (same as poll path parameter) |
tenantKey | string | Issuing tenant |
event | string | One of the event types above |
operation | string | liveness · enrollment · verification · idv |
status | string | pending · completed · expired · failed |
success | boolean | Whether the outcome should be treated as successful |
completedAt | string (ISO-8601) | When the outcome was recorded (UTC) |
callbackData | object or null | Your create-time snapshot |
launchId | string or null | FaceTec launch id when available |
idvDisposition | object or null | IDV review outcome |
idvVerificationMetrics | object or null | Sanitized FaceTec signals + biographics |
IDV disposition reasons
Common idvDisposition.reason values:
| Reason | Meaning |
|---|---|
face_did_not_match | 3D face did not match document photo |
definite_fraud | High-confidence fraud signal |
liveness_not_proven | Liveness check failed |
spoof_detected | Presentation attack / spoof |
idv_failed | Generic IDV failure / expired challenge |
manual_rejection | Rejected by reviewer |
manual_acceptance | Accepted by reviewer |
match_level_below_threshold | Match level under policy threshold |
barcode_check_failed | Barcode validation failed |
quality_check_failed | Image / quality check failed |
face_on_doc_mismatch | Face-on-document mismatch |
text_on_doc_mismatch | OCR / text fields mismatch |
ocr_template_mismatch | OCR template did not match |
unexpected_media | Unexpected media in capture |
incomplete_results | Incomplete FaceTec / audit results |
Sandbox webhook replays
Admin Portal sandbox harness replays include an extra header so you can ignore non-production traffic:
X-Sandbox-Webhook-Event: true
The JSON body matches the real event shape for the selected event and past
referenceId. HMAC signatures (when configured) are computed over the replay body as usual.
Poll ad-hoc challenge status
GET/api/v1/verifications/adhoc/{referenceId}
Retrieve the result of a mobile ad-hoc verification. Terminal statuses match browser sessions.
| Field | Valid values | Description |
|---|---|---|
referenceId | string | Challenge key from create response |
status |
pending · completed · expired · failed |
Challenge lifecycle |
completedAt | ISO-8601 or null | Set when the challenge reaches a terminal status |
{
"referenceId": "adhoc-7f3c...",
"status": "completed",
"completedAt": "2026-06-13T14:02:11Z"
}
Usage & billing
Completed verifications are logged to UsageLog (append-only audit rows for the
current billing period). FaceLock aggregates usage across your Reseller hierarchy — peak
concurrent MFA users and verification volume — then reports billable events to
Stripe Metronome via the ingest API.
Metronome billable metrics
| Metric | Config key | Aggregation | Source |
|---|---|---|---|
| MFA user accounts | Metronome:MfaUserMetricId |
Peak concurrent enrolled users in period | Temporal MFAAuthMethod rollup |
| IDV sessions | Metronome:VerificationMetricId |
Sum of new sessions since last report | UsageLog (ad-hoc, automated, and remote/self-serve enrollment IDV) |
| Credentials issued | Metronome:CredentialIssuedMetricId |
Sum since last report | UsageLog via FaceLock Creator |
| Reader audits | Metronome:ReaderAuditMetricId |
Sum since last report | POST /api/reader/audit webhook |
UsageEventReporter sends events to Metronome ingest with the Stripe Customer ID
as customer_id and an idempotent transaction_id per report.
Stripe handles invoicing, dunning, tax, and payment collection only — not usage metering.
Resellers see aggregated usage and on-demand reporting in Admin Portal
Usage & Billing. Integrator API calls that complete verification contribute
to the same UsageLog rows used for billing aggregation.
Sandbox
Sandbox API keys
Create a sandbox integrator key in Admin Portal → Integrator API Keys.
Sandbox keys use the flk_test_ prefix and return simulated responses without
issuing real MFA challenges to users.
POST /api/v1/verifications/adhoc → { "referenceId": "sandbox-...", "sandbox": true }
GET /api/v1/verifications/adhoc/sandbox-... → { "status": "completed", "sandbox": true }
POST /api/v1/verifications/sessions → { "referenceId": "sandbox-...", "hyperlink": "...", "sandbox": true }
GET /api/v1/verifications/sessions/sandbox-... → { "status": "completed", "sandbox": true }
Sandbox completion webhook
Separately from API keys, configure a Sandbox completion webhook under
Integrator API Keys to replay real payloads for past referenceId values.
Replays send X-Sandbox-Webhook-Event: true (see Events).
Live challenges always use the Production webhook until you swap.
OpenAPI spec: openapi.json
Microsoft Entra External MFA
FaceLock Elevated MFA integrates with Microsoft Entra ID as an External Authentication Method (OIDC). IT administrators add the discovery URL in Entra — they do not create an app registration, redirect URIs, or client secrets in the customer tenant.
Entra External MFA setup guide (IT administrators) →
Discovery URL: https://mfa.facelock.live/.well-known/openid-configuration (public OIDC metadata).
FaceLock External MFA client ID (platform app, not registered by customer):
6ec30e4d-87de-4bdd-bd6c-ecd3133ad1c8.
Partners map each customer Entra tid in Admin Portal → Tenant → MFA Configuration.
The separate Entra Graph app + client secret used for self-serve enrollment tooling is FaceLock-internal only.
Errors
| HTTP | Code | Meaning |
|---|---|---|
| 401 | missing_tenant_key | X-FaceLock-Tenant-Key header missing |
| 401 | invalid_api_key | API key does not match tenant |
| 400 | tenantUserKey or userId is required | Neither identity field provided on ad-hoc create |
| 400 | Target user not found for this tenant | Unknown or out-of-tenant user (uniform message) |
| 400 | Target user is not ready for ad-hoc verification | User exists but cannot receive a push challenge |