mcpField
D
didit-liveness-detection

didit-liveness-detection

github.com/didit-protocolSecurity29228 installsUpdated 2w ago
Repository

About didit-liveness-detection

A Didit skill for developers adding passive liveness checks to identity verification. It evaluates a single captured face image without video or user interaction and can save the result and enroll the face for later duplicate checks.

Key features

  1. 01Single-image passive liveness check
  2. 02Configurable score decline threshold
  3. 03Face blocklist and duplicate screening
  4. 04Optional session storage and face enrollment
  5. 05Warnings and decision status in response

Use cases

  1. 01Check a selfie during user onboarding
  2. 02Detect presentation attacks in identity checks
  3. 03Compare a face against blocklists and prior checks

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/didit-protocol/skills --skill didit-liveness-detection

After installing, type/didit-liveness-detectionin Claude Code to confirm it loaded.

Bundled filesscripts/check_liveness.py

Details

From the project README

Didit Passive Liveness API

Overview

Verifies that a user is physically present by analyzing a single captured image. No video, movement or user interaction is required.

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, so higher resolutions do not improve accuracy)
  • One clearly visible face. With several faces the largest one is scored and MULTIPLE_FACES_DETECTED is added
  • Original real-time capture (no screenshots, prints or photos of screens)

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.

Accuracy: 99.9% liveness detection accuracy, <0.1% false acceptance rate (FAR).

Decision: status is Declined when any error warning fires (no face, score at or below the threshold, presentation attack, blocklist hit) and Approved otherwise. There is no In Review on this endpoint. Only the score threshold is configurable.

Blocklist and duplicate screening: runs on every call, also with save_api_request=false. The face is compared with your application's face search index (your face blocklist and allowlist, faces from approved sessions, faces uploaded to User profiles and retained biometric templates). Earlier faces with the same vendor_data are skipped for the duplicate check, so re-verifying the same user does not raise DUPLICATED_FACE. A blocklisted face is still caught when it carries the same vendor_data. A match on your face allowlist suppresses the duplicate warnings.

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), the image and face crop are stored, and the face is enrolled into your face search index so later Face Search calls and duplicate checks can match it. With false nothing is stored, the face is not enrolled and request_id cannot be looked up.

Sandbox: sandbox API keys return a static Approved payload after request validation; nothing is saved or billed.

Liveness methods: this endpoint always uses PASSIVE (single-frame CNN). Hosted workflows can also use ACTIVE_3D (3D Action & Flash, highest security) and FLASHING (3D Flash, high security) through the face_liveness_method workflow setting.

API Reference: https://docs.didit.me/standalone-apis/passive-liveness Feature Guide: https://docs.didit.me/core-technology/liveness/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:

  1. Register: POST https://apx.didit.me/auth/v2/programmatic/register/ with {"email": "[email protected]", "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": "[email protected]", "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/passive-liveness/
Headers
HeaderValueRequired
x-api-keyYour API keyYes
Content-Typemultipart/form-data (set by your HTTP client)Yes
Request Parameters (multipart/form-data)
ParameterTypeRequiredDefaultConstraintsDescription
user_imagefileYes-tiff/jpg/jpeg/png/webp, max 5 MBFront-facing face image (selfie)
face_liveness_score_decline_thresholdfloatNo300-100A score at or below this value raises LOW_LIVENESS_SCORE and declines. Raise it for a stricter anti-spoofing posture
rotate_imagebooleanNofalse-Try 90-degree rotations and use the orientation with the best face detection. Adds latency
save_api_requestbooleanNotrue-Save as an API session and enroll the face (see Overview)
vendor_datastringNo--Your user identifier. Stored, echoed back, and used to skip the same user's earlier faces in the duplicate check
metadataJSON objectNo-JSON-encoded string in multipartStored with the session (when saved) and echoed back
Example
import json

import requests

with open("selfie.jpg", "rb") as f:
    response = requests.post(
        "https://verification.didit.me/v3/passive-liveness/",
        headers={"x-api-key": "YOUR_API_KEY"},
        files={"user_image": ("selfie.jpg", f, "image/jpeg")},
        data={
            "face_liveness_score_decline_threshold": "50",
            "vendor_data": "user-123",
            "metadata": json.dumps({"flow": "withdrawal"}),
        },
        timeout=60,
    )
response.raise_for_status()
liveness = response.json()["liveness"]
print(liveness["status"], liveness["score"], [w["risk"] for w in liveness["warnings"]])
const formData = new FormData();
formData.append("user_image", selfieFile);
formData.append("face_liveness_score_decline_threshold", "50");
formData.append("vendor_data", "user-123");
formData.append("metadata", JSON.stringify({ flow: "withdrawal" }));

const response = await fetch("https://verification.didit.me/v3/passive-liveness/", {
  method: "POST",
  headers: { "x-api-key": "YOUR_API_KEY" },
  body: formData,
});
const { liveness } = await response.json();
Response (200 OK)
{
  "request_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
  "liveness": {
    "status": "Approved",
    "method": "PASSIVE",
    "score": 95.0,
    "user_image": {
      "entities": [
        {"bbox": [661, 728, 1688, 2188], "confidence": 0.732973, "age": 26.91, "gender": "male"}
      ],
      "best_angle": 0
    },
    "warnings": [],
    "face_quality": 84.21,
    "face_luminance": 50.33
  },
  "vendor_data": "user-123",
  "metadata": {"flow": "withdrawal"},
  "created_at": "2026-06-12T01:04:42.763237+00:00"
}

A spoofed or blocklisted face still returns HTTP 200: always read liveness.status and liveness.warnings.

Status Values & Handling
StatusMeaningAction
"Approved"No error warning firedProceed. Still check warnings for DUPLICATED_FACE / POSSIBLE_DUPLICATED_FACE / MULTIPLE_FACES_DETECTED
"Declined"At least one error warning firedRead warnings[].risk: retake for capture problems, block or review for attacks and blocklist hits
Error Responses
CodeMeaningAction
400Invalid requestRead the field errors in the response body
400{"error": "Invalid user image format."}: allowed extension but the file cannot be decodedRe-encode the image (JPEG or PNG) and retry
401The API key has expiredRotate it or create a new key in Developers > API keys
403Missing, 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 creditsRead the message in detail or error: fix the x-api-key header or the key's access, or top up
429Rate limit: 300 POST/PATCH/DELETE and 600 GET requests a minute per keyWait the Retry-After seconds, then retry

Response Field Reference

FieldTypeDescription
request_iduuidSession id when save_api_request=true (use with GET /v3/session/{request_id}/decision/); otherwise a one-off correlation id
liveness.statusstring"Approved" or "Declined"
liveness.methodstringAlways "PASSIVE"
liveness.scorefloat or null0-100 (two decimals), higher means more likely a live person. null when the model returned no confidence; a null score counts as 0 against the threshold
liveness.user_image.entities[]arrayOne entry per detected face: bbox [x_min, y_min, x_max, y_max], confidence (0-1), age, gender. Age and gender are informational and do not affect the decision
liveness.user_image.best_angleinteger or nullRotation (degrees) that gave the best detection; non-zero only when rotate_image=true corrected it
liveness.warnings[]array{risk, feature, additional_data, log_type, short_description, long_description}. feature is always LIVENESS
liveness.face_qualityfloat or nullCapture quality of the face, 0-100. null when no reliable value
liveness.face_luminancefloat or nullBrightness of the face, 0-100. null when unavailable
vendor_datastring or nullEcho of what you sent
metadataobject or nullEcho of what you sent
created_atstringISO 8601 UTC time of the response

Warning Tags

TagSeverityDeclinesWhen it fires
NO_FACE_DETECTEDerrorYesThe liveness model found no face
LOW_LIVENESS_SCOREerrorYesscore at or below face_liveness_score_decline_threshold. Not raised together with NO_FACE_DETECTED
LIVENESS_FACE_ATTACKerrorYesPresentation attack detected (print, screen replay, mask, deepfake)
FACE_IN_BLOCKLISTerrorYesStrong match with a face on your face blocklist. additional_data includes blocklisted_session_id, blocklisted_session_number, api_service
POSSIBLE_FACE_IN_BLOCKLISTerrorYesWeaker match with a blocklisted face. This endpoint declines it (workflows send it to review)
MULTIPLE_FACES_DETECTEDwarningNoMore than one face; the largest one was scored
DUPLICATED_FACEinformationNoStrong match with a face from another approved session (or an imported face or retained template). additional_data includes duplicated_session_id, duplicated_session_number, api_service
POSSIBLE_DUPLICATED_FACEinformationNoWeaker match of the same kind
  • LOW_FACE_QUALITY, LOW_FACE_LUMINANCE and HIGH_FACE_LUMINANCE are never raised by this endpoint (they are workflow warnings). Apply your own rule to face_quality / face_luminance if you need one.
  • Base decisions on status and warnings[].risk. The severities above are this endpoint's rules; treat the log_type echoed on each warning as informational.

Common Workflows

Basic Liveness Check
1. Capture the user's selfie
2. POST /v3/passive-liveness/ with user_image and vendor_data=<your user id>
3. "Approved" -> the user is live, proceed
   "Declined" -> check warnings[].risk:
     NO_FACE_DETECTED / LOW_LIVENESS_SCORE -> ask for a retake: one well-lit, centered face
     LIVENESS_FACE_ATTACK                  -> suspected spoof, do not approve
     FACE_IN_BLOCKLIST / POSSIBLE_FACE_IN_BLOCKLIST -> block or send to manual review
4. "Approved" with DUPLICATED_FACE -> this face already passed under another vendor_data:
   look up additional_data.duplicated_session_id before creating a new account
Liveness + Face Match (combined)
1. POST /v3/passive-liveness/ -> verify the selfie is live
2. If Approved -> POST /v3/face-match/ with user_image=selfie, ref_image=ID portrait
3. Both Approved -> the live person matches the document photo

Need a ranked list of every similar face rather than a blocklist/duplicate verdict? Use the didit-face-search skill, and run it before saving the same selfie here (a saved liveness face is itself searchable).


Utility Scripts

export DIDIT_API_KEY="your_api_key"

python scripts/check_liveness.py selfie.jpg
python scripts/check_liveness.py selfie.jpg --threshold 50 --vendor-data user-123
python scripts/check_liveness.py selfie.jpg --rotate --no-save --metadata '{"flow": "withdrawal"}'

Comments

Markdown supported