LLM Guides

How to Do Face Comparison Using JavaScript

Aug 05, 2026

Face comparison sounds like a clean little API feature.

Upload image one. Upload image two. Ask the API if the faces match. Return a score. Done.

Then real life shows up with a blurry selfie, bad lighting, sunglasses, a cropped ID photo, a 7-year-old profile picture, and someone asking why the API says “maybe” instead of “yes.”

So yes, you can do face comparison using JavaScript.

But a useful face comparison workflow needs more than two image uploads and a similarity score. You need consent, image quality checks, clear thresholds, false match handling, privacy rules, and review logic around the API result.

In this guide, we’ll walk through a simple API-powered setup for comparing two face images and returning match results with JavaScript. We’ll also cover the best practices that matter when this feature touches real users.

What is face comparison?

Face comparison is a one-to-one face matching task.

You give the system two images:

source image → target image

The system compares the face in the first image with the face in the second image and returns whether they appear to belong to the same person.

Example response:

{
  "match": true,
  "similarity": 96.4,
  "confidence": "high"
}

This is commonly used for:

  1. Account verification.
  2. ID selfie checks.
  3. Employee check-in systems.
  4. User profile verification.
  5. KYC onboarding.
  6. Access control workflows.
  7. Fraud review.
  8. Duplicate account checks.
  9. Attendance systems.
  10. Internal identity workflows.

The important part: face comparison is usually one-to-one verification.

That means:

Does this selfie match this reference image?

This is different from one-to-many face search, where a system tries to find a person inside a database of many faces. One-to-many search has a different risk profile and usually needs much stricter legal, privacy, and policy review.

Face comparison vs face recognition

People often use these terms casually, but they are not always the same thing.

TermMeaning
Face detectionFinds whether a face exists in an image
Face comparisonCompares two face images
Face verificationConfirms whether two faces likely belong to the same person
Face identificationSearches one face against many enrolled faces
Liveness detectionChecks whether the person is physically present, not using a photo or replay attack
Face analysisEstimates attributes like pose, quality, occlusion, or emotion

For this article, we’re focusing on face comparison:

image A + image B → similarity score + match decision

That is the workflow you’d usually build for ID verification, account recovery, or profile matching.

Why we can write this guide

We’ve spent around 6 years working with AI APIs, computer vision workflows, document processing, identity-style automation, image analysis, and developer tutorials. We also checked current provider docs for Amazon Rekognition, Azure AI Face, and NIST face recognition evaluation resources while preparing this guide.

The practical lesson is simple: face comparison should be treated as a risk signal, not a magical identity truth machine.

NIST’s Face Recognition Technology Evaluation tracks one-to-one verification performance across algorithms, and NIST’s demographic effects page highlights that false match and false non-match behavior can vary by algorithm, demographics, and image quality. That matters because face comparison results affect real people. Your app needs careful thresholds, review paths, and privacy controls.

How face comparison APIs work

Most face comparison APIs follow the same basic pattern.

  1. You send two images.
  2. The API detects faces.
  3. The API compares the face embeddings or face features.
  4. The API returns a similarity score.
  5. Your backend decides whether the result is a match, no match, or needs review.

A common backend workflow looks like this:

frontend upload
→ JavaScript backend
→ face comparison API
→ similarity score
→ threshold logic
→ match / no match / review

You do not want the frontend making the final decision by itself.

Your backend should own:

  1. API keys.
  2. Thresholds.
  3. Logging.
  4. Review routing.
  5. Security checks.
  6. Consent rules.
  7. Data retention.
  8. Error handling.

Which face comparison API should you use?

There are several API options.

APIGood fit
Amazon Rekognition CompareFacesAWS-native face comparison
Azure AI Face VerifyMicrosoft/Azure identity workflows
Face++ Compare APISimple face comparison and face analysis workflows
KairosFace recognition and verification use cases
Veriff/Onfido-style identity vendorsFull KYC/ID verification workflows
Custom modelSpecialized internal use cases with strong ML support

For a simple JavaScript tutorial, Amazon Rekognition is one of the easiest examples because Amazon Rekognition CompareFaces has a direct comparison operation and supports the AWS SDK for JavaScript. The CompareFaces API reference explains that it returns similarity scores, face bounding boxes, and confidence values for detected faces.

Azure is also a strong option if you use Microsoft infrastructure. The Azure Face Verify Face-to-Face REST API returns a verification result and confidence score after comparing two detected face IDs.

For this guide, we’ll use Amazon Rekognition for the main JavaScript example, then explain how the same pattern works with other providers.

Before you build: important safety and consent notes

Face comparison involves biometric data.

That means you need to be careful before collecting, processing, storing, or comparing face images.

Best practices:

  1. Get clear user consent.
  2. Explain why the face comparison is needed.
  3. Do not compare faces secretly.
  4. Do not store face images longer than needed.
  5. Avoid storing biometric templates unless required.
  6. Encrypt images and results.
  7. Limit employee/admin access.
  8. Add audit logs.
  9. Provide a manual review path.
  10. Follow local biometric privacy laws.

Depending on where your users are, laws and rules may apply around biometric identifiers, consent, retention, deletion, and disclosure. This is one of those areas where legal review is not optional if you are shipping a real product.

A safer user-facing message might look like this:

We’ll compare your selfie with your profile image to help verify your account. We only use this image for verification and delete it after processing according to our retention policy.

Keep it plain. People deserve to know what is happening to their face data.

What should a good face comparison response include?

A useful response should include more than true or false.

Example:

{
  "status": "success",
  "decision": "review_required",
  "similarity": 87.3,
  "threshold": 90,
  "confidence_level": "medium",
  "face_count": {
    "source": 1,
    "target": 1
  },
  "warnings": [
    "Similarity is close to threshold.",
    "Manual review recommended."
  ]
}

Useful fields:

FieldWhy it matters
decisionFinal app-level result
similarityProvider score
thresholdDecision cutoff used
confidence_levelUser-friendly interpretation
face_countDetects missing or multiple faces
warningsTells frontend/reviewer what happened
providerDebugging and analytics
request_idAudit trail
review_requiredWorkflow routing

Your app should make the decision, not blindly echo the provider.

Step 1: Create a Node.js project

Create a new folder:

mkdir face-comparison-js
cd face-comparison-js
npm init -y

Install packages:

npm install express multer dotenv @aws-sdk/client-rekognition zod sharp

We’ll use:

PackageWhy
expressBackend API
multerFile uploads
dotenvEnvironment variables
@aws-sdk/client-rekognitionAmazon Rekognition client
zodValidation
sharpImage metadata and preprocessing

Add this to package.json:

{
  "type": "module"
}

Create .env:

PORT=3000
AWS_REGION=us-east-1
FACE_MATCH_THRESHOLD=90
MAX_UPLOAD_MB=8

You’ll also need AWS credentials configured through environment variables, AWS profiles, IAM roles, or your deployment platform’s secret manager.

For local testing, you might use:

AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key

For production, use IAM roles or a proper secret manager instead of hardcoding credentials.

Step 2: Create the Express server

Create server.js:

import express from "express";
import dotenv from "dotenv";

dotenv.config();

const app = express();

app.get("/health", (req, res) => {
  res.json({
    status: "ok"
  });
});

const port = process.env.PORT || 3000;

app.listen(port, () => {
  console.log(`Face comparison API running on http://localhost:${port}`);
});

Run it:

node server.js

Test:

curl http://localhost:3000/health

Expected response:

{
  "status": "ok"
}

Step 3: Add file upload handling

Face comparison needs two files:

  1. Source image.
  2. Target image.

Create upload.js:

import multer from "multer";

const maxUploadMb = Number(process.env.MAX_UPLOAD_MB || 8);

export const upload = multer({
  storage: multer.memoryStorage(),
  limits: {
    fileSize: maxUploadMb * 1024 * 1024
  }
});

Using memory storage keeps the demo simple. For production, you may prefer temporary encrypted storage or direct upload to object storage.

Now update server.js:

import express from "express";
import dotenv from "dotenv";
import { upload } from "./upload.js";

dotenv.config();

const app = express();

app.get("/health", (req, res) => {
  res.json({
    status: "ok"
  });
});

app.post(
  "/compare-faces",
  upload.fields([
    { name: "source", maxCount: 1 },
    { name: "target", maxCount: 1 }
  ]),
  async (req, res) => {
    return res.json({
      message: "Files received.",
      files: {
        source: req.files?.source?.[0]?.originalname,
        target: req.files?.target?.[0]?.originalname
      }
    });
  }
);

const port = process.env.PORT || 3000;

app.listen(port, () => {
  console.log(`Face comparison API running on http://localhost:${port}`);
});

Test:

curl -X POST http://localhost:3000/compare-faces \
  -F "[email protected]" \
  -F "[email protected]"

Now the backend can receive both images.

Step 4: Validate image uploads

Before calling a face API, validate files.

Checks to add:

  1. File exists.
  2. MIME type is allowed.
  3. File size is acceptable.
  4. Image dimensions are usable.
  5. Image is not absurdly tiny.
  6. Image is not too large for your provider.
  7. File is actually an image.

Create imageValidation.js:

import sharp from "sharp";

const allowedMimeTypes = new Set([
  "image/jpeg",
  "image/png"
]);

export async function validateFaceImage(file, label) {
  const errors = [];

  if (!file) {
    errors.push(`${label} image is required.`);
    return errors;
  }

  if (!allowedMimeTypes.has(file.mimetype)) {
    errors.push(`${label} image must be JPEG or PNG.`);
  }

  try {
    const metadata = await sharp(file.buffer).metadata();

    if (!metadata.width || !metadata.height) {
      errors.push(`${label} image dimensions could not be detected.`);
    }

    if (metadata.width < 250 || metadata.height < 250) {
      errors.push(`${label} image is too small. Use at least 250x250 pixels.`);
    }

    if (metadata.width > 6000 || metadata.height > 6000) {
      errors.push(`${label} image is too large. Resize before uploading.`);
    }
  } catch {
    errors.push(`${label} file could not be read as an image.`);
  }

  return errors;
}

export async function validateFaceComparisonFiles(sourceFile, targetFile) {
  const sourceErrors = await validateFaceImage(sourceFile, "Source");
  const targetErrors = await validateFaceImage(targetFile, "Target");

  return [...sourceErrors, ...targetErrors];
}

Update the route:

import { validateFaceComparisonFiles } from "./imageValidation.js";

app.post(
  "/compare-faces",
  upload.fields([
    { name: "source", maxCount: 1 },
    { name: "target", maxCount: 1 }
  ]),
  async (req, res) => {
    const sourceFile = req.files?.source?.[0];
    const targetFile = req.files?.target?.[0];

    const validationErrors = await validateFaceComparisonFiles(
      sourceFile,
      targetFile
    );

    if (validationErrors.length > 0) {
      return res.status(400).json({
        error: "Invalid images.",
        details: validationErrors
      });
    }

    return res.json({
      message: "Images look valid."
    });
  }
);

This saves API calls and gives users better feedback.

Image quality tips before comparison

A lot of face comparison failures are image-quality failures.

Tell users to upload images where:

  1. The face is clearly visible.
  2. The image is not blurry.
  3. Lighting is even.
  4. The person is facing the camera.
  5. The face is not covered by sunglasses, masks, hats, or heavy shadows.
  6. There is one main face in the image.
  7. The face is not too small in the frame.
  8. The image is recent enough for the use case.
  9. The file is not heavily compressed.
  10. The image has not been filtered or edited too much.

Good frontend instruction:

Use a clear, recent photo with your face facing the camera. Avoid sunglasses, masks, heavy filters, or dark lighting.

This matters because better images reduce false non-matches.

Amazon’s CompareFaces API reference also notes that orientation correction depends on image metadata and that PNG/JPEG images without EXIF orientation information may not be corrected automatically. In normal app terms: badly rotated or stripped images can create weird results.

Step 5: Create the Rekognition client

Create rekognitionClient.js:

import {
  RekognitionClient,
  CompareFacesCommand
} from "@aws-sdk/client-rekognition";

export const rekognition = new RekognitionClient({
  region: process.env.AWS_REGION || "us-east-1"
});

export async function compareFacesWithRekognition({
  sourceBuffer,
  targetBuffer,
  similarityThreshold
}) {
  const command = new CompareFacesCommand({
    SourceImage: {
      Bytes: sourceBuffer
    },
    TargetImage: {
      Bytes: targetBuffer
    },
    SimilarityThreshold: similarityThreshold
  });

  const response = await rekognition.send(command);

  return response;
}

Amazon Rekognition’s CompareFaces docs explain that the operation compares a face in the source image against faces in the target image and returns face matches with similarity scores.

Step 6: Build the comparison endpoint

Update server.js:

import express from "express";
import dotenv from "dotenv";
import { upload } from "./upload.js";
import { validateFaceComparisonFiles } from "./imageValidation.js";
import { compareFacesWithRekognition } from "./rekognitionClient.js";

dotenv.config();

const app = express();

app.get("/health", (req, res) => {
  res.json({
    status: "ok"
  });
});

app.post(
  "/compare-faces",
  upload.fields([
    { name: "source", maxCount: 1 },
    { name: "target", maxCount: 1 }
  ]),
  async (req, res) => {
    try {
      const sourceFile = req.files?.source?.[0];
      const targetFile = req.files?.target?.[0];

      const validationErrors = await validateFaceComparisonFiles(
        sourceFile,
        targetFile
      );

      if (validationErrors.length > 0) {
        return res.status(400).json({
          error: "Invalid images.",
          details: validationErrors
        });
      }

      const similarityThreshold = Number(
        process.env.FACE_MATCH_THRESHOLD || 90
      );

      const rawResult = await compareFacesWithRekognition({
        sourceBuffer: sourceFile.buffer,
        targetBuffer: targetFile.buffer,
        similarityThreshold
      });

      return res.json({
        provider: "amazon_rekognition",
        raw: rawResult
      });
    } catch (error) {
      return res.status(502).json({
        error: "Face comparison failed.",
        details: error.message
      });
    }
  }
);

const port = process.env.PORT || 3000;

app.listen(port, () => {
  console.log(`Face comparison API running on http://localhost:${port}`);
});

Test:

curl -X POST http://localhost:3000/compare-faces \
  -F "[email protected]" \
  -F "[email protected]"

Now you get the raw Rekognition response.

Next, we make it useful.

Step 7: Normalize the API response

Your frontend should not need to understand provider-specific response objects.

Create normalizeFaceResult.js:

export function normalizeFaceComparisonResult(rawResult, threshold) {
  const matches = rawResult.FaceMatches || [];
  const unmatchedFaces = rawResult.UnmatchedFaces || [];

  const bestMatch = matches.length > 0
    ? matches.sort((a, b) => b.Similarity - a.Similarity)[0]
    : null;

  const similarity = bestMatch?.Similarity || 0;

  const decision = getDecision(similarity, threshold, {
    matchCount: matches.length,
    unmatchedCount: unmatchedFaces.length
  });

  return {
    status: "success",
    provider: "amazon_rekognition",
    decision,
    match: decision === "match",
    review_required: decision === "review_required",
    similarity: Number(similarity.toFixed(2)),
    threshold,
    confidence_level: getConfidenceLevel(similarity, threshold),
    face_counts: {
      matches: matches.length,
      unmatched_faces: unmatchedFaces.length
    },
    warnings: getWarnings(similarity, threshold, matches, unmatchedFaces)
  };
}

function getDecision(similarity, threshold) {
  const reviewBand = 5;

  if (similarity >= threshold) {
    return "match";
  }

  if (similarity >= threshold - reviewBand) {
    return "review_required";
  }

  return "no_match";
}

function getConfidenceLevel(similarity, threshold) {
  if (similarity >= threshold + 5) {
    return "high";
  }

  if (similarity >= threshold - 5) {
    return "medium";
  }

  return "low";
}

function getWarnings(similarity, threshold, matches, unmatchedFaces) {
  const warnings = [];

  if (matches.length === 0) {
    warnings.push("No face match above the configured threshold.");
  }

  if (similarity > 0 && similarity < threshold && similarity >= threshold - 5) {
    warnings.push("Similarity is close to threshold. Manual review recommended.");
  }

  if (unmatchedFaces.length > 0) {
    warnings.push("One or more faces in the target image were not matched.");
  }

  return warnings;
}

Use it in the route:

import { normalizeFaceComparisonResult } from "./normalizeFaceResult.js";

Then:

const normalized = normalizeFaceComparisonResult(
  rawResult,
  similarityThreshold
);

return res.json(normalized);

Example response:

{
  "status": "success",
  "provider": "amazon_rekognition",
  "decision": "review_required",
  "match": false,
  "review_required": true,
  "similarity": 87.44,
  "threshold": 90,
  "confidence_level": "medium",
  "face_counts": {
    "matches": 0,
    "unmatched_faces": 1
  },
  "warnings": [
    "No face match above the configured threshold.",
    "Similarity is close to threshold. Manual review recommended."
  ]
}

This is much better than handing raw API output to the frontend.

Step 8: Choose a threshold carefully

Thresholds matter a lot.

A lower threshold catches more true matches but can increase false matches.

A higher threshold reduces false matches but can reject more real users.

Example:

ThresholdBehavior
80More permissive
90Balanced starting point
95+Stricter verification

For low-risk features, a lower threshold may be acceptable.

For account recovery, payments, identity verification, or access control, use stricter thresholds and manual review.

A practical threshold setup:

95+ → match
85-95 → manual review
below 85 → no match

But do not copy this blindly.

Test with your real data, real devices, real lighting, and real user demographics.

NIST’s face recognition evaluations show that algorithm performance can vary significantly across algorithms and conditions. The FRTE 1:1 verification page is useful background if your product depends heavily on biometric verification.

Step 9: Add image quality review states

Sometimes the right decision is not match or no match.

Sometimes the right decision is:

Please upload a better photo.

Add quality-related status labels:

  1. image_too_small
  2. face_not_detected
  3. multiple_faces_detected
  4. low_quality_review_required
  5. manual_review_required
  6. match
  7. no_match

A user-friendly response:

{
  "decision": "image_quality_failed",
  "message": "We could not compare these images reliably. Please upload a clearer photo where the face is visible and facing the camera."
}

That is better than saying “no match” when the selfie is just terrible.

Step 10: Detect face count before comparison

If your use case expects one person per image, reject images with no faces or multiple faces.

Amazon Rekognition can detect faces separately with DetectFaces.

Create detectFaces.js:

import {
  DetectFacesCommand
} from "@aws-sdk/client-rekognition";
import { rekognition } from "./rekognitionClient.js";

export async function detectFacesInImage(imageBuffer) {
  const command = new DetectFacesCommand({
    Image: {
      Bytes: imageBuffer
    },
    Attributes: ["DEFAULT"]
  });

  const response = await rekognition.send(command);

  return response.FaceDetails || [];
}

Use it before comparison:

const sourceFaces = await detectFacesInImage(sourceFile.buffer);
const targetFaces = await detectFacesInImage(targetFile.buffer);

if (sourceFaces.length !== 1 || targetFaces.length !== 1) {
  return res.status(400).json({
    decision: "image_quality_failed",
    message: "Each image must contain exactly one clearly visible face.",
    face_counts: {
      source: sourceFaces.length,
      target: targetFaces.length
    }
  });
}

This avoids weird results when someone uploads a group photo.

Best practices for face comparison UX

The frontend should not make this feel mysterious or creepy.

Good UX should explain:

  1. What images are being compared.
  2. Why the comparison is needed.
  3. What happens if the match fails.
  4. Whether a human can review the case.
  5. How long images are stored.
  6. How users can delete or appeal.

Good copy:

We need a clear selfie to compare with your profile photo. If the automatic check fails, you may be asked to try again or request manual review.

Avoid scary or absolute wording like:

Identity failed.

Better:

We could not verify the match automatically.

That leaves room for image quality problems, API uncertainty, and human review.

Best practices for privacy and storage

Face images are sensitive.

Use strict storage rules.

PracticeWhy it matters
Store images only when requiredReduces privacy risk
Delete temporary uploads quicklyLimits exposure
Encrypt stored imagesProtects sensitive data
Redact logsAvoid leaking image metadata or URLs
Restrict admin accessPrevents internal misuse
Audit accessTracks who viewed results
Separate raw images from user profile dataReduces blast radius
Use signed URLsAvoid public image links
Define retention periodsPrevents indefinite storage
Support deletion requestsGives users control

For many apps, the safest path is:

process image → store result → delete image

Only store face images if your product truly needs them.

Best practices for security

Face comparison endpoints need basic API security.

Add:

  1. Authentication.
  2. Rate limits.
  3. File size limits.
  4. MIME validation.
  5. Malware scanning for uploads.
  6. Abuse monitoring.
  7. CSRF protection if needed.
  8. Audit logs.
  9. Request IDs.
  10. Role-based access for admin review.

Also avoid returning too much provider detail to users.

For example, this may help attackers tune attempts:

{
  "similarity": 89.97,
  "threshold": 90
}

For high-risk identity flows, consider returning a softer user-facing message:

{
  "status": "review_required",
  "message": "We could not verify this automatically. Please try again or continue to manual review."
}

Keep detailed scores in internal logs or reviewer dashboards, not always in public UI.

Best practices for fraud prevention

Face comparison alone is not enough for serious identity verification.

Someone can try:

  1. Printed photo attacks.
  2. Screen replay attacks.
  3. Deepfake images.
  4. Face swaps.
  5. Stolen profile photos.
  6. Similar-looking person attacks.
  7. Edited images.
  8. Cropped ID photos.
  9. Old images.
  10. Synthetic faces.

For higher-risk workflows, add:

LayerWhy it helps
Liveness detectionChecks if a real person is present
ID document verificationCompares selfie to verified ID
Device fingerprintingAdds fraud context
IP/location checksSpots suspicious patterns
Manual reviewHandles edge cases
Risk scoringCombines multiple signals
Rate limitsSlows abuse
Audit trailsSupports investigation

A safer high-risk workflow:

selfie
→ liveness check
→ ID document check
→ face comparison
→ risk score
→ manual review if uncertain

Face comparison is one signal. It should not be the entire fraud system.

Step 11: Add a review band

A review band helps avoid bad automatic decisions near the threshold.

Example:

95+ = match
85-95 = review
below 85 = no match

Implement:

export function decideFaceMatch(similarity) {
  if (similarity >= 95) {
    return {
      decision: "match",
      review_required: false
    };
  }

  if (similarity >= 85) {
    return {
      decision: "review_required",
      review_required: true
    };
  }

  return {
    decision: "no_match",
    review_required: false
  };
}

Review bands are useful because face comparison scores are probabilistic.

Near-threshold results deserve more caution.

Step 12: Log requests safely

You need logs for debugging and audits.

But do not log raw face images.

Log metadata:

{
  "request_id": "fc_123",
  "user_id": "user_456",
  "provider": "amazon_rekognition",
  "decision": "review_required",
  "similarity_bucket": "85_95",
  "source_face_count": 1,
  "target_face_count": 1,
  "latency_ms": 812,
  "created_at": "2026-08-18T22:15:00Z"
}

Good logs include:

  1. Request ID.
  2. User/workspace ID.
  3. Provider.
  4. Decision.
  5. Similarity bucket, not always exact score.
  6. Face count.
  7. Error type.
  8. Latency.
  9. Review outcome.
  10. Model/provider version where available.

Avoid logging:

  1. Raw images.
  2. Base64 image data.
  3. Public image URLs.
  4. Full biometric templates.
  5. Sensitive identity document details.

Step 13: Add manual review workflow

For real apps, add review.

A reviewer should see:

  1. Source image.
  2. Target image.
  3. Match score.
  4. Image quality warnings.
  5. API decision.
  6. User/account context.
  7. Previous attempts.
  8. Fraud signals.
  9. Final reviewer decision.
  10. Reviewer notes.

A normalized review record:

{
  "review_id": "review_123",
  "user_id": "user_456",
  "decision": "review_required",
  "similarity": 88.2,
  "reason": "Similarity close to threshold.",
  "reviewer_status": "pending"
}

Manual review is especially important for:

  1. Account recovery.
  2. Financial workflows.
  3. Hiring/access systems.
  4. Identity verification.
  5. Borderline scores.
  6. Poor image quality.
  7. Repeated failed attempts.
  8. High-value accounts.

Step 14: Add Azure Face comparison option

If your product uses Microsoft Azure, Azure AI Face can verify two faces.

The flow is usually:

detect face in image 1 → get faceId1
detect face in image 2 → get faceId2
call verify face-to-face → get isIdentical + confidence

The Azure Face Verify Face-to-Face docs show a REST operation where two faceId values are compared and a verification result is returned.

A simplified JavaScript request pattern:

import axios from "axios";

export async function verifyFacesWithAzure({ endpoint, apiKey, faceId1, faceId2 }) {
  const response = await axios.post(
    `${endpoint}/face/v1.2/verify`,
    {
      faceId1,
      faceId2
    },
    {
      headers: {
        "Ocp-Apim-Subscription-Key": apiKey,
        "Content-Type": "application/json"
      }
    }
  );

  return response.data;
}

Azure may require setup steps and responsible AI access requirements depending on region and use case, so check current Azure Face service docs before implementation.

Step 15: Full simplified endpoint example

Here is a compact version of the full Amazon Rekognition setup.

import express from "express";
import dotenv from "dotenv";
import multer from "multer";
import sharp from "sharp";
import {
  RekognitionClient,
  CompareFacesCommand,
  DetectFacesCommand
} from "@aws-sdk/client-rekognition";

dotenv.config();

const app = express();

const upload = multer({
  storage: multer.memoryStorage(),
  limits: {
    fileSize: Number(process.env.MAX_UPLOAD_MB || 8) * 1024 * 1024
  }
});

const rekognition = new RekognitionClient({
  region: process.env.AWS_REGION || "us-east-1"
});

const allowedMimeTypes = new Set([
  "image/jpeg",
  "image/png"
]);

async function validateImage(file, label) {
  const errors = [];

  if (!file) {
    errors.push(`${label} image is required.`);
    return errors;
  }

  if (!allowedMimeTypes.has(file.mimetype)) {
    errors.push(`${label} image must be JPEG or PNG.`);
  }

  try {
    const metadata = await sharp(file.buffer).metadata();

    if (!metadata.width || !metadata.height) {
      errors.push(`${label} image dimensions could not be detected.`);
    }

    if (metadata.width < 250 || metadata.height < 250) {
      errors.push(`${label} image is too small.`);
    }
  } catch {
    errors.push(`${label} file is not a valid image.`);
  }

  return errors;
}

async function detectFaces(buffer) {
  const command = new DetectFacesCommand({
    Image: {
      Bytes: buffer
    },
    Attributes: ["DEFAULT"]
  });

  const response = await rekognition.send(command);

  return response.FaceDetails || [];
}

async function compareFaces(sourceBuffer, targetBuffer, threshold) {
  const command = new CompareFacesCommand({
    SourceImage: {
      Bytes: sourceBuffer
    },
    TargetImage: {
      Bytes: targetBuffer
    },
    SimilarityThreshold: threshold
  });

  return rekognition.send(command);
}

function normalizeResult(rawResult, threshold) {
  const matches = rawResult.FaceMatches || [];
  const bestMatch = matches.length
    ? matches.sort((a, b) => b.Similarity - a.Similarity)[0]
    : null;

  const similarity = bestMatch?.Similarity || 0;

  let decision = "no_match";

  if (similarity >= threshold) {
    decision = "match";
  } else if (similarity >= threshold - 5) {
    decision = "review_required";
  }

  return {
    status: "success",
    provider: "amazon_rekognition",
    decision,
    match: decision === "match",
    review_required: decision === "review_required",
    similarity: Number(similarity.toFixed(2)),
    threshold,
    warnings: decision === "review_required"
      ? ["Similarity is close to threshold. Manual review recommended."]
      : []
  };
}

app.post(
  "/compare-faces",
  upload.fields([
    { name: "source", maxCount: 1 },
    { name: "target", maxCount: 1 }
  ]),
  async (req, res) => {
    try {
      const sourceFile = req.files?.source?.[0];
      const targetFile = req.files?.target?.[0];

      const validationErrors = [
        ...(await validateImage(sourceFile, "Source")),
        ...(await validateImage(targetFile, "Target"))
      ];

      if (validationErrors.length > 0) {
        return res.status(400).json({
          status: "error",
          decision: "image_quality_failed",
          errors: validationErrors
        });
      }

      const sourceFaces = await detectFaces(sourceFile.buffer);
      const targetFaces = await detectFaces(targetFile.buffer);

      if (sourceFaces.length !== 1 || targetFaces.length !== 1) {
        return res.status(400).json({
          status: "error",
          decision: "image_quality_failed",
          message: "Each image must contain exactly one clearly visible face.",
          face_counts: {
            source: sourceFaces.length,
            target: targetFaces.length
          }
        });
      }

      const threshold = Number(process.env.FACE_MATCH_THRESHOLD || 90);

      const rawResult = await compareFaces(
        sourceFile.buffer,
        targetFile.buffer,
        threshold
      );

      return res.json(normalizeResult(rawResult, threshold));
    } catch (error) {
      return res.status(502).json({
        status: "error",
        decision: "provider_error",
        message: "Face comparison failed.",
        details: error.message
      });
    }
  }
);

app.listen(process.env.PORT || 3000, () => {
  console.log(`Face comparison API running on port ${process.env.PORT || 3000}`);
});

This is enough for a demo backend.

For production, add authentication, storage rules, audit logs, rate limits, review workflow, and legal/privacy review.

Common errors and what they mean

ErrorLikely causeFix
No face detectedFace too small, blurry, hidden, or side-facingAsk user for clearer image
Multiple faces detectedGroup photo uploadedRequire one face per image
Low similarityDifferent person or poor image qualityRetry with better photo or review
Provider errorAPI credentials, region, timeout, or file issueCheck logs and provider response
Unsupported image typeUser uploaded HEIC, GIF, PDF, etc.Convert or restrict uploads
High false rejectionThreshold too strict or poor image qualityTune threshold and upload guidance
Too many suspicious attemptsPotential fraud or bad UXAdd rate limits and review

The best error messages should help users fix the problem without exposing sensitive matching logic.

Where LLMAPI fits

LLMAPI can fit after face comparison when your app needs review notes, user messages, routing, or workflow automation.

Face comparison returns a structured result:

{
  "decision": "review_required",
  "similarity": 88.2,
  "warnings": [
    "Similarity is close to threshold."
  ]
}

LLMAPI can turn that into a safer internal note:

Manual review is recommended because the similarity score is close to the configured threshold. Ask the reviewer to confirm image quality and compare the submitted selfie with the reference image before making a final decision.

Useful LLMAPI tasks:

TaskExample
Review noteSummarize why manual review is needed
User-facing messageAsk for a clearer selfie without sounding accusatory
Fraud triage summarySummarize repeated failed attempts
Workflow routingSend uncertain cases to identity review
Support responseDraft a careful account verification message
Audit summaryExplain decision factors in plain language
Batch reportsSummarize weekly verification failures

A practical workflow:

face images
→ face comparison API
→ normalized result
→ threshold/review logic
→ LLMAPI review note or user message
→ human review if needed

The face API compares. Your backend decides. LLMAPI explains and routes.

Best practices checklist

Use this before shipping.

  • Get clear user consent before processing face images.
  • Explain why face comparison is needed.
  • Keep API keys on the backend.
  • Validate file type, size, and dimensions.
  • Require exactly one face per image for verification flows.
  • Add upload guidance for better image quality.
  • Use thresholds based on real testing, not guesses.
  • Add a manual review band near the threshold.
  • Avoid absolute user-facing language.
  • Store images only as long as needed.
  • Encrypt sensitive data.
  • Redact logs.
  • Add audit trails for reviewer decisions.
  • Rate-limit verification attempts.
  • Add liveness detection for high-risk workflows.
  • Check legal requirements for biometric data.
  • Test performance across real user conditions.
  • Track false matches and false non-matches.
  • Let users retry or appeal failed checks.

That checklist matters more than another tiny code snippet.

The practical takeaway

You can do face comparison using JavaScript by building a backend that accepts two images, validates them, sends them to a face comparison API like Amazon Rekognition or Azure AI Face, normalizes the result, and applies your own decision logic.

The basic flow looks like this:

two face images
→ JavaScript backend
→ face comparison API
→ similarity score
→ match / no match / review

But the real work is around the API call.

Use clear consent. Validate image quality. Require one face per image. Pick thresholds carefully. Add a review band. Avoid overconfident user-facing messages. Store biometric data carefully. Add liveness checks for high-risk workflows. Track errors and review outcomes. Use LLMAPI when you need careful review notes, support messages, or workflow routing after the comparison result.

That is how face comparison becomes a responsible product feature, not just a similarity score in JSON.

Deploy in minutes