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
It helps to be precise about the layers, because no single API does all of them and overclaiming here is how teams ship insecure onboarding:
- 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. A separate concern.
- Document authenticity - confirm the ID itself is genuine and unaltered. Also separate, and the hardest to automate.
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"]
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
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"])
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"
Auto-approve when everything lines up, auto-reject on a clear failure, and send the ambiguous middle to a human queue rather than guessing.
Honest limits
These two layers catch the common failure modes, but they are not a complete eKYC stack on their own. There is no liveness here, so a face match alone cannot tell a live selfie from a held-up photo of the ID owner; add a liveness step for anything high-risk. And OCR reads what is printed, it does not verify the ID is genuine; forged documents need a separate authenticity layer. Treat this as the face-match and data-extraction core of your eKYC, not the whole thing.
Read the full guide with the Flask endpoint, the response shapes, and the decision logic on ai-engine.net.
Top comments (0)