facer-widget
v1.0.9
Published
Drop-in camera widget for the Facer face recognition API — vanilla & React
Downloads
1,172
Readme
facer-widget
Drop-in face recognition camera widget for the Facer API. Works as a plain HTML custom element or a React component — no framework required.
Installation
npm install facer-widgetOr via CDN (no install needed):
<script src="https://unpkg.com/facer-widget/dist/facer-widget.js"></script>Quick Start
Vanilla HTML
<!doctype html>
<html>
<head>
<script src="https://unpkg.com/facer-widget/dist/facer-widget.js"></script>
</head>
<body>
<facer-cam
mode="search"
api-url="https://your-api.com"
token-url="https://your-backend.com/session-token"
collection="staff"
></facer-cam>
<script>
document.querySelector("facer-cam").addEventListener("facer:result", (e) => {
console.log("Result:", e.detail);
});
</script>
</body>
</html>React
npm install facer-widgetSearch
import { useState } from "react";
import { FacerCam } from "facer-widget/react";
import type { SearchResult } from "facer-widget";
export function FaceSearch() {
const [result, setResult] = useState<SearchResult | null>(null);
return (
<div>
<FacerCam
mode="search"
apiUrl="https://your-api.com"
tokenUrl="https://your-backend.com/session-token"
collection="staff"
topK={5}
onResult={(r) => setResult(r as SearchResult)}
onError={(e) => console.error(e.error)}
style={{ width: "100%", maxWidth: 400 }}
/>
{result?.results.map((match) => (
<p key={match.face_id}>
{match.person_id} — {Math.round(match.confidence * 100)}%
</p>
))}
</div>
);
}Enroll
import { FacerCam } from "facer-widget/react";
import type { EnrollResult } from "facer-widget";
export function FaceEnroll() {
return (
<FacerCam
mode="enroll"
apiUrl="https://your-api.com"
tokenUrl="https://your-backend.com/session-token"
collection="staff"
personId="john_doe"
metadata={JSON.stringify({ department: "engineering" })}
onResult={(r) => {
const res = r as EnrollResult;
if (res.enrolled) console.log("Enrolled:", res.face_id);
}}
style={{ width: "100%", maxWidth: 400 }}
/>
);
}Match (compare two faces)
import { FacerCam } from "facer-widget/react";
import type { MatchResult } from "facer-widget";
export function FaceMatch() {
return (
<FacerCam
mode="match"
apiUrl="https://your-api.com"
tokenUrl="https://your-backend.com/session-token"
onResult={(r) => {
const res = r as MatchResult;
console.log(res.match ? "Match!" : "No match", res.confidence);
}}
style={{ width: "100%", maxWidth: 400 }}
/>
);
}ID Verify (selfie vs ID photo)
import { FacerCam } from "facer-widget/react";
import type { VerifyResult } from "facer-widget";
export function IDVerify() {
return (
<FacerCam
mode="verify"
apiUrl="https://your-api.com"
tokenUrl="https://your-backend.com/session-token"
onResult={(r) => {
const res = r as VerifyResult;
console.log(res.match ? "Verified" : "Failed", res.confidence);
}}
style={{ width: "100%", maxWidth: 400 }}
/>
);
}Multi-mode with tab strip
import { FacerCam } from "facer-widget/react";
export function MultiMode() {
return (
<FacerCam
modes="detect,search,enroll"
apiUrl="https://your-api.com"
tokenUrl="https://your-backend.com/session-token"
collection="staff"
onResult={(r) => console.log(r)}
style={{ width: "100%", maxWidth: 400 }}
/>
);
}Fetch session token manually
If you prefer to manage the token yourself instead of using tokenUrl:
import { useState, useEffect } from "react";
import { FacerCam } from "facer-widget/react";
export function WithManualToken() {
const [token, setToken] = useState<string | null>(null);
useEffect(() => {
async function fetchToken() {
const res = await fetch("https://your-backend.com/session-token", { method: "POST" });
const data = await res.json();
setToken(data.token);
}
fetchToken();
// Refresh every 4 minutes — tokens expire in 5 minutes by default
const id = setInterval(fetchToken, 4 * 60 * 1000);
return () => clearInterval(id);
}, []);
if (!token) return <p>Loading…</p>;
return (
<FacerCam
mode="search"
apiUrl="https://your-api.com"
sessionToken={token}
collection="staff"
onResult={(r) => console.log(r)}
style={{ width: "100%", maxWidth: 400 }}
/>
);
}ESM (Vite / webpack)
import "facer-widget";
// <facer-cam> is now registered as a custom elementAuthentication
The widget supports two credential types:
Session token (recommended for browsers)
Your backend mints a short-lived token using your secret API key and passes it to the widget. The widget never sees your secret key.
<!-- Widget auto-fetches and refreshes the token -->
<facer-cam
token-url="https://your-backend.com/session-token"
mode="search"
></facer-cam>Your backend endpoint (POST /session-token) should call:
POST https://your-api.com/face/session-token
Authorization: Bearer <your-secret-api-key>
Content-Type: application/json
{ "duration_seconds": 300 }And return:
{ "token": "v4.local...." }Secret API key (server-side only)
<!-- Never use a secret key in a public webpage -->
<facer-cam api-key="sk_live_..." mode="detect"></facer-cam>Modes
| Mode | Description | Call limit |
|------|-------------|-----------|
| detect | Detects faces in the camera frame | 2 per session |
| search | Searches enrolled faces in a collection for a match | 2 per session |
| enroll | Auto-enrolls a face when detected — requires person-id prop | 3 per session |
| match | Compares two faces captured from the camera | unlimited |
| verify | Compares a selfie against an ID photo (upload or camera) | unlimited |
Once the per-session limit is reached the widget emits facer:error with context: "limit_reached" and stops scanning. The user must reload to reset.
Enable multiple modes with the modes attribute — the widget renders a tab strip:
<facer-cam modes="detect,search,enroll" ...></facer-cam>Attributes
| Attribute | Type | Description |
|-----------|------|-------------|
| mode | string | Active mode: detect | search | enroll | match | verify |
| modes | string | Comma-separated list of enabled modes (renders tab strip) |
| api-url | string | Base URL of your Facer API (default: https://api.facer.io) |
| session-token | string | Short-lived PASETO widget token |
| api-key | string | Secret API key (server-side only) |
| token-url | string | Your backend URL — widget POSTs here to fetch/refresh a session token |
| collection | string | Collection name for enroll and search modes |
| person-id | string | Required for enroll. Set by the developer — the widget auto-enrolls as soon as a face is detected. Not a user input. |
| metadata | string | Optional JSON metadata string stored with the enrolled face |
| top-k | number | Max results returned by search (default: 5) |
| auto | boolean | Continuously capture and submit at interval ms |
| interval | number | Milliseconds between auto-captures (default: 2000) |
Events
Listen for events on the <facer-cam> element:
const widget = document.querySelector("facer-cam");
widget.addEventListener("facer:result", (e) => {
// e.detail contains the API response
console.log(e.detail);
});
widget.addEventListener("facer:error", (e) => {
// e.detail.context: "token_fetch" | "limit_reached" | undefined
console.error(e.detail.error, e.detail.context);
});
widget.addEventListener("facer:token", (e) => {
// Fired when a session token is fetched/refreshed
console.log("Token expires in", e.detail.expires_in, "seconds");
});
widget.addEventListener("facer:usage", (e) => {
// Fired before every API call with running totals per mode
// e.detail: { detect, search, enroll, match, verify, total }
console.log("API calls so far:", e.detail.total, e.detail);
});Result shapes
detect
{
"count": 1,
"faces": [{ "bbox": [x0, y0, x1, y1], "confidence": 0.99 }]
}search
{
"collection": "staff",
"count": 1,
"results": [{
"person_id": "john_doe",
"confidence": 0.94,
"distance": 0.21,
"collection_name": "staff"
}]
}enroll
{
"enrolled": true,
"person_id": "john_doe",
"collection": "staff",
"face_id": "uuid"
}match / verify
{
"match": true,
"confidence": 0.96,
"distance": 0.18
}React Props
| Prop | Type | Description |
|------|------|-------------|
| mode | Mode | Active mode |
| modes | string | Comma-separated enabled modes |
| apiUrl | string | API base URL |
| sessionToken | string | Short-lived widget token |
| apiKey | string | Secret API key (server-side only) |
| tokenUrl | string | Backend URL for auto token fetch |
| collection | string | Collection name |
| personId | string | Person ID for enroll |
| metadata | string | Metadata for enroll |
| topK | number | Max search results |
| auto | boolean | Auto-capture mode |
| interval | number | Auto-capture interval (ms) |
| onResult | (result: FacerResult) => void | Result callback |
| onError | (detail: { error: string; context?: string }) => void | Error callback |
| onUsage | (counts: { detect: number; search: number; enroll: number; match: number; verify: number; total: number }) => void | Fired before each API call with running totals |
License
MIT
