Catalogskills.sh · didit-protocol

didit-face-search

Runs a 1:N face search with Didit's standalone Face Search API (POST /v3/face-search/) against every face enrolled in the application: verification sessions, saved standalone calls, faces uploaded to User profiles, face lists and retained biometric templates. Use when detecting duplicate accounts, checking whether a face already exists, preventing repeat registrations, screening a face against the face blocklist, or investigating fraud rings. Returns up to 5 ranked matches with similarity percentages, match source and session details, plus blocklist and duplicate-face warnings.

Review advisedVerified hub389 installs

Savant verdict: Review advised

Security analysis or source-hub review identified patterns requiring attention.

Live evaluation

Not evaluated yet. Workspaces can request a live evaluation.

Security assessment

Scan pending.

Structure

  • Ships 1 executable scriptscripts/search_faces.py. Review what they do before enabling the skill for agents with tool access.
  • No license declaredConfirm you may reuse this skill before importing it into your repository.
  • The source hub's security checks flagged this skillRunlayer: fail (2/2 files flagged)

SKILL.md

---
name: didit-face-search
description: >
  Runs a 1:N face search with Didit's standalone Face Search API (POST /v3/face-search/)
  against every face enrolled in the application: verification sessions, saved standalone
  calls, faces uploaded to User profiles, face lists and retained biometric templates.
  Use when detecting duplicate accounts, checking whether a face already exists, preventing
  repeat registrations, screening a face against the face blocklist, or investigating
  fraud rings. Returns up to 5 ranked matches with similarity percentages, match source and
  session details, plus blocklist and duplicate-face warnings.
version: 1.3.0
metadata:
  openclaw:
    requires:
      env:
        - DIDIT_API_KEY
    primaryEnv: DIDIT_API_KEY
    emoji: "🔍"
    homepage: https://docs.didit.me
---

# Didit 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_DETECTED` is added
- `vendor_data` does **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](https://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](https://business.didit.me) 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:

1. **Register:** `POST https://apx.didit.me/auth/v2/programmatic/register/` with `{"email": "you@yourdomain.com", "password": "MyStr0ng!Pass"}`
2. **Check email** for a 6-character verification code
3. **Verify:** `POST https://apx.didit.me/auth/v2/programmatic/verify-email/` with `{"email": "you@yourdomain.com", "code": "A3K9F2"}`. The key is in the response at `application.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

```python
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"])
```

```typescript
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)

```json
{
  "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_DETECTED` warning: an image without a face returns HTTP `400`.
- Base decisions on `status`, `warnings[].risk` and `matches[]`; treat the `log_type` echoed 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_id` and `similarity_percentage`; keep biometric images off your servers.

---

## Utility Scripts

**search_faces.py**: Search for matching faces from the command line.

```bash
# 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
```
didit-face-search · Skill catalog | Savant