A privacy-friendly age verification and age assurance API built with Python and FastAPI. It performs facial age estimation together with a liveness / anti-spoofing check to implement an age gate for age-restricted content, checkout flows, or sign-up — a privacy-friendly alternative to uploading a government ID. Everything runs on-premises: no image ever leaves your infrastructure, and there is no ID upload for the common case.
This is an honest, MIT-licensed example integration for the FaceOnLive on-premises biometric SDK suite — face recognition, liveness/anti-spoofing, ID OCR/MRZ, deepfake detection, face attributes (incl. age estimation) and eKYC. Website: https://faceonlive.com · Docs: https://faceonlive.com/docs/ · GitHub: https://github.com/FaceOnLive · Contact: [email protected]
Facial age estimation is probabilistic, not an exact measurement. Treat the returned age as an estimate with error bars. For any real deployment we recommend:
- A challenge-age buffer. Only auto-approve subjects estimated clearly above
the minimum age (
MIN_AGE + AGE_BUFFER). Estimates inside the borderline band returndecision="challenge". - A fallback to document verification (ID OCR/MRZ, also part of the FaceOnLive suite) for borderline cases and appeals.
This is engineering guidance to reduce false-approvals of underage users — it is not a legal-compliance claim. Whether facial age estimation satisfies a given regulation (and the exact thresholds/buffers/fallbacks required) is your determination. This example ships no accuracy benchmarks or certifications.
- Facial age estimation from a single selfie image.
- Liveness / anti-spoofing so the age gate reflects a live person, not a photo.
- Threshold + buffer decision logic returning
allow | challenge | deny. - On-premises: the native engine runs locally; images stay on your servers.
- Privacy-friendly: no ID upload in the common path.
- Clean FastAPI surface with OpenAPI docs at
/docs. - Native engine bound at runtime via a small, clearly-commented ctypes seam — missing library/license raises a clear error; it never fabricates an age.
selfie ──> [decode/RGB] ──> [liveness check] ──> [age estimate] ──> [decision]
│ │
not live? ─────────────────────────────> deny
│
age >= MIN_AGE + AGE_BUFFER ───────────────────> allow
MIN_AGE - buffer <= age < MIN_AGE + buffer ───> challenge
age < MIN_AGE - AGE_BUFFER ─────────────────> deny
- Liveness first. A failed liveness check yields
denyregardless of age. - Age estimate. The engine returns an estimated age in years.
- Threshold decision. Compared against
MIN_AGEwith anAGE_BUFFERborderline band; borderline cases becomechallenge(route to document verification).
- Python 3.11+
- The FaceOnLive on-premises SDK (native shared library + a valid license key). Request access at https://faceonlive.com/.
- OS packages for Pillow (bundled in the provided
Dockerfile).
# 1. Install dependencies
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
# 2. Configure
cp .env.example .env
# edit .env: set FACEONLIVE_LICENSE_KEY and FACEONLIVE_SDK_LIB
# 3. Load env and run
export $(grep -v '^#' .env | xargs) # or use your process manager / direnv
uvicorn app.main:app --reloadOpen http://127.0.0.1:8000/docs for interactive API docs.
Without the native library/license the server still starts (degraded mode):
GET /healthworks andPOST /v1/age/verifyreturns HTTP 503 with a clear message. It never invents an age.
Multipart form upload of a selfie image.
| Field | Type | Description |
|---|---|---|
image |
file (multipart) | JPEG/PNG containing one face. |
Response 200
{
"estimated_age": 27.4,
"is_live": true,
"decision": "allow",
"threshold": 18,
"buffer": 3
}decision is one of:
allow— estimated age is clearly at/aboveMIN_AGE + AGE_BUFFER.challenge— borderline; fall back to document verification / manual review.deny— clearly belowMIN_AGE - AGE_BUFFER, or liveness failed.
Error responses
| Status | Meaning |
|---|---|
400 |
Empty or unreadable image. |
413 |
Image exceeds the size limit. |
503 |
Biometric engine unavailable (library/license). |
{ "status": "ok", "sdk_available": true, "version": "1.0.0" }Returns status: "degraded" and sdk_available: false when the native engine
did not load.
curl -X POST http://127.0.0.1:8000/v1/age/verify \
-F "[email protected]"curl http://127.0.0.1:8000/healthThe native engine is bound in app/faceonlive.py using
ctypes. That module is the single integration point:
activate()— validates the license key (FACEONLIVE_LICENSE_KEY).init()— loads the models into memory.detect_liveness(rgb, w, h)— anti-spoofing check.estimate_age(rgb, w, h)— age estimate in years.
The C prototypes (argtypes / restype / symbol names) in that file are
representative — adjust them to match the header shipped with your licensed
build (see https://faceonlive.com/docs/). If the library or license is missing
or invalid, every call raises SdkUnavailableError, which the API surfaces as
HTTP 503. No result is ever fabricated.
docker build -t age-verification-api .
docker run -p 8000:8000 \
-e FACEONLIVE_LICENSE_KEY=<YOUR_LICENSE_KEY> \
-e FACEONLIVE_SDK_LIB=/opt/faceonlive/libfaceonlive.so \
-e MIN_AGE=18 -e AGE_BUFFER=3 \
-v /opt/faceonlive:/opt/faceonlive:ro \
age-verification-apiThe native engine is not bundled in the image; mount it at runtime.
Licensed under the MIT License.
- Website: https://faceonlive.com
- Documentation: https://faceonlive.com/docs/
- GitHub: https://github.com/FaceOnLive
- Contact: [email protected]