About didit-face-search
didit-face-search documents Didit's 1:N face search API for applications that need to find similar or listed faces in an enrolled index. It describes search modes, match and warning interpretation, request options, and storage behavior.
Key features
- 01Search an enrolled face index
- 02Return up to five ranked matches
- 03Screen blocklists and allowlists
- 04Choose whether requests enroll faces
- 05Report duplicate and multiple-face warnings
Use cases
- 01Detect duplicate accounts during onboarding
- 02Screen a submitted face against a blocklist
- 03Investigate possible fraud-ring links
- 04Review matches and warnings in an integration
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ didit-protocol/ skills --skill didit-face-searchAfter installing, type/didit-face-searchin Claude Code to confirm it loaded.
Bundled filesscripts/search_faces.py
Details
From the project READMEDidit Face Search API (1:N)
Overview
Searches the largest face in an image against your application's face search index and returns up to 5 ranked matches above the similarity floor, plus blocklist and duplicate warnings.
What is searched: faces enrolled from verification sessions of any status, saved standalone calls (e.g. Passive Liveness or Age Estimation with save_api_request=true), faces uploaded directly to User profiles, faces on your lists (blocklist, allowlist), and image-free biometric templates you chose to retain on a User. Faces enrolled by earlier Face Search calls are never returned, so repeated searches do not match each other.
Search modes (search_type):
most_similar(default): ranks every enrolled face by similarity. Use for deduplication and fraud-ring investigation.blocklisted_or_approved: only blocklisted faces, allowlisted faces, faces from approved sessions, imported User faces and retained templates, with blocklisted entries ranked first. Use for blocklist screening.
Key constraints:
- Allowed file extensions: tiff, jpg, jpeg, png, webp (no PDF)
- Maximum file size: 5 MB (images are compressed to about 0.5 MB before processing)
- The image must contain a detectable face, otherwise HTTP
400{"error": "No face detected in the image"} - With several faces the largest one is searched and
MULTIPLE_FACES_DETECTEDis added vendor_datadoes not exclude the same user's earlier faces from the results (unlike Passive Liveness)
Pricing: $0.05 (per successful call; standalone calls have no free tier). These are the list prices until 1 November 2026, when Didit pricing changes and the 500 free checks a month become $10 of free credit a month. Check didit.me/pricing for current prices.
Decision: Declined only when FACE_IN_BLOCKLIST or POSSIBLE_FACE_IN_BLOCKLIST fires; otherwise Approved. Duplicate and multiple-face warnings never decline, and there is no In Review. Approved does not mean the face is new: check total_matches and the duplicate warnings. This endpoint returns no liveness verdict (use didit-liveness-detection for that).
Saving and enrollment (save_api_request, default true): the call is saved as an API session (Business Console, GET /v3/session/{request_id}/decision/, status.updated webhook), match_image_url values are presigned URLs, and the searched face is enrolled into the index (but never returned by later searches). With false nothing is stored, the face is not enrolled, and match_image_url values are internal storage paths that cannot be downloaded.
Similarity guidance: the Face Search docs read 90%+ as a strong likelihood of the same person and 70-89% as a possible match that may need review. Didit's own verdict is the warning: DUPLICATED_FACE / FACE_IN_BLOCKLIST for strong matches, POSSIBLE_DUPLICATED_FACE / POSSIBLE_FACE_IN_BLOCKLIST for weaker ones.
Sandbox: sandbox API keys return a static Approved payload after request validation; nothing is saved or billed.
API Reference: https://docs.didit.me/standalone-apis/face-search Feature Guide: https://docs.didit.me/core-technology/face-search/overview
Authentication
Every request needs the x-api-key header. Use your application's API key from the Didit Business Console under Developers > API keys, or create an account and key programmatically (see Getting Started). Named keys with custom access need Sessions write to run standalone checks; a key limited to some workflows or to approved sessions cannot run them.
Getting Started (No Account Yet?)
If you don't have a Didit API key, create one in 2 API calls:
- Register:
POST https://apx.didit.me/auth/v2/programmatic/register/with{"email": "[email protected]", "password": "MyStr0ng!Pass"} - Check email for a 6-character verification code
- Verify:
POST https://apx.didit.me/auth/v2/programmatic/verify-email/with{"email": "[email protected]", "code": "A3K9F2"}. The key is in the response atapplication.api_key.
To add credits: GET /v3/billing/balance/ to check, POST /v3/billing/top-up/ with {"amount_in_dollars": 50} for a Stripe checkout link.
See the didit-verification-management skill for full platform management (workflows, sessions, users, billing).
Endpoint
POST https://verification.didit.me/v3/face-search/
Headers
| Header | Value | Required |
|---|---|---|
x-api-key | Your API key | Yes |
Content-Type | multipart/form-data (set by your HTTP client) | Yes |
Request Parameters (multipart/form-data)
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
user_image | file | Yes | - | Face image to search (tiff/jpg/jpeg/png/webp, max 5 MB) |
search_type | string | No | most_similar | most_similar or blocklisted_or_approved (see Overview) |
rotate_image | boolean | No | false | Try 90-degree rotations and use the orientation with the best face detection. Adds latency |
save_api_request | boolean | No | true | Save as an API session and enroll the searched face (see Overview) |
vendor_data | string | No | - | Your user identifier, stored and echoed back. Does not filter results |
metadata | JSON object | No | - | Stored with the session (when saved) and echoed back. Send as a JSON-encoded string in multipart |
Example
import requests
with open("photo.jpg", "rb") as f:
response = requests.post(
"https://verification.didit.me/v3/face-search/",
headers={"x-api-key": "YOUR_API_KEY"},
files={"user_image": ("photo.jpg", f, "image/jpeg")},
data={"search_type": "most_similar", "vendor_data": "user-123"},
timeout=60,
)
response.raise_for_status()
result = response.json()["face_search"]
for match in result["matches"]:
print(match["similarity_percentage"], match["source"], match["session_id"], match["is_blocklisted"])
const formData = new FormData();
formData.append("user_image", photoFile);
formData.append("search_type", "most_similar");
const response = await fetch("https://verification.didit.me/v3/face-search/", {
method: "POST",
headers: { "x-api-key": "YOUR_API_KEY" },
body: formData,
});
const { face_search } = await response.json();
Response (200 OK)
{
"request_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
"face_search": {
"status": "Approved",
"total_matches": 1,
"matches": [
{
"session_id": "882c42d5-8a4d-4d20-8080-a22f57822c86",
"session_number": 323442,
"similarity_percentage": 97.31,
"source": "session",
"vendor_data": "user-1",
"vendor_user_id": "2f7c1a9e-4b28-4a6d-97f1-83576f80e1b4",
"biometric_template_id": null,
"verification_date": "2025-01-01T00:00:00Z",
"user_details": {
"full_name": "Jane Doe",
"document_type": "ID",
"document_number": "X1234567"
},
"match_image_url": "https://<media-host>/face/3f6a1c2e/reference.jpg",
"status": "Approved",
"is_blocklisted": false,
"is_allowlisted": false,
"api_service": null
}
],
"user_image": {
"entities": [
{"bbox": [40, 40, 120, 120], "confidence": 0.732973}
],
"best_angle": 0
},
"warnings": [
{
"risk": "DUPLICATED_FACE",
"feature": "LIVENESS",
"additional_data": {
"duplicated_session_id": "882c42d5-8a4d-4d20-8080-a22f57822c86",
"duplicated_session_number": 323442,
"api_service": null
},
"log_type": "information",
"short_description": "Duplicated face from other approved session",
"long_description": "The system identified a duplicated face from another approved session, requiring further investigation."
}
]
},
"vendor_data": "user-123",
"metadata": null,
"created_at": "2026-06-12T01:04:42.763237+00:00"
}
Status Values & Handling
| Status | Meaning | Action |
|---|---|---|
"Approved" | No blocklist hit | Check total_matches and DUPLICATED_FACE / POSSIBLE_DUPLICATED_FACE before treating the user as new |
"Declined" | FACE_IN_BLOCKLIST or POSSIBLE_FACE_IN_BLOCKLIST fired | Block or review; see matches[].is_blocklisted and the warning's additional_data |
Error Responses
| Code | Meaning | Action |
|---|---|---|
400 | Invalid request | Read the field errors in the response body |
400 | {"error": "No face detected in the image"} | Ask for a new image with one clear, front-facing face (or retry with rotate_image=true) |
401 | The API key has expired | Rotate it or create a new key in Developers > API keys |
403 | Missing, wrong or revoked API key; a scoped key without access to this endpoint; a request from outside the key's IP allowlist; or not enough credits | Read the message in detail or error: fix the x-api-key header or the key's access, or top up |
429 | Rate limit: 300 POST/PATCH/DELETE and 600 GET requests a minute per key | Wait the Retry-After seconds, then retry |
Response Field Reference
Top level
| Field | Type | Description |
|---|---|---|
request_id | uuid | Session id when save_api_request=true (use with GET /v3/session/{request_id}/decision/); otherwise a one-off correlation id |
face_search.status | string | "Approved" or "Declined" |
face_search.total_matches | integer | Number of entries in matches (0 to 5) |
face_search.matches[] | array | Up to 5 matches, ordered by similarity (blocklisted first with blocklisted_or_approved). Empty when nothing is similar enough |
face_search.user_image.entities[] | array | One entry per detected face, with only bbox [x_min, y_min, x_max, y_max] and confidence (0-1) |
face_search.user_image.best_angle | integer | Rotation (degrees) that gave the best detection; 0 unless rotate_image=true corrected it |
face_search.warnings[] | array | {risk, feature, additional_data, log_type, short_description, long_description}. feature is always LIVENESS |
vendor_data / metadata | string / object or null | Echo of what you sent |
created_at | string | ISO 8601 UTC time of the response |
Match Object
| Field | Type | Description |
|---|---|---|
similarity_percentage | float | 0-100 similarity between your face and the matched face |
source | string | session (verification session or saved standalone call), imported (face uploaded to a User profile), list_entry (face on one of your lists), retained_template (retained image-free template, see /v3/biometric-templates/) |
session_id | uuid or null | Matched session. null for imported and retained_template; for list_entry the session the entry was taken from, if any |
session_number | integer or null | Session number shown in the Business Console, same rule as session_id |
vendor_data | string or null | vendor_data of the matched session, or of the linked User for imported / retained_template |
vendor_user_id | uuid or null | Linked User id. null for list entries and sessions without a User |
biometric_template_id | uuid or null | Set only for retained_template |
verification_date | string or null | When the matched face was captured (YYYY-MM-DDThh:mm:ssZ). null for list_entry |
user_details | object or null | {full_name, document_type, document_number} from the matched session's document verification. imported matches carry only full_name. null when there is no identity data (e.g. liveness-only sessions) and for retained_template |
match_image_url | string or null | Time-limited URL of the matched face (4 hours by default) when save_api_request=true; an internal storage path when false; null for retained_template |
status | string or null | Status of the matched session (Approved, Declined, In Review, ...). null for non-session matches |
is_blocklisted | boolean | Matched face is on your face blocklist |
is_allowlisted | boolean | Matched face is on one of your allowlists. An allowlisted match suppresses the duplicate warnings |
api_service | string or null | Set when the matched face came from a standalone API call (e.g. PASSIVE_LIVENESS) |
Warning Tags
| Tag | Severity | Declines | When it fires |
|---|---|---|---|
FACE_IN_BLOCKLIST | error | Yes | Strong match with a blocklisted face. additional_data includes blocklisted_session_id, blocklisted_session_number, api_service |
POSSIBLE_FACE_IN_BLOCKLIST | error | Yes | Weaker match with a blocklisted face |
MULTIPLE_FACES_DETECTED | warning | No | More than one face; the largest one was searched |
DUPLICATED_FACE | information | No | Strong match with a face from an approved session, an imported face or a retained template. Not raised when FACE_IN_BLOCKLIST is. additional_data includes duplicated_session_id, duplicated_session_number, api_service |
POSSIBLE_DUPLICATED_FACE | information | No | Weaker match of the same kind. Not raised when POSSIBLE_FACE_IN_BLOCKLIST is |
- There is no
NO_FACE_DETECTEDwarning: an image without a face returns HTTP400. - Base decisions on
status,warnings[].riskandmatches[]; treat thelog_typeechoed on each warning as informational. - Manage the face blocklist and allowlists with the Lists API (see didit-verification-management).
Common Workflows
Duplicate Account Detection
1. New user signs up -> POST /v3/face-search/ with the selfie (search_type=most_similar)
2. status "Declined" -> blocklisted face: reject or send to manual review
3. DUPLICATED_FACE / POSSIBLE_DUPLICATED_FACE -> the face already passed on another account:
compare matches[].vendor_data / vendor_user_id with the current user, then investigate
4. total_matches == 0 -> no similar face is enrolled
Blocklist Screening Only
1. POST /v3/face-search/ with search_type=blocklisted_or_approved
2. "Declined" -> blocklist hit (matches[] lists blocklisted entries first)
Combined Verification + Dedup
1. POST /v3/face-search/ -> check for existing accounts (run it BEFORE any call that saves this selfie)
2. POST /v3/passive-liveness/ -> verify the user is live (also enrolls the face when saved)
3. POST /v3/id-verification/ -> verify the identity document
4. POST /v3/face-match/ -> compare the selfie to the document portrait
5. All Approved and no duplicate warning -> verified, unique, real user
Run the search first: a selfie already saved by Passive Liveness or Age Estimation is in the index and comes back as a near-100% match whose api_service names that standalone API (e.g. PASSIVE_LIVENESS).
Security: match image URLs expire (4 hours by default). Store only
request_id,session_idandsimilarity_percentage; keep biometric images off your servers.
Utility Scripts
search_faces.py: Search for matching faces from the command line.
# Requires: pip install requests
export DIDIT_API_KEY="your_api_key"
python scripts/search_faces.py selfie.jpg
python scripts/search_faces.py photo.png --search-type blocklisted_or_approved
python scripts/search_faces.py photo.png --rotate --vendor-data user-123 --no-save