ProctorSafe Liveness Integration Guide

This guide explains how to add standalone liveness checks to your website or application using the ProctorSafe SDK.

It is written for developers integrating our SDK and APIs, and focuses on how to use the SDK, not on internal implementation details.

  • If you want full exam proctoring (continuous monitoring, marks/sections, trust score, network monitoring, etc.), see the main ProctorSafe Integration Guide.
  • If you only need to verify that a user is present and matches a reference (for onboarding, identity verification, or high‑risk actions), this liveness guide is for you.

Table of Contents

  1. What is liveness and when to use it
  2. Prerequisites
  3. SDK installation
  4. Quick start: run a liveness check
  5. Frontend integration
  6. Signature
  7. Backend verification (optional but recommended)
  8. Dashboard access and APIs
  9. Configuration options
  10. Accessibility
  11. Troubleshooting
  12. Next steps and related docs

What is liveness and when to use it

Liveness checks help you confirm that:

  • There is a real person in front of the camera (not a static photo or replay).
  • The person follows on‑screen instructions (e.g. tilt head left/right/up/down).
  • Optionally, the person matches a reference image and/or their estimated age is within an expected range.

Typical use cases:

  • Onboarding / KYC: Verify that the person creating an account is real and matches an identity document.
  • High‑risk actions: Step‑up verification when users change sensitive data or perform high‑value operations.
  • Exam / session unlock: Verify liveness once before granting access to a protected flow.

If you later decide you also need continuous exam monitoring, you can combine liveness with the full proctoring integration described in the main integration guide.


Prerequisites

Before integrating liveness, make sure you have:

  1. Tenant name (slug) – Provided by your tenant manager, e.g. your-tenant-slug.
  2. Liveness feature enabled for your tenant – In the dashboard, an admin must enable “Liveness check” in the tenant feature settings. If this is disabled, attempts to start a liveness session will be rejected.
  3. Allowed origins configured – Your web application’s domain(s) must be added to the tenant’s allowed origins list so the SDK and APIs can be called from your site (see the CORS guidance in the main integration guide).
  4. Optionally: Signature configuration – Either use your own ECDSA key pair or the create API helper (POST /api/proctor/create) so you can pass signature and timestamp when starting liveness (if your tenant requires signatures).
  5. User environment requirements – A modern browser with camera access and reasonable lighting conditions.

The SDK will run basic environment checks and show a preflight screen if enabled.


SDK installation

Liveness is part of the same ProctorSafe SDK used for proctoring sessions. If you already installed the SDK for the main integration guide, you can reuse that setup.

1. Include the SDK script (required)

Add the ProctorSafe SDK to your HTML page:

<!DOCTYPE html> <html> <head> <title>Your Application</title> </head> <body> <!-- Your application content --> <!-- ProctorSafe SDK (auto-loads all dependencies) --> <script src="https://test.proctorsafe.eu/sdk/proctor.iife.js"></script> <!-- Your application scripts --> <script src="your-app.js"></script> </body> </html>

The SDK automatically loads its internal dependencies when needed. You don’t need to include any extra scripts yourself.

2. Optional: TypeScript support

If you are using TypeScript, you can reuse the same proctor.d.ts file described in the main integration guide:

  1. Download https://test.proctorsafe.eu/sdk/proctor.d.ts
  2. Place it in a types/ directory in your project
  3. Add the directory to your tsconfig.json include section

This gives you full type support for the window.Proctor object, including LivenessConfig and LivenessResult types.


Quick start: run a liveness check

This is the simplest way to run a liveness check from your frontend.

// Ensure the SDK is loaded (script tag present and window.Proctor available) const proctor = window.Proctor; async function runLiveness() { if (!proctor) { alert('ProctorSafe SDK not loaded'); return; } // Replace with your own values const tenantName = 'your-tenant-slug'; const applicationReference = 'onboarding-v1'; // Optional: signature + timestamp if your tenant requires signatures const { signature, timestamp } = await generateSignatureForTenant( tenantName, applicationReference ); const result = await proctor.liveness({ tenantName, applicationReference, signature, timestamp, ageCheck: false, showPreflight: true, }); if (result.livenessOk) { console.log('Liveness passed:', result); } else { console.log('Liveness failed:', result); } }

What this does:

  • Shows an optional preflight dashboard (camera/mic/lighting check).
  • Guides the user through looking straight and tilting their head in random directions.
  • Returns a LivenessResult object with:
    • livenessOk: whether the liveness check passed.
    • Optional estimatedAge.
    • Optional likenessPercentage and referenceImageSha256 (if you use a reference image).
    • livenessSessionId and resultBinding (for backend verification).

The following sections explain how to integrate this into your UI and backend.


Frontend integration

1. Prepare configuration

At a minimum, you must provide:

  • tenantName: your tenant slug.
  • applicationReference: a string that identifies the flow (e.g. onboarding-v1, kyc-step-2).

If your tenant requires signatures, you must also provide:

  • signature: base64 signature for your tenant + reference + timestamp.
  • timestamp: the timestamp used in the signature payload.

You can reuse the same signature generation logic for both proctoring and liveness. For the full process — generating keys, picking between the sign callback and the static fields, and what to do when signatures are disabled — see the Signature section below.

2. Call Proctor.liveness(...)

Basic example:

async function startLivenessCheck() { const tenantName = 'your-tenant-slug'; const applicationReference = 'kyc-onboarding-2024'; // Optional: obtain signature and timestamp if required const { signature, timestamp } = await generateSignatureForTenant( tenantName, applicationReference ); const result = await window.Proctor.liveness({ tenantName, applicationReference, signature, timestamp, ageCheck: true, // enable age estimation showPreflight: true, // show preflight camera/mic/lighting screen }); if (!result.livenessOk) { // Liveness failed (e.g. user did not follow instructions) showError('Liveness check failed. Please try again.'); return; } // Liveness passed – you can now proceed console.log('Liveness result:', result); }

3. Optional: reference image for face match

If you want to verify that the user matches a specific face image (for example, a selfie from a previous session or a document scan), you can pass a base64 encoded reference image:

async function startLivenessWithReference(referenceImageBase64) { const result = await window.Proctor.liveness({ tenantName: 'your-tenant-slug', applicationReference: 'kyc-onboarding-2024', ageCheck: true, showPreflight: true, referenceImageBase64, // data URL: e.g. "data:image/jpeg;base64,...." }); if (result.likenessPercentage != null) { console.log('Likeness percentage:', result.likenessPercentage, '%'); } if (!result.livenessOk) { showError('Liveness or face match failed. Please try again.'); return; } // Use resultBinding + livenessSessionId for backend verification }

The SDK will:

  • Sample likeness throughout the interaction.
  • Return a likeness percentage (0–100) when available.
  • Return the SHA‑256 hash of the reference image.

You can use these values in your own business rules (for example, require a minimum likeness score).


Signature

ProctorSafe uses cryptographic signatures to ensure only authorized clients can start a liveness check for your tenant. Signatures are highly recommended for production, and the tenant manager dashboard can make them mandatory.

Signature overview

  • ECDSA P-256: signatures use the standard prime256v1 (a.k.a. secp256r1) curve.
  • Payload: the signature covers the comma-separated string `${tenantName},${applicationReference},${timestamp}` — nothing else. The signed payload does not include the WASM-derived client_public_key, the liveness_token, or any device state.
  • Server verification: the server verifies the signature against your tenant's stored public key (autoGeneratedPublicKey by default, or customPublicKey if you uploaded your own).
  • Freshness: the timestamp must be within the tenant's initTimestampMaxAgeMinutes window (default 15 minutes). Signatures older than that are rejected with HTTP 400 TIMESTAMP_TOO_OLD.

Signature payload structure

The exact payload you must sign is a comma-separated string:

$tenant,$reference,$timestamp

Example (the timestamp is a JavaScript milliseconds-since-Unix-epoch value):

my-tenant,kyc-onboarding-2024,1704067200000

This string is signed with ECDSA P-256 + SHA-256. The server expects the signature as base64-encoded ASN.1 DER (the default output of OpenSSL, Node's crypto.sign, and most standard libraries).

Three ways to provide a signature

You can use any of the three approaches below; the server-side verification is identical for all of them.

You generate an ECDSA P-256 key pair, upload the public key to the tenant dashboard (Settings → Custom Public Key), and sign the canonical payload on your backend with the private key.

Generate a key pair and sign (Node.js):

const crypto = require('crypto'); // Generate ECDSA P-256 key pair const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1', publicKeyEncoding: { type: 'spki', format: 'der', }, privateKeyEncoding: { type: 'sec1', format: 'der', }, }); // Store the public key (hex-encoded) - upload this to tenant dashboard const publicKeyHex = publicKey.toString('hex'); console.log('Public key (upload to dashboard):', publicKeyHex); // Keep the private key secure (never expose to client-side) const privateKeyHex = privateKey.toString('hex'); // Sign the canonical payload: $tenant,$reference,$timestamp function signPayload(tenantName, applicationReference, timestamp, privateKeyHex) { const payloadString = `${tenantName},${applicationReference},${timestamp}`; const sign = crypto.createSign('SHA256'); sign.update(payloadString); sign.end(); const privateKey = crypto.createPrivateKey({ key: Buffer.from(privateKeyHex, 'hex'), format: 'der', type: 'sec1', }); return sign.sign(privateKey).toString('base64'); } // Example const timestamp = Date.now(); const signature = signPayload('my-tenant', 'kyc-onboarding-2024', timestamp, privateKeyHex); console.log({ signature, timestamp });

To generate the key pair with OpenSSL instead of Node.js, see the OpenSSL script in the proctoring guide. The on-the-wire format (hex SPKI public key, base64 DER signature) is identical.

Then upload the public key to Settings → Custom Public Key in the tenant dashboard. From this point on, the server will verify every /liveness/init request against your uploaded public key.

Method 2 — The Create API helper

For development and testing, or when you'd rather not manage keys yourself, you can have the server mint the signature for you. The endpoint uses the tenant's auto-generated private key (stored encrypted server-side) to produce a fresh signature on demand.

Endpoint: POST /api/proctor/create

Authentication: Authorization: Bearer <api-key> — obtain the API key from the tenant dashboard (Settings → API Keys).

Request body:

{ "tenant_name": "my-tenant", "application_reference": "kyc-onboarding-2024" }

Response:

{ "tenant_name": "my-tenant", "application_reference": "kyc-onboarding-2024", "signature": "base64-encoded-signature", "timestamp": 1704067200000 }

Example (browser):

async function generateSignature(tenantName, applicationReference, apiKey) { const response = await fetch('https://test.proctorsafe.eu/api/proctor/create', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, }, body: JSON.stringify({ tenant_name: tenantName, application_reference: applicationReference, }), }); if (!response.ok) { throw new Error('Failed to generate signature'); } return await response.json(); } // In your liveness flow: const { signature, timestamp } = await generateSignature( 'my-tenant', 'kyc-onboarding-2024', apiKey, ); const result = await window.Proctor.liveness({ tenantName: 'my-tenant', applicationReference: 'kyc-onboarding-2024', signature, timestamp, });

⚠️ This method exposes your API key to the browser. Use it only in development or in trusted environments where the key is otherwise protected (e.g. a backend-for-frontend that brokers the call). For production, prefer Method 1 or Method 3.

When the tenant private key is held only on your backend, or when your flow is long enough that a signature minted at config-build time risks exceeding the freshness window, pass a sign callback instead of static signature/timestamp fields. The SDK will mint a fresh timestamp and POST the integrator-returned signature just before /liveness/init, so the signature never goes stale across a slow preflight or a cross-device handoff.

The callback receives a SignRequest and returns a SignedPayload:

interface SignRequest { tenantName: string; applicationReference: string; /** SDK-minted timestamp (ms since epoch). Use this or override it. */ timestamp: number; } interface SignedPayload { signature: string; /** Optional: override the timestamp. When provided it MUST equal the one you signed over. */ timestamp?: number; }

Canonical pattern — your backend mints the timestamp atomically with the signature (preferred when your private key is on a server):

const result = await window.Proctor.liveness({ tenantName: 'my-tenant', applicationReference: 'kyc-onboarding-2024', sign: async ({ tenantName, applicationReference }) => { // Ask your backend to mint + sign the canonical string atomically. // It must return { signature, timestamp } with the SAME timestamp it signed over. const { signature, timestamp } = await fetch('/api/proctorsafe/sign', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tenantName, applicationReference }), }).then((r) => r.json()); return { signature, timestamp }; }, ageCheck: true, showPreflight: true, });

Your backend handler signs `${tenantName},${applicationReference},${timestamp}` with your stored private key (the same way Method 1 does) and returns both signature and timestamp in one response so they cannot drift.

Fallback — let the SDK mint the timestamp (use this only when the SDK and your backend both trust a clock value, and the backend can't return its own timestamp):

const result = await window.Proctor.liveness({ tenantName: 'my-tenant', applicationReference: 'kyc-onboarding-2024', sign: async ({ tenantName, applicationReference, timestamp }) => { // The SDK has already minted `timestamp`. Sign the canonical string on your // backend and return ONLY the signature — the SDK uses its timestamp unchanged. const signature = await fetch('/api/proctorsafe/sign', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tenantName, applicationReference, timestamp, // pass the SDK-minted value through to your signer }), }).then((r) => r.text()); return { signature }; }, });

When both sign and static signature/timestamp are configured, the sign callback wins.

When signature is not required

If your tenant has signature enforcement disabled (Settings → Require signature), you can omit both the static fields and the sign callback. The SDK will POST to /liveness/init without any signature fields and the server will accept the request.

Example without signature:

await window.Proctor.liveness({ tenantName: 'my-tenant', applicationReference: 'kyc-onboarding-2024', ageCheck: true, showPreflight: true, // No signature / timestamp / sign callback });

This is appropriate for prototypes, internal tools, or behind a trusted gateway. For any production flow that touches a real candidate, sign.


For many use cases, it is enough to:

  1. Run Proctor.liveness(...) on the frontend.
  2. Check result.livenessOk.
  3. Store the high‑level outcome in your own system.

If you need stronger guarantees (for example, for regulated flows), you can use the liveness result verification endpoint from your backend to make sure the result:

  • Comes from ProctorSafe.
  • Has not been tampered with.
  • Matches the session and reference you expect.

1. Data returned to the frontend

The LivenessResult object includes:

  • livenessOk: overall success/failure.
  • livenessSessionId: unique session identifier.
  • resultBinding: a verification token (HMAC) bound to the session and result.
  • Optional metadata (estimatedAge, likenessPercentage, referenceImageSha256).

Your frontend should send at least the following to your backend:

{ "livenessSessionId": "lsn_123...", "livenessOk": true, "resultBinding": "hex-hmac-value", "likenessPercentage": 92, "referenceImageSha256": "abc123...", "estimatedAge": 27 }

2. Verify result on your backend

On your backend, you call the ProctorSafe liveness result API:

  • Method: POST
  • Path: /api/proctor/liveness/result

This endpoint verifies the result binding and returns a simple JSON status.

Example (Node.js / server‑side):

import fetch from 'node-fetch'; async function verifyLivenessResult(result) { const response = await fetch('https://test.proctorsafe.eu/api/proctor/liveness/result', { method: 'POST', headers: { 'Content-Type': 'application/json', // If authentication headers are required for your tenant, add them here }, body: JSON.stringify({ liveness_session_id: result.livenessSessionId, liveness_ok: result.livenessOk, likeness_percentage: result.likenessPercentage ?? null, reference_image_sha256: result.referenceImageSha256 ?? null, estimated_age: result.estimatedAge ?? null, result_binding: result.resultBinding, }), }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.error || 'Liveness result verification failed'); } const data = await response.json(); return data.ok === true; }

Typical flow:

  1. Frontend runs Proctor.liveness(...).
  2. Frontend sends LivenessResult to your backend.
  3. Backend calls /api/proctor/liveness/result.
  4. If verification succeeds, your backend can safely accept the liveness result and continue the flow.

Time limits and validity window

There are a few important time-related limits to be aware of:

  • Signature timestamp window (if signatures are required)
    When you pass signature and timestamp in the liveness config, the backend will only accept the timestamp if:

    • It is not more than 15 minutes in the past, and
    • It is not in the future.
      In practice, this means you should generate the signature right before you call liveness init and avoid reusing old signatures.
  • Liveness session validity (server-side verification window)
    After a successful liveness init, the server creates a per-session secret that is used to verify the resultBinding. This secret:

    • Is valid for 15 minutes from liveness init.
    • Can be used to verify the result multiple times during that window.
      Within the TTL, repeated calls to /api/proctor/liveness/result with the same session and same payload will succeed (idempotent verification). If your backend calls the endpoint after the 15‑minute window, the server will clear the key and return an error such as “Session key expired” or “Result already verified or session key expired”, and you must run a new liveness check.
  • User-facing attempts and timeouts (SDK behavior)
    The SDK itself enforces sensible limits to avoid liveness checks running forever:

    • Each step (look straight / tilt) has a step timeout (about 15 seconds). If the user does not complete the instruction in time, the engine restarts the flow for another attempt.
    • The engine allows a limited number of attempts (by default 3 attempts). After that, the check ends with livenessOk: false and you should decide how your application handles the failure (e.g. allow retry later, route to manual review, etc.).
      These limits are handled automatically by the SDK; you do not need to configure them for a typical integration.

You do not need to implement any cryptography yourself; the endpoint handles verification for you.

Note: If your deployment uses a different base URL than https://test.proctorsafe.eu, adjust the hostname accordingly.


Dashboard access and APIs

View liveness sessions in the dashboard

In the dashboard, liveness sessions appear in a dedicated Liveness sessions view. There you can:

  • Filter liveness sessions by tenant, application reference, and date.
  • Inspect high‑level results and metadata.
  • Confirm that your integration is working and that sessions are being recorded.

(The URL and navigation are the same dashboard you use for proctoring sessions; liveness is just a separate section.)

Programmatic access

For most integrations, you do not need a separate liveness API beyond the result verification endpoint described above. If you already use the standard ProctorSafe session APIs described in the main integration guide, you can continue to use them alongside liveness.

If you have specific reporting or export requirements, please contact support so we can recommend the best approach for your use case.


Configuration options

The Proctor.liveness call accepts a configuration object. The most important fields for integrators are:

  • Tenant and identification

    • tenantName (string, required): Your tenant slug.
    • applicationReference (string, required): Identifies the flow (onboarding, reset, etc.).
  • Security

    • signature (string, optional but required if your tenant enforces signatures): base64 ECDSA P-256 signature over `${tenantName},${applicationReference},${timestamp}`. See Signature for the full process.
    • timestamp (number, optional): Milliseconds since Unix epoch, used with signature.
    • sign (function, optional): deferred-signing callback (req: SignRequest) => Promise<SignedPayload>. Preferred when your private key is on a backend, or when a signature must be minted at /liveness/init time instead of at config-build. See Signature → Method 3.
  • User experience

    • showPreflight (boolean, default false): Show a preflight dashboard before the check starts, so users can confirm camera, microphone and lighting.
  • Additional checks

    • ageCheck (boolean, default false): Enable age estimation; result includes a trimmed‑mean estimated age.
    • referenceImageBase64 (string, optional): Data URL of a reference image to compare against (e.g. data:image/jpeg;base64,...). Enables likeness scoring and reference image hashing.

We intentionally do not document every low‑level option here to keep the guide focused on the most relevant settings for integrators. If you need to tune advanced behavior, contact support and we can advise on best practices.


Accessibility

The liveness UI aims to conform to WCAG 2.2 AA and works out of the box:

  • Set a language on your page. The SDK copies your document's <html lang="…"> onto its own UI so screen readers announce it correctly, falling back to English (en) if none is set.
  • Spoken instructions. Each liveness step ("turn your head left", "look up", …) is announced to screen readers as it changes, so a candidate who can't see the screen still hears what to do next. The overlay is a proper modal dialog with a trapped focus order.
  • Preflight and consent dialogs are keyboard-operable and restore focus when they close.
  • Reduced motion is respected automatically for candidates who enable it at the OS level.

No configuration is required. These behaviors do not change what data leaves the candidate's device.


Troubleshooting

“Liveness check not enabled in configuration”

  • The tenant has liveness disabled.
  • Ask a tenant manager to enable “Liveness check” in the tenant settings.

“SDK not loaded” or window.Proctor is undefined

  • Make sure the SDK script tag is present and loaded before your code runs.
  • Verify you are using the correct script URL and that no ad‑blocker or CSP is blocking it.

CORS or “Origin not allowed” errors

  • Your application origin is not in the tenant’s allowed origins list.
  • Ask an administrator to add your domain (production, staging, and local development) to the allowed origins, as described in the main integration guide.

User cannot start camera or preflight fails

  • The browser may have blocked camera/mic permissions.
  • Ask the user to enable camera access for your site.
  • Recommend good lighting and a stable internet connection.

Liveness fails repeatedly

Common causes:

  • The user does not follow the head‑movement instructions.
  • Lighting is too poor or the face is partially outside the frame.

Recommendations:

  • Show a clear error message and offer a retry button.
  • Provide brief tips (e.g. “Center your face in the frame and try again”).

Result verification fails on the backend

  • Ensure you are passing the exact livenessSessionId and resultBinding values returned by the SDK.
  • Check that you are calling the correct base URL for your environment.
  • Log the error response from /api/proctor/liveness/result for more detail and contact support if needed.

  • Full proctoring integration: For continuous exam monitoring with a trust score and rich timeline, see the main ProctorSafe Integration Guide.
  • Upgrading from liveness‑only to full proctoring:
    • Keep your existing liveness flow for onboarding or high‑risk actions.
    • Add Proctor.start(...) sessions for exams or long‑running activities, reusing your tenant configuration and signature setup.
  • Support:

If you are unsure whether you should use liveness only or full proctoring, start with this liveness guide for single‑step verification, and add full proctoring later for ongoing monitoring.