Quick Start

Liveness is a challenge-response biometric API. Your backend creates a session, your frontend (or a hosted page) runs the camera checks, and you receive the result via webhook or direct API response.

01
Create session
Your server calls POST /v1/sessions with your API key and gets back a session ID.
02
User completes challenges
The browser SDK or hosted page opens the camera and submits frames every 800ms.
03
Receive result
Session status updates to passed or failed. Your webhook receives the payload.
Server — create a session (Node.js)
const res = await fetch('https://api.liveness.io/v1/sessions', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.LIVENESS_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ challenge_count: 5 }),
})
const { session_id, hosted_url } = await res.json()
// → send session_id / hosted_url to the client
Client — run checks with the browser SDK
import { LivenessClient } from '@liveness/sdk'

const client = new LivenessClient({ apiUrl: 'https://api.liveness.io' })
const result = await client.run(sessionId, videoElement, apiKey)
// result: { passed: true, score: 0.968, sessionId: '...' }

Authentication

All API requests require an X-API-Key header. Generate keys from the Dashboard. Keys are prefixed lv_ and are shown only once on creation.

curl https://api.liveness.io/v1/sessions \
  -H "X-API-Key: lv_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"challenge_count": 5}'

Keep your API key server-side

Never embed your API key in frontend JavaScript. Create sessions from your backend and pass only the session_id (or hosted_url) to the client.

Create Session

POST/v1/sessions

Creates a new session with a random set of challenges. Requires a valid API key. Sessions expire after 5 minutes.

Request body

FieldTypeDefaultDescription
challenge_countinteger5Number of challenges (1–8)

Response

{
  "session_id":        "a1b2c3d4-...",
  "challenges":        ["BLINK", "SMILE", "TURN_LEFT", "OPEN_MOUTH", "RAISE_EYEBROWS"],
  "current_challenge": "BLINK",
  "challenge_index":   0,
  "total_challenges":  5,
  "expires_in_seconds": 300,
  "hosted_url":        "https://app.liveness.io/check?s=a1b2c3d4-..."
}

Verify Frame

POST/v1/sessions/:id/verify
POST/v1/sessions/:id/verify-public

Submit a base64-encoded JPEG frame for the current challenge. The frame is analyzed by the MediaPipe face mesh engine. Call this on a timer (every ~800ms) while the camera is running.

Request body

FieldTypeDescription
frame_b64stringBase64-encoded JPEG (no data URI prefix). Max 4MB.

Response — challenge in progress

{
  "challenge_passed": false,
  "face_detected":    true,
  "session_status":   "pending",
  "current_challenge": "BLINK",
  "confidence":       0.12,
  "message":          "Challenge not passed yet — keep trying"
}

Response — challenge passed, more remain

{
  "challenge_passed": true,
  "session_status":   "pending",
  "next_challenge":   "SMILE",
  "challenge_index":  1,
  "total_challenges": 5,
  "message":          "Great! Now: SMILE"
}

Response — session complete

{
  "challenge_passed":  true,
  "session_status":    "passed",
  "score":             0.968,
  "liveness_verified": true,
  "message":           "Liveness verified successfully"
}

Response — spoof detected

{
  "challenge_passed": false,
  "session_status":   "failed",
  "reason":           "spoof_detected",
  "message":          "Liveness check failed — please use a live camera"
}

Hosted Page (Mobile & WebView)

The simplest way to integrate on mobile. Create a session server-side, open thehosted_url in a browser or WebView. No SDK needed on the client.

Browser redirect

Open hosted_url in a new tab or redirect. The page closes or shows a result when done.

Mobile WebView

Embed in a WebView. Listen for postMessage to receive the result in-app.

React Native — WebView integration
import { WebView } from 'react-native-webview'
import { useRef } from 'react'

export default function LivenessScreen({ hostedUrl, onResult }) {
  const ref = useRef(null)

  return (
    <WebView
      ref={ref}
      source={{ uri: hostedUrl }}
      onMessage={(e) => {
        const { type, passed, score, session_id } = JSON.parse(e.nativeEvent.data)
        if (type === 'liveness_result') {
          onResult({ passed, score, session_id })
        }
      }}
    />
  )
}
Flutter — WebView integration
WebViewController()
  ..setJavaScriptMode(JavaScriptMode.unrestricted)
  ..addJavaScriptChannel('LivenessResult', onMessageReceived: (msg) {
    final data = jsonDecode(msg.message);
    // data['passed'], data['score'], data['session_id']
  })
  ..loadRequest(Uri.parse(hostedUrl));

The hosted page posts the message to both window.parent (iframe) andwindow (WebView). The message shape is:

{
  "type":       "liveness_result",
  "passed":     true,
  "score":      0.968,
  "session_id": "a1b2c3d4-..."
}

Webhooks

Configure a callback URL in your Dashboard under Integrations. We POST to this URL whenever a session completes (passed or failed).

Payload

{
  "event":      "session.completed",
  "session_id": "a1b2c3d4-...",
  "passed":     true,
  "score":      0.968,
  "reason":     null,
  "timestamp":  "2026-06-01T12:00:00.000Z"
}

Verifying delivery

Your endpoint must return a 2xx status. We retry once after 5 seconds on timeout or non-2xx. Webhook calls timeout after 5 seconds.

Express — receive webhook
app.post('/webhooks/liveness', express.json(), (req, res) => {
  const { event, session_id, passed, score } = req.body

  if (event === 'session.completed') {
    if (passed) {
      // unlock the user's account, continue onboarding flow, etc.
      console.log(`Session ${session_id} passed with score ${score}`)
    } else {
      console.log(`Session ${session_id} failed`)
    }
  }

  res.sendStatus(200)
})

Browser SDK

The browser SDK wraps camera access, frame capture, and the verify API into a single run() call. Use it when you want the camera UI embedded directly in your own page.

Installation

npm install @liveness/sdk

Usage

import { LivenessClient } from '@liveness/sdk'

const client = new LivenessClient({
  apiUrl: 'https://api.liveness.io',
})

// videoElement is an HTMLVideoElement already in the DOM
const result = await client.run(sessionId, videoElement, apiKey)

// result shape:
// { passed: boolean, score: number, sessionId: string }

if (result.passed) {
  console.log('Liveness score:', result.score) // 0.0 – 1.0
}

LivenessClient options

OptionTypeDescription
apiUrlstringBase URL of the Liveness API
frameIntervalnumberms between frame captures (default: 800)
jpegQualitynumberJPEG quality 0–1 (default: 0.8)

React component

Prefer a drop-in React component? Import LivenessWidget from your local components or use the hosted page instead.

import LivenessWidget from '@/components/LivenessWidget'

<LivenessWidget
  apiKey={sessionApiKey}
  challengeCount={5}
  onComplete={({ passed, score }) => {
    console.log('done', passed, score)
  }}
/>

Challenge Types

7 challenge types are available. 5 are chosen randomly per session so the sequence is unpredictable. Each is detected via MediaPipe 468-landmark face mesh.

IDLabelDetection method
BLINKBlink your eyesEye Aspect Ratio (EAR) below threshold
SMILESmileMouth width / face width ratio
TURN_LEFTTurn head leftNose X offset from face center
TURN_RIGHTTurn head rightNose X offset from face center
OPEN_MOUTHOpen mouth wideMouth gap / face height ratio
LOOK_DOWNLook downNose Y position relative to face
RAISE_EYEBROWSRaise eyebrowsEyebrow-to-eye gap / face height

Spoof detection

Every frame is also checked for Laplacian variance. A flat-printed photo or screen replay has very low texture variance and is immediately rejected with reason: "spoof_detected", failing the session regardless of challenge state.

Webhook Signing

Every webhook request includes an X-Liveness-Signature header so your backend can confirm the request came from us and not a third party.

How it works

01
Secret generated
A whsec_ secret is created when you save your webhook URL in the Dashboard.
02
Payload signed
We compute HMAC-SHA256 of the raw JSON body using your secret and attach it as a header.
03
You verify
Recompute the HMAC on your side and compare with timingSafeEqual to prevent timing attacks.

Request headers

POST /webhooks/liveness HTTP/1.1
Content-Type: application/json
X-Liveness-Signature: sha256=a3f1c8...

The value is always sha256= followed by the lowercase hex HMAC-SHA256 of the raw request body. Compute it against the raw bytes before any JSON parsing — parsing and re-serialising can change whitespace and break the check.

Verification — Node.js

Express
const crypto = require('crypto')

function verifyWebhook(rawBody, signatureHeader, secret) {
  const expected = 'sha256=' +
    crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
  // timingSafeEqual prevents timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signatureHeader),
    Buffer.from(expected)
  )
}

// Use express.raw() so req.body is a Buffer (unparsed)
app.post('/webhooks/liveness', express.raw({ type: '*/*' }), (req, res) => {
  const sig = req.headers['x-liveness-signature']
  if (!sig || !verifyWebhook(req.body, sig, process.env.WEBHOOK_SECRET)) {
    return res.sendStatus(401)
  }
  const { event, passed, score, session_id } = JSON.parse(req.body)
  if (event === 'session.completed' && passed) {
    // unlock account, continue onboarding, etc.
  }
  res.sendStatus(200)
})

Verification — Python

FastAPI / Flask
import hmac, hashlib, os

def verify_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)

# FastAPI
@app.post("/webhooks/liveness")
async def webhook(request: Request):
    raw = await request.body()
    sig = request.headers.get("x-liveness-signature", "")
    if not verify_webhook(raw, sig, os.environ["WEBHOOK_SECRET"]):
        raise HTTPException(status_code=401)
    payload = json.loads(raw)
    # handle payload …
    return {"ok": True}

Rotate your secret safely

If a secret is compromised, click Rotate secret in the Dashboard. Update your environment variable before the next webhook fires. Old and new secrets are never active at the same time.

Errors

All errors return a JSON body with an error string and an appropriate HTTP status code.

StatusMeaning
400 Bad RequestMissing or invalid field in the request body
401 UnauthorizedMissing or invalid API key
404 Not FoundSession ID does not exist
409 ConflictSession is no longer pending (already passed, failed, or expired)
410 GoneSession has expired (5-minute TTL exceeded)
429 Too Many RequestsRate limit hit — 30 frame submissions per 10 seconds
502 Bad GatewayDetection engine temporarily unavailable
{ "error": "Session is already 'passed'" }