AI Integration Skill
Biometry Integration Architect — API v2
This document is an instruction set for AI coding agents (Claude, GPT, Copilot, etc.) building biometric verification against Biometry API v2. Feed it to your agent as context before it writes integration code.
Machine-readable contract, if your agent can fetch a URL:
https://api.biometrysolutions.com/api-gateway/v2/openapi.json.
Working on an existing v1 integration? See the v1 skill and the migration guide.
System architecture
Biometry follows an API proxy pattern. The customer’s backend proxies all Biometry calls; the frontend never calls Biometry directly, because the API token is a secret that must not reach a browser or mobile binary.
┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐│ Customer Frontend │ │ Customer Backend │ │ Biometry API ││ │ │ │ │ ││ Web SDK / Flutter │────▶│ /api/verify │────▶│ /api-gateway/v2/ ││ (captures media) │◀────│ (holds API token) │◀────│ │└─────────────────────┘ └──────────────────────┘ └─────────────────────┘Base URL: https://api.biometrysolutions.com/api-gateway/v2
Other prefixes you will need (unversioned, shared with v1):
/api-consent— consent records/api-transactions— transaction history/api-projects— API key management/api-auth— Console user auth; not part of a backend integration
Authentication
One header, on every call:
Authorization: Bearer <API_TOKEN>The token is a JWT minted in the Console. It encodes project_id, the enabled
services, and optionally a scoring system ID. It does not expire — it is
valid until revoked, so treat it as a long-lived secret: environment variable or
secret manager, one token per application, never in version control.
A second credential exists — the Console user session — and it is not
interchangeable. These endpoints require it and will reject an API token with
401: scoring systems (/v2/scoring-systems/*), webhooks
(/v2/webhooks/*), consent document authoring, GET /v2/sessions,
DELETE /v2/users/{user_id}. If your task says “create a webhook”, the answer
is the Console, not the API token.
The two rules that differ most from v1
1. Identity is user_id, in the body. No X-User-Fullname. user_id is an
opaque, caller-chosen string, 1–128 chars matching ^[A-Za-z0-9._:@-]+$, unique
inside the project. Use the customer’s own primary key, a UUID, or a hash —
never a person’s name, and never anything with spaces (it will be rejected with
400).
2. Parameters live in the body. File-carrying endpoints are
multipart/form-data with a single JSON part named request alongside the file
parts. The v1 business headers (X-User-Fullname, X-Session-ID,
X-Action-Trigger, X-Use-Prefilled-Video, X-Inhouse-Docauth,
X-Inhouse-MRZ) are stripped on v2 routes — sending them does nothing.
Headers that remain headers: Authorization, X-Request-Id, X-Client-App,
X-Client-Version, X-App-Version, X-Device-Info, X-Geo-Location.
Response envelope
Every response, success or failure:
{ "data": { }, "decision": { "status": "pass", "score": 87.5, "reasons": [] }, "meta": { "request_id": "b6f0…", "message": "video processed successfully" }}{ "error": { "code": "invalid_request", "message": "user_id is required" }, "meta": { "request_id": "b6f0…" }}data— endpoint payload. Omitted when there is nothing to return.decision— present only when a scoring system was evaluated.status∈pass|refer|fail.meta.request_id— always present. Log it on every call; it is what Biometry support traces.
Service results are keyed by public service name: face_liveness_detection,
active_speaker_detection, visual_speech_recognition, face_recognition,
voice_recognition. Each value is { code, description, result, score } with
score on a 0–100 scale. Only services enabled on the token appear.
Failure modes an agent must handle
These are the three things naive generated code gets wrong.
1. 200 does not mean “passed”. Services run in parallel and report
independently. A failed one appears as {"error": "…"} under its own key while
the HTTP status stays 200:
{ "data": { "face_recognition": { "error": "there is no enrolled face for this person" } } }Always inspect each service entry and the decision, never the status alone.
2. Rejected enrollment is also 200. POST /v2/enrollments/face returns
meta.message: "failed to enroll face" with a non-zero
data.enrollment_result.code (1 no face / too small / bad quality, 2 more
than one face, 3 face not centered). Voice returns data.status of error or
qafailed. Branch on those, not on the status code.
3. Missing consent silently drops services. Face Recognition and Voice
Recognition require authorization consent. Without it they are removed from the
call and the response carries the header X-Removed-Services: true — you get a
200 with the identity check simply absent. Check consent up front or check
that header.
Error codes to branch on: invalid_request (400), invalid_api_key /
invalid_token (401), service_not_enabled (403), forbidden (403, includes
missing consent), not_found (404), conflict (409),
unprocessable_entity (422), internal_error (500, retry with backoff).
Consent
Consent still lives on the unversioned consent service, and this is the single
most common integration mistake: pass your user_id as user_fullname.
That field is the identity key the platform matches on.
POST /api-consent/consent → authorization consent (recognition, enrollment)POST /api-consent/strg-consent → storage consent (enrollment only)Authorization: Bearer <API_TOKEN>Content-Type: application/json
{ "is_consent_given": true, "user_fullname": "<same value as user_id>" }POST grants (rejects false with 400); PUT on the same path changes an
existing record — that is how a withdrawal is recorded. GET /api-consent/history?user_fullname=…
returns the audit trail.
Integration flow: verify a returning user
1. Pre-flight — is this user enrolled?
GET /api-gateway/v2/users/{user_id}{ "data": { "user_id": "…", "enrollments": { "face": true, "voice": false }, "voice_phrases": [] } }An unknown user is not a 404 — it returns enrollments all false. Use
this to route between onboarding and login, and to skip a check the user cannot
pass. Batch form: GET /v2/users?ids=a,b,c (max 100).
2. Start a session
POST /api-gateway/v2/sessions → { "data": { "session_id": "…" } }POST /api-gateway/v2/sessions?warmup=true (pre-warms ML services)Sessions group the calls of one attempt and store the first uploaded video, so
/v2/face-match can reuse it. Pass session_id in the request part of every
subsequent call.
3. Liveness
POST /api-gateway/v2/livenessContent-Type: multipart/form-data
request = {"user_id":"…","session_id":"…","phrase":"one two three four five six"}video = <file>Returns face_liveness_detection, active_speaker_detection,
visual_speech_recognition. phrase is required — it is what Visual Speech
Recognition compares its transcript against. Optional fields in request:
vocabulary, trigger, services.exclude (array of service keys).
4. Identity
POST /api-gateway/v2/face-verify request = {"user_id","phrase","session_id"}, video = <file>POST /api-gateway/v2/voice-verify request = {"user_id","phrase","session_id"}, voice = <file>Each compares against the user’s enrolled template and returns its own
decision. Run them only for modalities the pre-flight said are enrolled.
5. End the session
POST /api-gateway/v2/sessions/{id}/end{ "phone_number": "+996700000000" } ← optional SIM-swap checkEnding a session does not return a combined verdict. Compose the per-call
decision objects yourself. Do not generate code that waits for a session-level
score.
Integration flow: document onboarding
POST /api-gateway/v2/sessions → session_idPOST /api-gateway/v2/documents/check request = {"session_id":"…","provider":"inhouse"} (provider/mrz_provider optional) document = <file> → extracted fields + portrait_photoPOST /api-gateway/v2/liveness request+video → anti-spoof resultsPOST /api-gateway/v2/face-match request = {"user_id":"…","session_id":"…","use_session_video":true} reference_image = <document portrait> → match against the stored videoPOST /api-gateway/v2/enrollments/face (optional, for future logins) request = {"user_id":"…","use_session_video":true} → enrolls from the session's videoPOST /api-gateway/v2/sessions/{id}/enduse_session_video is honoured by /v2/face-match, /v2/face-verify,
/v2/voice-verify and /v2/enrollments/face — every one of them reuses the
video the liveness step stored on the session, so the capture is uploaded once.
/v2/enrollments/voice does not take it: a voiceprint needs several distinct
recordings. Enroll before /v2/sessions/{id}/end; once the session is closed
its video can no longer be enrolled from.
Asynchronous: deepfake detection
POST /api-gateway/v2/deepfake/checks request = {"user_id","session_id"}, video = <file> → 201GET /api-gateway/v2/deepfake/checks/{request_id}The ID to poll with is data.metadata.request_id from the 201 response — not
meta.request_id, which identifies the HTTP call. Poll until data.status is
completed or failed; the verdict is data.result.prediction (real /
fake) with confidence and score. Or register a webhook and skip polling.
Checklist before shipping generated code
- API token read from the environment on the backend; never sent to a client
-
user_idis opaque and stable, matches^[A-Za-z0-9._:@-]{1,128}$, and is the same value used asuser_fullnamefor consent - Consent collected before enrollment (authorization and storage) and before recognition (authorization)
- Parameters in the
requestJSON part, not inX-*headers - Every response checked per service, not by HTTP status
-
meta.request_idlogged on every call -
session_idpassed to every call in one attempt, and the session ended on both success and failure paths - Retries only on
5xx, with backoff; never retry a successful enrollment or deepfake submission - Media captured client-side via the Web or Flutter SDK and forwarded through your backend
Reference
| Topic | Page |
|---|---|
| Envelope, identity model, service keys | API v2 Overview |
| End-to-end curl walkthrough | v2 Quick Start |
| Every error code | Error catalog |
| Verification endpoints in full | Verification |
| How the verdict is computed | Scoring Systems |
| Reading results back | Transactions |