One API call to verify your AI-generated avatar still looks like the same person. Checks images and video frames against reference photos. Returns a similarity score, pass/fail, and prompt fixes when it drifts.
You generate 50 images of your AI avatar. Image #1 looks perfect. By image #30, the jawline has shifted, the eyes are slightly different, and the nose is wrong. You can't tell at a glance, but your audience can. HeyGen video renders it worse — face drift happens frame by frame inside the animation.
FaceSync catches it programmatically so your pipeline can reject bad outputs and retry with corrected prompts — without human review.
# check a generated image in one call
curl https://facesync.dev/api/v1/checks \
-H "Authorization: Bearer fs_live_..." \
-d '{
"characterId": "chr_a1b2c3",
"imageUrl": "https://cdn.example.com/gen_047.png",
"threshold": 90
}'
# response
{
"overallSimilarity": 94.7,
"isConsistent": true,
"referenceResults": [
{ "similarity": 96.1, "boundingBox": {...} },
{ "similarity": 93.2, "boundingBox": {...} }
]
}
Set the threshold parameter (0–100) to control strictness. Think of it as a temperature dial for face consistency:
95-100 Strict. Same angle, same lighting.
Product shots, headshots.
85-95 Recommended. Natural expression
and angle variation allowed.
70-85 Lenient. Stylistic variation OK.
Cartoon/illustration of same face.
<70 Very lenient. "Same vibe" checks
across different art styles.
When a face fails, you don't just get "fail." You get actionable prompt fixes for the next generation:
{
"remediation": {
"severity": "medium",
"gap": 17.6,
"suggestions": [
"Add specific facial descriptors:
eye shape, jawline, nose bridge",
"Lock the face with IP-Adapter
or face-swap post-process"
],
"promptSnippet": "Same person as
reference, consistent [eye shape,
jawline, nose width]..."
}
}
Single Node.js process. SQLite for state. AWS Rekognition for face comparison. S3 for reference storage. No containers, no Kubernetes, no nonsense.
Base URL: https://facesync.dev/api/v1
Auth: Authorization: Bearer fs_live_xxx — create an account to get a key. Shown once.
{ name, description? }{ imageUrl } or { imageBase64 }{ characterId, imageUrl, threshold? }{ characterId, images[], threshold? }{ videoUrl, fps?, maxFrames? }tools array.Still-image checks can't catch face drift that happens inside a video. HeyGen, Synthesia, and other avatar tools can render a perfect first frame and then subtly morph the face during animation. FaceSync's video endpoint catches this.
Send a video URL. FaceSync downloads it, extracts frames with ffmpeg (1fps default, up to 5fps), and runs each frame through Rekognition against your character's references.
Returns per-frame similarity scores, timestamps, overall pass rate, and the worst frame. A video passes if ≥80% of frames are consistent.
# check a HeyGen render
curl https://facesync.dev/api/v1/video/check \
-H "Authorization: Bearer fs_live_..." \
-d '{
"videoUrl": "https://cdn.heygen.com/v/abc.mp4",
"characterId": "chr_a1b2c3",
"fps": 2,
"threshold": 88
}'
# response
{
"overall": {
"passRate": 0.92,
"averageSimilarity": 91.3,
"isConsistent": true,
"worstFrame": {
"frameIndex": 14,
"timestampSeconds": 7.0,
"similarity": 82.1
}
},
"frameResults": [...]
}
n8n / Make integration: Can't run ffmpeg in your cloud automation? Use /video/extract-frames to get base64 frames, then pipe each to /checks or to Claude vision for a second opinion. The server-side extraction runs on our infrastructure.
FaceSync speaks every format your AI pipeline already uses. Fetch a schema and wire it in.
# 1. create an account
curl -X POST https://facesync.dev/api/v1/accounts \
-H "Content-Type: application/json" \
-d '{"email": "dev@example.com"}'
# -> { "apiKey": "fs_live_a8f3...", "id": "acc_..." }
# save the key. it's shown once.
# 2. create a character
curl -X POST https://facesync.dev/api/v1/characters \
-H "Authorization: Bearer fs_live_a8f3..." \
-H "Content-Type: application/json" \
-d '{"name": "Elena", "description": "Brand avatar, Latina, 30s"}'
# 3. upload reference images (2-5 recommended)
curl -X POST https://facesync.dev/api/v1/characters/CHR_ID/references \
-H "Authorization: Bearer fs_live_a8f3..." \
-H "Content-Type: application/json" \
-d '{"imageUrl": "https://cdn.example.com/elena-front.jpg"}'
# 4. check a generated image
curl -X POST https://facesync.dev/api/v1/checks \
-H "Authorization: Bearer fs_live_a8f3..." \
-H "Content-Type: application/json" \
-d '{"characterId": "CHR_ID", "imageUrl": "https://cdn.example.com/gen_047.png", "threshold": 90}'
import requests
API_KEY = "fs_live_a8f3..."
BASE = "https://facesync.dev/api/v1"
headers = {"Authorization": f"Bearer {API_KEY}"}
# create character
char = requests.post(f"{BASE}/characters", headers=headers, json={
"name": "Elena",
"description": "Brand avatar"
}).json()
# upload reference
requests.post(f"{BASE}/characters/{char['id']}/references",
headers=headers,
json={"imageUrl": "https://cdn.example.com/ref.jpg"})
# check a generated image
result = requests.post(f"{BASE}/checks", headers=headers, json={
"characterId": char["id"],
"imageUrl": "https://cdn.example.com/gen_047.png",
"threshold": 90
}).json()
if result["isConsistent"]:
print(f"PASS: {result['overallSimilarity']}% match")
else:
print(f"FAIL: {result['overallSimilarity']}%")
for tip in result["remediation"]["suggestions"]:
print(f" fix: {tip}")
# check a video for face drift
video = requests.post(f"{BASE}/video/check", headers=headers, json={
"videoUrl": "https://cdn.heygen.com/render.mp4",
"characterId": char["id"],
"fps": 2,
"threshold": 88
}).json()
print(f"Video pass rate: {video['overall']['passRate']}")
const API_KEY = "fs_live_a8f3...";
const BASE = "https://facesync.dev/api/v1";
const headers = {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json"
};
// check a generated image
const res = await fetch(`${BASE}/checks`, {
method: "POST",
headers,
body: JSON.stringify({
characterId: "chr_a1b2c3",
imageUrl: "https://cdn.example.com/gen_047.png",
threshold: 90
})
});
const result = await res.json();
console.log(result.isConsistent
? `PASS: ${result.overallSimilarity}%`
: `FAIL: ${result.overallSimilarity}% — ${result.remediation.suggestions[0]}`
);
// Option 1: OpenAI Assistants — fetch tools from the API
const tools = await fetch("https://facesync.dev/api/v1/schemas/openai")
.then(r => r.json());
const assistant = await openai.beta.assistants.create({
model: "gpt-4o",
tools: tools.map(t => ({ type: "function", function: t })),
instructions: "You verify face consistency for AI avatars using FaceSync."
});
// Option 2: Custom GPT Actions — paste the OpenAPI spec
// GET https://facesync.dev/api/v1/schemas/openapi
// Copy the JSON → GPT Builder → Actions → paste
// Set auth: API Key, header: Authorization, prefix: Bearer
// Option 3: Direct function calling
const response = await openai.chat.completions.create({
model: "gpt-4o",
tools: tools.map(t => ({ type: "function", function: t })),
messages: [{
role: "user",
content: "Check if gen_047.png matches Elena"
}]
});
// Option A: Claude API — tool_use
const tools = await fetch("https://facesync.dev/api/v1/schemas/claude")
.then(r => r.json());
const msg = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
tools: tools,
messages: [{ role: "user", content: "Check this face against Elena" }]
});
// Option B: MCP Server — add to claude_desktop_config.json
{
"mcpServers": {
"facesync": {
"command": "node",
"args": ["/path/to/facesync/mcp-server.js"],
"env": {
"MCP_API_KEY": "fs_live_xxx"
}
}
}
}
// or via SSH to a remote server:
"command": "ssh",
"args": ["user@server", "node", "/opt/facesync/mcp-server.js"]
n8n / Make / Zapier — use the HTTP Request node
Method: POST
URL: https://facesync.dev/api/v1/checks
Auth: Header Auth
Name: Authorization
Value: Bearer fs_live_xxx
Body: JSON
{
"characterId": "{{ $json.characterId }}",
"imageUrl": "{{ $json.imageUrl }}",
"threshold": 90
}
Recommended n8n pipeline:
HeyGen Webhook
|
Extract Frames ──> POST /video/extract-frames
|
Loop Over Frames
|
Check Face ──────> POST /checks
|
IF isConsistent == false
|
Retry with ──────> use remediation.promptSnippet
fixed prompt in next HeyGen generation
Import the OpenAPI spec directly:
GET https://facesync.dev/api/v1/schemas/openapi
Paste into n8n's HTTP node or Zapier's custom API
FaceSync is a single Node.js process. No Docker required. Runs on any $6/mo VPS.
node ≥ 18ffmpeg (for video checking)express — HTTP serverbetter-sqlite3 — SQLite (WAL mode)@aws-sdk/client-rekognition@aws-sdk/client-s3@modelcontextprotocol/sdk — MCP# clone and install
git clone <repo> facesync && cd facesync
npm install
# configure
cp .env.example .env
# set: PORT, AWS_PROFILE, S3_BUCKET
# optional: MCP_API_KEY
# create S3 bucket (one-time)
aws s3 mb s3://your-bucket --region us-east-2
# run
node server.js
# -> FaceSync API on http://0.0.0.0:4180
# production: systemd + reverse proxy
sudo systemctl enable facesync
sudo systemctl start facesync
# Caddyfile (auto-SSL)
facesync.dev {
reverse_proxy localhost:4180
}
Start free. Upgrade when you need more checks or characters. Overage is billed at the end of the month — your pipeline never stops.
All plans include MCP, OpenAI, and Claude integrations. Video frame checking included at no extra per-frame cost — each frame counts as one check.
Payments via SumUp. All major cards. Cancel anytime.
Sign in to get your API key and start checking faces. No credit card required for the free tier.
Or create an account via API:
curl -X POST https://facesync.dev/api/v1/accounts \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com"}'
# -> { "apiKey": "fs_live_...", "tier": "free", "checksRemaining": 50 }