v1 — now live

Face consistency
for AI pipelines

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.

REST API MCP Server OpenAI / Claude / n8n

The problem

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": {...} }
  ]
}

Workflow

How it works

01 Upload refs 2-5 photos of your character. Different angles help.
02 Generate as usual HeyGen, Midjourney, DALL-E, Flux, SD — any pipeline.
03 POST /checks Send the generated image or video URL.
04 Get verdict Similarity %, pass/fail, and fix prompts if it drifted.

Adjustable threshold

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.

Smart remediation

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]..."
  }
}

Under the hood

Architecture

Single Node.js process. SQLite for state. AWS Rekognition for face comparison. S3 for reference storage. No containers, no Kubernetes, no nonsense.

Your Pipeline | POST /checks | imageUrl + characterId v +--------------+ | FaceSync | Express on :4180 | REST API | Bearer token auth +--------------+ / | \ v v v SQLite S3 Rekognition state refs CompareFaces WAL mode us-east-2 per-reference +--------------+ | MCP Server | stdio transport | same lib/ | for Claude Desktop +--------------+ Connectors: /schemas/openai -> OpenAI function calling JSON /schemas/claude -> Claude tool_use JSON /schemas/openapi -> OpenAPI 3.0 spec (importable everywhere)
<200ms
avg check latency
6
MCP tools
15
REST endpoints
3
schema formats

Reference

API endpoints

Base URL: https://facesync.dev/api/v1

Auth: Authorization: Bearer fs_live_xxx — create an account to get a key. Shown once.

Accounts

POST/accountsCreate account. Returns API key (shown once). No auth required.
GET/accountAccount info, tier, usage stats, remaining checks.

Characters

POST/charactersCreate a character (reference set). { name, description? }
GET/charactersList all characters in your account.
GET/characters/:idCharacter detail + reference image list.
DEL/characters/:idDelete character and all S3 reference data.
POST/characters/:id/referencesUpload reference image. { imageUrl } or { imageBase64 }
DEL/characters/:id/references/:refIdRemove a single reference image.

Checks

POST/checksCheck candidate image against character. { characterId, imageUrl, threshold? }
POST/checks/batchBatch check multiple images. { characterId, images[], threshold? }
GET/checks/:idRetrieve a past check result by ID.
GET/checksList recent checks with scores.

Video

POST/video/extract-framesExtract frames from video URL. Returns base64 JPEGs. { videoUrl, fps?, maxFrames? }
POST/video/checkAll-in-one: extract frames + check each against character. Full video consistency report.

Schemas

GET/schemas/openaiOpenAI function calling JSON. Paste into Assistants or GPT Actions.
GET/schemas/claudeClaude tool_use JSON. Include in your Anthropic API tools array.
GET/schemas/openapiOpenAPI 3.0 spec. Import into Postman, n8n, Zapier, or any OpenAPI consumer.

New

Video frame checking

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.

How it works

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.

Plug in

Developer integrations

FaceSync speaks every format your AI pipeline already uses. Fetch a schema and wire it in.

curl
python
node.js
openai
claude / mcp
n8n / zapier
# 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

Self-host

Deploy your own instance

FaceSync is a single Node.js process. No Docker required. Runs on any $6/mo VPS.

Requirements

  • node ≥ 18
  • ffmpeg (for video checking)
  • AWS account with Rekognition + S3
  • ~512MB RAM

Stack

  • express — HTTP server
  • better-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
}

Pricing

Simple, usage-based

Start free. Upgrade when you need more checks or characters. Overage is billed at the end of the month — your pipeline never stops.

Free

$0
  • 50 checks / month
  • 1 character
  • Full API access
  • Image + video checks
  • Remediation prompts
  • Hard limit at 50

Pro

$29/mo
  • 5,000 checks / month
  • Unlimited characters
  • All integrations
  • Batch API + webhooks
  • Overage: $0.005/check
  • Priority support

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.

Get started in 30 seconds

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 }