24 KiB
VibeCheck API Reference
Version: 0.1.0 Last Updated: 2026-02-06
Table of Contents
Core Library API
Installation
npm install @lilithftw/vibecheck-core
# or
bun add @lilithftw/vibecheck-core
LivenessDetector
The main class for performing liveness detection checks.
Constructor
new LivenessDetector(options?: LivenessOptions)
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
options |
LivenessOptions |
{} |
Configuration options |
LivenessOptions Interface:
interface LivenessOptions {
/**
* MediaPipe model asset path
* @default 'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task'
*/
modelAssetPath?: string;
/**
* Delegate for computation (GPU recommended)
* @default 'GPU'
*/
delegate?: 'CPU' | 'GPU';
/**
* Minimum confidence threshold (0.0 - 1.0)
* Lower = more lenient, Higher = more strict
* @default 0.7
*/
confidenceThreshold?: number;
/**
* Minimum number of blinks required
* @default 2
*/
minBlinks?: number;
/**
* Require head movement detection
* @default true
*/
requireHeadMovement?: boolean;
/**
* Enable depth consistency checking
* @default true
*/
enableDepthCheck?: boolean;
/**
* Maximum check duration in milliseconds
* @default 15000 (15 seconds)
*/
maxDuration?: number;
/**
* Video resolution constraints
* @default { width: 1280, height: 720 }
*/
videoConstraints?: MediaTrackConstraints;
/**
* Enable debug logging
* @default false
*/
debug?: boolean;
}
Example:
import { LivenessDetector } from '@lilithftw/vibecheck-core';
const detector = new LivenessDetector({
confidenceThreshold: 0.8,
minBlinks: 3,
requireHeadMovement: true,
debug: true
});
Methods
initialize()
Initialize MediaPipe and request camera permissions.
async initialize(): Promise<void>
Returns: Promise that resolves when initialization is complete
Throws:
CameraPermissionError- User denied camera permissionMediaPipeInitError- Failed to load MediaPipe modelsBrowserNotSupportedError- Browser lacks required features
Example:
try {
await detector.initialize();
console.log('Ready to check liveness');
} catch (error) {
if (error instanceof CameraPermissionError) {
console.error('Please allow camera access');
} else {
console.error('Initialization failed:', error);
}
}
check()
Perform the liveness detection check.
async check(): Promise<LivenessResult>
Returns: Promise that resolves with LivenessResult
Throws:
NotInitializedError- Must callinitialize()firstCheckTimeoutError- Check exceededmaxDurationNoFaceDetectedError- No face detected during check
Example:
try {
const result = await detector.check();
if (result.isLive) {
console.log(`Human verified with ${result.confidence * 100}% confidence`);
} else {
console.log('Failed liveness check');
}
} catch (error) {
console.error('Check failed:', error);
}
cleanup()
Release resources and stop camera.
cleanup(): void
Example:
// Always cleanup when done
detector.cleanup();
isInitialized()
Check if detector is initialized.
isInitialized(): boolean
Returns: true if initialized, false otherwise
Example:
if (!detector.isInitialized()) {
await detector.initialize();
}
Full Usage Example
import { LivenessDetector } from '@lilithftw/vibecheck-core';
async function verifyUser() {
const detector = new LivenessDetector({
confidenceThreshold: 0.75,
minBlinks: 2,
requireHeadMovement: true
});
try {
// Step 1: Initialize
await detector.initialize();
console.log('Camera ready, starting check...');
// Step 2: Perform check
const result = await detector.check();
// Step 3: Handle result
if (result.isLive && result.confidence >= 0.75) {
console.log('User verified!');
return true;
} else {
console.log('Verification failed');
return false;
}
} catch (error) {
console.error('Verification error:', error);
return false;
} finally {
// Step 4: Always cleanup
detector.cleanup();
}
}
Types
LivenessResult
The result of a liveness check.
interface LivenessResult {
/**
* Did the user pass the liveness check?
*/
isLive: boolean;
/**
* Confidence score (0.0 - 1.0)
* Higher = more confident
*/
confidence: number;
/**
* Timestamp when check completed (Unix epoch milliseconds)
*/
timestamp: number;
/**
* Detailed metrics (debug mode only)
*/
metrics?: {
blinks: {
count: number;
timestamps: number[];
durations: number[];
};
headMovement: {
leftTurn: { detected: boolean; magnitude: number };
rightTurn: { detected: boolean; magnitude: number };
nod: { detected: boolean; magnitude: number };
};
depth: {
consistent: boolean;
confidence: number;
};
duration: number; // milliseconds
};
}
Example:
const result: LivenessResult = {
isLive: true,
confidence: 0.923,
timestamp: 1707234567890
};
// With debug mode enabled:
const detailedResult: LivenessResult = {
isLive: true,
confidence: 0.923,
timestamp: 1707234567890,
metrics: {
blinks: {
count: 3,
timestamps: [1234.5, 2345.6, 3456.7],
durations: [150, 180, 165]
},
headMovement: {
leftTurn: { detected: true, magnitude: 0.23 },
rightTurn: { detected: true, magnitude: 0.19 },
nod: { detected: true, magnitude: 0.18 }
},
depth: {
consistent: true,
confidence: 0.87
},
duration: 5234
}
};
Errors
All VibeCheck errors extend VibeCheckError.
class VibeCheckError extends Error {
code: string;
constructor(message: string, code: string);
}
Error Types
CameraPermissionError
User denied camera permission.
class CameraPermissionError extends VibeCheckError {
code: 'CAMERA_PERMISSION_DENIED';
}
Handling:
try {
await detector.initialize();
} catch (error) {
if (error instanceof CameraPermissionError) {
// Show UI: "Please allow camera access"
}
}
MediaPipeInitError
Failed to load MediaPipe models.
class MediaPipeInitError extends VibeCheckError {
code: 'MEDIAPIPE_INIT_FAILED';
}
Common Causes:
- Network offline during model download
- CDN unavailable
- CORS issues with custom
modelAssetPath
BrowserNotSupportedError
Browser lacks required features.
class BrowserNotSupportedError extends VibeCheckError {
code: 'BROWSER_NOT_SUPPORTED';
}
Required Features:
- WebRTC (
getUserMedia) - WebAssembly
- WebGL 2.0
NotInitializedError
Called check() before initialize().
class NotInitializedError extends VibeCheckError {
code: 'NOT_INITIALIZED';
}
CheckTimeoutError
Check exceeded maxDuration.
class CheckTimeoutError extends VibeCheckError {
code: 'CHECK_TIMEOUT';
}
NoFaceDetectedError
No face detected during check.
class NoFaceDetectedError extends VibeCheckError {
code: 'NO_FACE_DETECTED';
}
Common Causes:
- Poor lighting
- Face not in frame
- Webcam obstructed
Error Handling Example
import {
LivenessDetector,
CameraPermissionError,
NoFaceDetectedError,
CheckTimeoutError
} from '@lilithftw/vibecheck-core';
async function safeCheck() {
const detector = new LivenessDetector();
try {
await detector.initialize();
const result = await detector.check();
return result;
} catch (error) {
if (error instanceof CameraPermissionError) {
return { error: 'Please allow camera access' };
} else if (error instanceof NoFaceDetectedError) {
return { error: 'No face detected. Ensure good lighting.' };
} else if (error instanceof CheckTimeoutError) {
return { error: 'Check timed out. Please try again.' };
} else {
return { error: 'Unexpected error. Please contact support.' };
}
} finally {
detector.cleanup();
}
}
React Component API
Installation
npm install @lilithftw/vibecheck-react
# or
bun add @lilithftw/vibecheck-react
Note: Requires React 18.0+ or React 19.0+
VibeCheck Component
High-level component with built-in UI.
import { VibeCheck } from '@lilithftw/vibecheck-react';
Props
interface VibeCheckProps {
/**
* Callback when liveness check succeeds
*/
onSuccess: (result: LivenessResult) => void;
/**
* Callback when liveness check fails or errors
*/
onFailure: (error: VibeCheckError) => void;
/**
* Callback when status changes (optional)
*/
onStatusChange?: (status: CheckStatus) => void;
/**
* Liveness detector configuration (optional)
*/
config?: LivenessOptions;
/**
* Theme for built-in UI (optional)
* @default 'light'
*/
theme?: 'light' | 'dark' | Theme;
/**
* Custom CSS class name (optional)
*/
className?: string;
/**
* Custom inline styles (optional)
*/
style?: React.CSSProperties;
/**
* Auto-start check on mount (optional)
* @default false
*/
autoStart?: boolean;
/**
* Show video preview during check (optional)
* @default true
*/
showVideo?: boolean;
/**
* Custom instruction text (optional)
*/
instructions?: string | React.ReactNode;
}
CheckStatus Type:
type CheckStatus =
| 'idle' // Not started
| 'initializing' // Loading models, requesting camera
| 'ready' // Ready to start check
| 'checking' // Actively checking liveness
| 'success' // Check passed
| 'failure' // Check failed
| 'error'; // Error occurred
Theme Interface:
interface Theme {
colors: {
primary: string;
background: string;
text: string;
error: string;
success: string;
};
borderRadius: string;
fontFamily: string;
}
Basic Example
import { VibeCheck } from '@lilithftw/vibecheck-react';
function RegistrationPage() {
const handleSuccess = (result) => {
console.log('User verified!', result);
// Proceed with registration
};
const handleFailure = (error) => {
console.error('Verification failed:', error);
// Show error message
};
return (
<div>
<h1>Create Account</h1>
<VibeCheck
onSuccess={handleSuccess}
onFailure={handleFailure}
/>
</div>
);
}
Advanced Example
import { VibeCheck } from '@lilithftw/vibecheck-react';
import { useState } from 'react';
function AdvancedCheck() {
const [status, setStatus] = useState('idle');
return (
<div>
<p>Status: {status}</p>
<VibeCheck
onSuccess={(result) => {
console.log('Confidence:', result.confidence);
// Send result to server
fetch('/api/verify-liveness', {
method: 'POST',
body: JSON.stringify(result)
});
}}
onFailure={(error) => {
alert(`Failed: ${error.message}`);
}}
onStatusChange={setStatus}
config={{
confidenceThreshold: 0.8,
minBlinks: 3,
debug: true
}}
theme="dark"
showVideo={true}
instructions="Please blink twice and turn your head left and right"
/>
</div>
);
}
useVibeCheck Hook
Headless hook for custom UI implementations.
import { useVibeCheck } from '@lilithftw/vibecheck-react';
Signature
function useVibeCheck(
options?: LivenessOptions
): UseVibeCheckReturn
Returns:
interface UseVibeCheckReturn {
/**
* Is the detector initialized?
*/
isInitialized: boolean;
/**
* Is a check currently running?
*/
isChecking: boolean;
/**
* Current check status
*/
status: CheckStatus;
/**
* Result of last check (if successful)
*/
result: LivenessResult | null;
/**
* Error from last check (if failed)
*/
error: VibeCheckError | null;
/**
* Start the liveness check
*/
startCheck: () => Promise<void>;
/**
* Reset state to initial
*/
reset: () => void;
/**
* Cleanup resources (call on unmount)
*/
cleanup: () => void;
}
Example
import { useVibeCheck } from '@lilithftw/vibecheck-react';
import { useEffect } from 'react';
function CustomCheckUI() {
const {
isInitialized,
isChecking,
status,
result,
error,
startCheck,
reset,
cleanup
} = useVibeCheck({
confidenceThreshold: 0.75
});
// Cleanup on unmount
useEffect(() => {
return () => cleanup();
}, [cleanup]);
return (
<div>
{/* Status display */}
<div>Status: {status}</div>
{/* Start button */}
{status === 'idle' && (
<button onClick={startCheck} disabled={!isInitialized}>
Start Verification
</button>
)}
{/* Loading indicator */}
{isChecking && <div>Checking... Please blink and move your head</div>}
{/* Success message */}
{result && (
<div>
✅ Verified! Confidence: {(result.confidence * 100).toFixed(1)}%
</div>
)}
{/* Error message */}
{error && (
<div>
❌ Failed: {error.message}
<button onClick={reset}>Try Again</button>
</div>
)}
</div>
);
}
useLivenessDetector Hook
Low-level hook that exposes the detector instance directly.
import { useLivenessDetector } from '@lilithftw/vibecheck-react';
Signature
function useLivenessDetector(
options?: LivenessOptions
): LivenessDetector | null
Returns: LivenessDetector instance or null if not initialized
Example
import { useLivenessDetector } from '@lilithftw/vibecheck-react';
import { useEffect, useState } from 'react';
function LowLevelControl() {
const detector = useLivenessDetector({ debug: true });
const [initialized, setInitialized] = useState(false);
useEffect(() => {
if (detector) {
detector.initialize().then(() => setInitialized(true));
return () => detector.cleanup();
}
}, [detector]);
const handleCheck = async () => {
if (!detector) return;
try {
const result = await detector.check();
console.log('Result:', result);
} catch (error) {
console.error('Error:', error);
}
};
return (
<div>
<button onClick={handleCheck} disabled={!initialized}>
Start Check
</button>
</div>
);
}
Examples
Vanilla JavaScript
import { LivenessDetector } from '@lilithftw/vibecheck-core';
// Initialize on page load
const detector = new LivenessDetector();
document.getElementById('verify-btn').addEventListener('click', async () => {
try {
// Initialize if needed
if (!detector.isInitialized()) {
await detector.initialize();
}
// Perform check
const result = await detector.check();
if (result.isLive) {
alert('Verified!');
} else {
alert('Failed verification');
}
} catch (error) {
console.error('Error:', error);
alert('Verification error: ' + error.message);
}
});
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
detector.cleanup();
});
React with Form
import { VibeCheck } from '@lilithftw/vibecheck-react';
import { useState } from 'react';
function RegistrationForm() {
const [verified, setVerified] = useState(false);
const [formData, setFormData] = useState({ email: '', password: '' });
const handleSubmit = async (e) => {
e.preventDefault();
if (!verified) {
alert('Please complete liveness check');
return;
}
// Submit registration
const response = await fetch('/api/register', {
method: 'POST',
body: JSON.stringify(formData)
});
if (response.ok) {
alert('Registration successful!');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required
/>
<input
type="password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
required
/>
{/* Liveness check */}
{!verified && (
<VibeCheck
onSuccess={(result) => {
setVerified(true);
console.log('Verified with confidence:', result.confidence);
}}
onFailure={(error) => {
alert('Verification failed: ' + error.message);
}}
/>
)}
{verified && <p>✅ Liveness verified</p>}
<button type="submit" disabled={!verified}>
Create Account
</button>
</form>
);
}
Next.js (App Router)
'use client';
import { VibeCheck } from '@lilithftw/vibecheck-react';
import { useRouter } from 'next/navigation';
export default function VerifyPage() {
const router = useRouter();
return (
<div>
<h1>Bot Check</h1>
<VibeCheck
onSuccess={async (result) => {
// Send result to API route
const response = await fetch('/api/verify-liveness', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result)
});
if (response.ok) {
router.push('/dashboard');
}
}}
onFailure={(error) => {
console.error('Verification failed:', error);
}}
theme="dark"
/>
</div>
);
}
Server-Side Validation (Express)
const express = require('express');
const app = express();
app.post('/api/verify-liveness', express.json(), (req, res) => {
const { isLive, confidence, timestamp } = req.body;
// Validation checks
if (typeof isLive !== 'boolean' || typeof confidence !== 'number') {
return res.status(400).json({ error: 'Invalid result format' });
}
// Timestamp validation (prevent replay attacks)
const now = Date.now();
const age = now - timestamp;
if (age > 60000) { // 1 minute
return res.status(400).json({ error: 'Result too old' });
}
// Confidence threshold
if (!isLive || confidence < 0.7) {
return res.status(403).json({ error: 'Liveness check failed' });
}
// Rate limiting (use Redis in production)
// ...
// Store result in session/database
req.session.livenessVerified = true;
req.session.livenessTimestamp = timestamp;
res.json({ success: true, message: 'Verification successful' });
});
Custom Theme
import { VibeCheck, type Theme } from '@lilithftw/vibecheck-react';
const customTheme: Theme = {
colors: {
primary: '#6366f1', // Indigo
background: '#1f2937', // Dark gray
text: '#f9fafb', // White
error: '#ef4444', // Red
success: '#10b981' // Green
},
borderRadius: '12px',
fontFamily: 'Inter, system-ui, sans-serif'
};
function ThemedCheck() {
return (
<VibeCheck
onSuccess={(result) => console.log('Success:', result)}
onFailure={(error) => console.error('Error:', error)}
theme={customTheme}
/>
);
}
Migration Guide
From Manual Implementation
If you're currently using MediaPipe directly:
Before (Manual MediaPipe):
import { FaceLandmarker, FilesetResolver } from '@mediapipe/tasks-vision';
// Manual initialization
const vision = await FilesetResolver.forVisionTasks('...');
const landmarker = await FaceLandmarker.createFromOptions(vision, {...});
// Manual video stream
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
// Manual landmark detection
const results = landmarker.detectForVideo(videoElement, timestamp);
// Manual blink detection logic
// ... 100+ lines of custom code
After (VibeCheck):
import { LivenessDetector } from '@lilithftw/vibecheck-core';
const detector = new LivenessDetector();
await detector.initialize();
const result = await detector.check();
detector.cleanup();
Migration Benefits:
- ✅ 100+ lines → 4 lines
- ✅ Built-in blink/head movement detection
- ✅ Error handling included
- ✅ Resource cleanup automated
Version Updates
Breaking Changes from 0.0.x to 0.1.0:
-
Constructor signature changed:
// Old (0.0.x) new LivenessDetector(modelPath, options); // New (0.1.0) new LivenessDetector({ modelAssetPath: modelPath, ...options }); -
Result interface updated:
// Old (0.0.x) { success: boolean, score: number } // New (0.1.0) { isLive: boolean, confidence: number, timestamp: number } -
Error classes renamed:
// Old (0.0.x) PermissionError → CameraPermissionError InitError → MediaPipeInitError
Browser Support
VibeCheck requires WebRTC, WebAssembly, and WebGL 2.0 for MediaPipe face landmark detection.
Desktop Browsers
| Browser | Minimum Version | Status |
|---|---|---|
| Chrome | 80+ | Fully supported |
| Edge | 80+ | Fully supported (Chromium-based) |
| Firefox | 80+ | Fully supported |
| Safari | 15+ | Supported (WebGL2 required) |
| Opera | 67+ | Fully supported (Chromium-based) |
Mobile Browsers
| Browser | Minimum Version | Status |
|---|---|---|
| Chrome Android | 80+ | Fully supported |
| Safari iOS | 15+ | Supported (WebGL2 required) |
| Samsung Internet | 13+ | Fully supported |
| Firefox Android | 80+ | Supported |
Not Supported
- Internet Explorer (all versions)
- Opera Mini
- Browsers without WebGL 2.0 support
- Browsers without WebAssembly support
Required Web APIs
| API | Used For |
|---|---|
getUserMedia (WebRTC) |
Camera access |
| WebAssembly | MediaPipe WASM runtime |
| WebGL 2.0 | GPU-accelerated face detection |
| ES2020+ | Core library runtime |
Feature Detection
VibeCheck throws BrowserNotSupportedError during initialize() if required features are missing. To check support proactively:
function isVibeCheckSupported(): boolean {
const hasWebRTC = !!(navigator.mediaDevices?.getUserMedia);
const hasWasm = typeof WebAssembly === 'object';
const canvas = document.createElement('canvas');
const hasWebGL2 = !!canvas.getContext('webgl2');
return hasWebRTC && hasWasm && hasWebGL2;
}
Known Limitations
- Safari < 15: Lacks WebGL 2.0 support required by MediaPipe
- iOS browsers: All use WebKit engine; requires iOS 15+ for WebGL 2.0
- Firefox on some Linux: May need GPU drivers configured for WebGL 2.0
- Incognito/Private modes: Some browsers restrict
getUserMediain private browsing
Need Help?
- GitHub Issues: https://github.com/LilithFTW/vibecheck/issues
- Documentation: https://vibecheck.lilithftw.com/docs
Maintained by: LilithFTW License: MIT Last Review: 2026-02-11