DEV Community

Cover image for Build eKYC for Your SaaS: Match a Selfie to an ID with an API
AI Engine
AI Engine

Posted on Edited on Originally published at ai-engine.net

Build eKYC for Your SaaS: Match a Selfie to an ID with an API

Your SaaS needs to confirm new users are who they claim to be: a fintech opening accounts, a marketplace onboarding sellers, a gig app vetting workers. The standard step is eKYC. The user uploads a government ID and takes a selfie, and your backend checks that the selfie matches the photo on the ID. Building that from scratch means face-recognition models and document parsing. Two APIs handle both halves, and you can wire the onboarding step together in an afternoon.

This walkthrough builds the two layers you can ship quickly: extracting the ID data with an OCR API, and matching the selfie to the ID photo with a face comparison API, then combining them into an approve / review / reject decision. Every snippet runs against live endpoints.

Want to test the match step now? Try the Face Analyzer API on a selfie and an ID photo.

What eKYC actually requires

A complete eKYC flow has four layers, and two of them can be live in an afternoon:

  • Document data extraction - read the name, date of birth, document number, and expiry off the ID. This uses an OCR API.
  • Face match - confirm the selfie is the same person as the ID photo. This uses a face comparison API.
  • Liveness - confirm the selfie is a live person, not a photo of a photo or a screen replay. Covered in the production tips below.
  • Document authenticity - confirm the ID itself is genuine and unaltered. Also covered in the production tips.

The first two are the layers you can build today with a couple of API calls, and they catch the most common case: a user submitting someone else's ID.

Step 1: Extract the ID data with OCR

Send the ID image to the OCR endpoint and get the text back. For a national ID, license, or passport, it returns every printed field:

import requests

OCR_HEADERS = {
    "x-rapidapi-key": "YOUR_API_KEY",
    "x-rapidapi-host": "ocr-wizard.p.rapidapi.com",
}

def read_id_document(id_image_path):
    """OCR an ID document, return the raw extracted text."""
    with open(id_image_path, "rb") as f:
        r = requests.post(
            "https://ocr-wizard.p.rapidapi.com/ocr",
            headers=OCR_HEADERS,
            files={"image": f},
        )
    return r.json()["body"]["fullText"]
Enter fullscreen mode Exit fullscreen mode

Labels and values come back on separate lines, exact order varying by document. To turn that into structured fields, pipe it through an LLM, the same pattern as an invoice or receipt parser.

Reading IDs, invoices, or receipts? Try the OCR Wizard API on your own document.

Step 2: Match the selfie to the ID photo

Now the core of identity verification: does the selfie belong to the same person as the photo on the ID. The compare-faces endpoint takes two images and returns the faces that matched. A useful detail: you can pass the full ID image as the target without cropping the photo first, and the API locates the face on the document for you.

FACE_HEADERS = {
    "x-rapidapi-key": "YOUR_API_KEY",
    "x-rapidapi-host": "faceanalyzer-ai.p.rapidapi.com",
}

def faces_match(selfie_path, id_image_path):
    """Return True if the selfie matches the face on the ID document."""
    with open(selfie_path, "rb") as selfie, open(id_image_path, "rb") as id_img:
        r = requests.post(
            "https://faceanalyzer-ai.p.rapidapi.com/compare-faces",
            headers=FACE_HEADERS,
            files={
                "source_image": ("selfie.jpg", selfie, "image/jpeg"),
                "target_image": ("id.jpg", id_img, "image/jpeg"),
            },
        )
    body = r.json()["body"]
    return len(body.get("matchedFaces", [])) > 0
Enter fullscreen mode Exit fullscreen mode

A non-empty matchedFaces means the selfie and the ID photo are the same person. An empty match with a populated unmatchedFaces means a mismatch.

Curious how it scores your own pair of images? Run it on a selfie and an ID and read the matched faces back.

Step 3: Quality-gate the inputs with face detection

The compare call finds the face on the ID for you, so you do not need detection to make the match. Where detection earns its place is as a quality gate before the comparison: confirm the selfie shows exactly one clear face, and confirm the ID actually has a detectable photo. That turns a useless empty match into a clear reason like "no face found on the document":

def count_faces(image_path):
    """Return how many faces the detector finds in an image."""
    with open(image_path, "rb") as f:
        r = requests.post(
            "https://faceanalyzer-ai.p.rapidapi.com/faceanalysis",
            headers=FACE_HEADERS,
            files={"image": f},
        )
    return len(r.json()["body"]["faces"])
Enter fullscreen mode Exit fullscreen mode

Zero faces on the selfie means a bad capture, two or more means someone else is in frame, and zero on the ID means the photo region was not readable. Each is a reason to route to review rather than guess.

Step 4: Turn it into a decision

No single call is a decision. Combine the detection gate, the document data, the expiry check, and the face match into one verdict your flow can act on:

import re
from datetime import date

def verify_identity(selfie_path, id_image_path):
    if count_faces(selfie_path) != 1:
        return "review", "selfie must show exactly one clear face"
    if count_faces(id_image_path) != 1:
        return "review", "could not find a single face on the ID document"

    text = read_id_document(id_image_path)
    m = re.search(r"EXPIRES\s+(\d{4}-\d{2}-\d{2})", text)
    expiry = date.fromisoformat(m.group(1)) if m else None
    if expiry is not None and expiry < date.today():
        return "reject", "ID is expired"

    if not faces_match(selfie_path, id_image_path):
        return "review", "selfie did not match the ID photo"
    if expiry is None:
        return "review", "could not read the expiry date"
    return "approve", "identity verified"
Enter fullscreen mode Exit fullscreen mode

Auto-approve when everything lines up, auto-reject on a clear failure, and send the ambiguous middle to a human queue rather than guessing.

Production tips

The two layers here handle the most common cases. For a complete eKYC stack, add a liveness step for high-risk flows, so you know the selfie is live, and a document authenticity layer, so you know the ID is genuine. This gives you the face-match and data-extraction core of your eKYC.

Read the full guide with the Flask endpoint, the response shapes, and the decision logic on ai-engine.net.

Top comments (0)