FaceLock Developer Docs

Integrate ad-hoc and browser-based identity verifications into your applications

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).

  1. Obtain a tenant API key from your FaceLock administrator (same key for ad-hoc and browser sessions).
  2. For ad-hoc: identify the target user via tenantUserKey or registered contact userId (email / E.164 phone).
  3. For browser sessions: POST with operation and share the returned minimal hyperlink (?referenceId= only) with the applicant.
  4. Poll the challenge referenceId until status is completed or expired.

Authentication

All integrator requests require two headers:

HeaderDescription
X-FaceLock-Tenant-KeyYour 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

FieldTypeRequiredDescription
tenantUserKeystringone of*Opaque TenantUser key (e.g. tu-abc…)
userIdstringone of*Registered contact: email or E.164 phone (e.g. +15551234567)
purposestringyesShown to the user in the Authenticator
initiatorReferencestringnoYour system's reference (stored as initiator)
expiresInMinutesnumbernoDefault 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

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

FieldTypeRequiredDescription
operationstringyesliveness, enrollment, verification, or idv
purposestringyesHuman-readable reason (audit)
tenantUserKeystringno*Existing tenant user (required for verification if no userId)
userIdstringno*Email or external applicant id
displayNamestringnoApplicant display name
initiatorReferencestringnoYour system's reference
expiresInMinutesnumbernoDefault 30; min 5, max 10080 (7 days)
callbackDataobjectnoOptional arbitrary JSON object stored with the session. Returned on poll (GET /sessions/{referenceId}) and sent in completion webhooks. If omitted, callbackData will be null.
redirectOnCompletionbooleannoDefault false. When true, the proof portal navigates to redirectUrl after FaceTec finishes (instead of the static “Biometric complete” page). Requires redirectUrl.
redirectUrlstringno*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}}:

PlaceholderDescriptionExample
referenceIdSession / challenge keya1b2c3...
sdkStatusFaceTec session status nameSessionCompleted, UserCancelledFaceScan
sdkStatusCodeFaceTec status numeric code0, 2
operationSession operationidv
statusCoarse status: completed, cancelled, or failedcompleted
externalDatabaseRefIdFaceTec 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:

SurfaceWhat you sendWhat 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 KeysCompletion 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:

HeaderValue
X-Webhook-TimestampUnix time in seconds (UTC)
X-Webhook-Signaturesha256= + lowercase hex HMAC-SHA256
Content-Typeapplication/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.

FieldValid valuesMeaning
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

FieldValid values / shapeNotes
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, "...": "..." }
}
FieldTypeDescription
referenceIdstringChallenge / session key (same as poll path parameter)
tenantKeystringIssuing tenant
eventstringOne of the event types above
operationstringliveness · enrollment · verification · idv
statusstringpending · completed · expired · failed
successbooleanWhether the outcome should be treated as successful
completedAtstring (ISO-8601)When the outcome was recorded (UTC)
callbackDataobject or nullYour create-time snapshot
launchIdstring or nullFaceTec launch id when available
idvDispositionobject or nullIDV review outcome
idvVerificationMetricsobject or nullSanitized FaceTec signals + biographics

IDV disposition reasons

Common idvDisposition.reason values:

ReasonMeaning
face_did_not_match3D face did not match document photo
definite_fraudHigh-confidence fraud signal
liveness_not_provenLiveness check failed
spoof_detectedPresentation attack / spoof
idv_failedGeneric IDV failure / expired challenge
manual_rejectionRejected by reviewer
manual_acceptanceAccepted by reviewer
match_level_below_thresholdMatch level under policy threshold
barcode_check_failedBarcode validation failed
quality_check_failedImage / quality check failed
face_on_doc_mismatchFace-on-document mismatch
text_on_doc_mismatchOCR / text fields mismatch
ocr_template_mismatchOCR template did not match
unexpected_mediaUnexpected media in capture
incomplete_resultsIncomplete 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.

FieldValid valuesDescription
referenceIdstringChallenge key from create response
status pending · completed · expired · failed Challenge lifecycle
completedAtISO-8601 or nullSet 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

MetricConfig keyAggregationSource
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

HTTPCodeMeaning
401missing_tenant_keyX-FaceLock-Tenant-Key header missing
401invalid_api_keyAPI key does not match tenant
400tenantUserKey or userId is requiredNeither identity field provided on ad-hoc create
400Target user not found for this tenantUnknown or out-of-tenant user (uniform message)
400Target user is not ready for ad-hoc verificationUser exists but cannot receive a push challenge