DEV Community

Cover image for Kairos and Betaface API Alternative: Drop-in Python Code
AI Engine
AI Engine

Posted on Originally published at ai-engine.net

Kairos and Betaface API Alternative: Drop-in Python Code

Kairos and Betaface were two of the go-to face recognition APIs of the 2010s: enroll a face under a name, then recognize it in any photo. In 2026, Kairos sells identity verification and the public Betaface API is offline, which leaves attendance apps, visitor kiosks and photo tools built on them looking for a replacement.

Below: a small Python module that keeps the Kairos function names (enroll, recognize, verify, galleries) and the Kairos response shape on top of a hosted face recognition API. Every output comes from a live run on real photos of an office team.

Want to run it on your own photos? Get a key for the Face Analyzer API.

Where Kairos and Betaface stand in 2026

Checked on 28 October 2026:

  • Kairos: kairos.com now presents identity verification (documents, selfies, liveness). The classic face recognition docs still live on face.kairos.com, but sign-up asks for a business email and how you plan to use Kairos, and pricing is a contact-sales form
  • Betaface: the betaface.com products page reads "Public betafaceapi.com is temporarily down", the latest news post (17 March 2026) says the website is being restored, and the betafaceapi.com domain no longer resolves. On-premise licences are still offered

Kairos endpoints mapped

Kairos Face Analyzer API
/enroll /save-face-in-repository (subject_id becomes external_id)
/recognize /faceanalysis, then /search-face-in-repository per face
/verify /search-face-in-repository filtered on the subject
/detect /faceanalysis (box, landmarks, age range, gender, emotions)
/gallery/list_all /list-facial-repositories
/gallery/view /list-repository-faces
/gallery/remove_subject /delete-face-from-repository
/gallery/remove /delete-facial-repository
confidence 0 to 1, default 0.60 Similarity 0 to 100, we use 90

For Betaface, a namespace becomes a face repository, a person such as john@mynamespace becomes the external_id, face tags (age, gender, glasses, smile) come from /faceanalysis, verification is /compare-faces and identification is /search-face-in-repository. The module below works as the base for both migrations.

Step 1: a Kairos-compatible module

Kairos /recognize matches every face in a photo, while the repository search looks at the main face of the image it receives, so recognize detects the faces first and searches a crop of each one. Save as kairos_compat.py:

import io
import requests
from PIL import Image, ImageOps

BASE = "https://faceanalyzer-ai.p.rapidapi.com"
HEADERS = {
    "x-rapidapi-host": "faceanalyzer-ai.p.rapidapi.com",
    "x-rapidapi-key": "YOUR_API_KEY",
}
MIN_SIMILARITY = 90  # 0 to 100 scale


def _post(endpoint, files=None, data=None):
    r = requests.post(f"{BASE}{endpoint}", headers=HEADERS, files=files, data=data)
    return r.json()["body"]


def _get(endpoint, params=None):
    r = requests.get(f"{BASE}{endpoint}", headers=HEADERS, params=params)
    return r.json()["body"]


def _search(face_jpg, gallery_name):
    """Return the matches of one face in a gallery, best first."""
    body = _post(
        "/search-face-in-repository",
        files={"image": ("face.jpg", face_jpg, "image/jpeg")},
        data={"repository_id": gallery_name},
    )
    matches = [m for m in body.get("FaceMatches", []) if m["Similarity"] >= MIN_SIMILARITY]
    return sorted(matches, key=lambda m: m["Similarity"], reverse=True)


def enroll(image_path, subject_id, gallery_name):
    """Kairos /enroll: store the face under subject_id, creating the gallery if needed."""
    if gallery_name not in _get("/list-facial-repositories")["FacialRepositoriesId"]:
        _post("/create-facial-repository", data={"repository_id": gallery_name})
    with open(image_path, "rb") as f:
        body = _post(
            "/save-face-in-repository",
            files={"image": f},
            data={"repository_id": gallery_name, "external_id": subject_id, "max_faces": "1"},
        )
    return {"face_id": body["FacesId"][0], "subject_id": subject_id}


def recognize(image_path, gallery_name):
    """Kairos /recognize: match every face in the photo against the gallery."""
    with open(image_path, "rb") as f:
        faces = _post("/faceanalysis", files={"image": f})["faces"]
    im = ImageOps.exif_transpose(Image.open(image_path)).convert("RGB")
    w, h = im.size
    images = []
    for face in faces:
        b = face["boundingBox"]
        x1, y1 = int(b["topLeft"]["x"] * w), int(b["topLeft"]["y"] * h)
        x2, y2 = int(b["bottomRight"]["x"] * w), int(b["bottomRight"]["y"] * h)
        m = int((x2 - x1) * 0.4)
        buf = io.BytesIO()
        im.crop((max(0, x1 - m), max(0, y1 - m), min(w, x2 + m), min(h, y2 + m))).save(buf, "JPEG")
        matches = _search(buf.getvalue(), gallery_name)
        transaction = {"topLeftX": x1, "topLeftY": y1, "width": x2 - x1, "height": y2 - y1}
        if matches:
            transaction.update(
                status="success",
                subject_id=matches[0]["ExternalImageId"],
                confidence=round(matches[0]["Similarity"] / 100, 5),
            )
        else:
            transaction.update(status="failure", message="no match found")
        candidates = [
            {"subject_id": m["ExternalImageId"], "confidence": round(m["Similarity"] / 100, 5)}
            for m in matches
        ]
        images.append({"transaction": transaction, "candidates": candidates})
    return {"images": images}


def verify(image_path, subject_id, gallery_name):
    """Kairos /verify: is the main face in the photo this subject?"""
    with open(image_path, "rb") as f:
        matches = _search(f.read(), gallery_name)
    scores = [m["Similarity"] for m in matches if m["ExternalImageId"] == subject_id]
    confidence = round(max(scores, default=0) / 100, 5)
    return {"status": "success" if scores else "failure", "subject_id": subject_id, "confidence": confidence}


def gallery_view(gallery_name):
    """Kairos /gallery/view: the subject_ids enrolled in a gallery."""
    faces = _get("/list-repository-faces", {"repository_id": gallery_name})["Faces"]
    return {"subject_ids": sorted({f["ExternalImageId"] for f in faces})}


def gallery_remove_subject(gallery_name, subject_id):
    """Kairos /gallery/remove_subject: delete every face enrolled for subject_id."""
    faces = _get("/list-repository-faces", {"repository_id": gallery_name})["Faces"]
    face_ids = [f["FaceId"] for f in faces if f["ExternalImageId"] == subject_id]
    _post("/delete-face-from-repository", data={"repository_id": gallery_name, "face_ids": face_ids})
    return {"status": "Complete", "removed": len(face_ids)}


def gallery_remove(gallery_name):
    """Kairos /gallery/remove: delete the gallery and all its faces."""
    return _post("/delete-facial-repository", data={"repository_id": gallery_name})
Enter fullscreen mode Exit fullscreen mode

Step 2: enroll your people

Four staff badges, one photo each, with the same call as Kairos. The gallery is created on first use:

from kairos_compat import enroll, recognize, verify, gallery_view, gallery_remove_subject

GALLERY = "office-staff"
for name in ["marcus", "andre", "nia", "daniel"]:
    print(enroll(f"badges/{name}.jpg", name, GALLERY))
# {'face_id': 'b2ab7035-...', 'subject_id': 'marcus'}
# ... one line per person
Enter fullscreen mode Exit fullscreen mode

Step 3: recognize every face in a group photo

result = recognize("team.jpg", GALLERY)
for image in result["images"]:
    t = image["transaction"]
    print(t["status"], t.get("subject_id", "-"), t.get("confidence", ""))
# success daniel 0.9998
# success nia 0.99934
# failure -
# success andre 0.99999
# success marcus 0.99999
Enter fullscreen mode Exit fullscreen mode

Five faces, five correct answers in under 3 seconds: the four enrolled people are named above 0.999, and the fifth man, never enrolled, comes back as failure with no candidate and the same "no match found" message Kairos uses.

Step 4: verify one person

Tested on a photo from another day of the shoot, in a suit and tie instead of the white t-shirt of his badge:

print(verify("daniel_suit.jpg", "daniel", GALLERY))
print(verify("daniel_suit.jpg", "marcus", GALLERY))
# {'status': 'success', 'subject_id': 'daniel', 'confidence': 0.99975}
# {'status': 'failure', 'subject_id': 'marcus', 'confidence': 0.0}
Enter fullscreen mode Exit fullscreen mode

Face verification with a Kairos-compatible Python module: an enrolled badge photo, the same man in a suit on another day, a success at 0.99975 for daniel and a failure for marcus

Step 5: manage the gallery

print(gallery_view(GALLERY))
print(gallery_remove_subject(GALLERY, "andre"))
print(gallery_view(GALLERY))
# {'subject_ids': ['andre', 'daniel', 'marcus', 'nia']}
# {'status': 'Complete', 'removed': 1}
# {'subject_ids': ['daniel', 'marcus', 'nia']}
Enter fullscreen mode Exit fullscreen mode

Migration tips

  • Re-enroll from the original photos: face templates are specific to each engine, so run your source images through enroll once with the same subject_ids
  • Set the threshold on the new scale: 90 on the 0 to 100 Similarity scale is a strict start for access control
  • Enroll more than one photo per person: a profile shot or a photo with glasses helps in real conditions
  • Switch behind a flag: the module returns Kairos-shaped JSON, so you can compare answers on a share of traffic first
  • One gallery per customer: never mix the faces of two customers in a SaaS

Full guide with the Betaface mapping and sources: Kairos and Betaface API alternative on ai-engine.net.

Sources: Kairos API reference, Betaface products page, test photos by Kindel Media on Pexels.

👉 Grab a key from the Face Analyzer API page and point your app at kairos_compat.py.

Top comments (0)