Verification

Standalone tools

One-shot utilities — AML, face search, signature verify/search/enroll, document extract — with full request/response JSON and no session attached.

The /v3/tools/* endpoints mirror the console's Manual Tools. They are stateless: none of them create or mutate a verification session. Each is tenant-scoped by your API key (x-api-key), and image arguments are base64 strings — raw or a data: URI.

Warning

A tool result is a lookup, not a verification. It carries no liveness, no face match, and no audit trail. Don't use document-extract or aml-check as a substitute for a real module in a compliance-bearing decision — use the tools for enrichment, triage and operator tooling.

Conventions

  • Auth: x-api-key: <tenant_api_key> (or X-API-Key). See Authentication.
  • Images: base64 (optionally a data: URI), JPEG or PNG only, max 8 MB. Other formats (HEIC, WEBP, GIF, …) return 415 unsupported_image_type; a missing/undecodable image returns 400 <field>_required / 400 invalid_<field>.
  • Rate limit: the tools share a per-tenant bucket of 120 requests/minute (kyc_v3_tools). Over-limit requests get 429.
  • Entitlement + billing: each chargeable tool is gated by your plan. A disabled feature returns 403 feature_not_enabled (with a features array); an empty balance returns 402 insufficient_credits. A successful call debits one use.
  • Errors: /v3 tool errors return { "detail": "<code>" } with the HTTP status carrying the machine signal. Branch on status + the stable code, not the message.
EndpointPurposeKey body fieldsChargeable
POST /v3/tools/aml-check/Sanctions/PEP/adverse-media screeningfull_name | first_name+last_name, date_of_birth?, nationality?, country?, document_number?, lists?, match_threshold?
POST /v3/tools/face-search/1:N face search over enrolled identitiesimage_base64, threshold?, top_k?
POST /v3/tools/signature-verify/1:1 signature comparisonreference_base64, probe_base64, threshold?
POST /v3/tools/signature-search/1:N signature search (beta)image_base64, threshold?, top_k?
POST /v3/tools/signature-enroll/Add a signature specimen to an identityindividual_id | external_user_id, image_base64, label?
POST /v3/tools/document-extract/Identity-document OCRimage_base64, document_type?

AML check

Ad-hoc watchlist screening for a name — the same engine as the AML_SCREENING module. A name is the minimum useful selector; every other field narrows the candidate set.

curl -X POST "$TF_BASE_URL/v3/tools/aml-check/" \
  -H "x-api-key: $TF_API_KEY" -H "Content-Type: application/json" \
  -d '{
        "full_name": "Aarav Sharma",
        "date_of_birth": "1990-04-12",
        "nationality": "NP",
        "lists": ["sanctions", "pep"],
        "match_threshold": 0.7
      }'
const res = await fetch(`${process.env.TF_BASE_URL}/v3/tools/aml-check/`, {
  method: "POST",
  headers: {
    "x-api-key": process.env.TF_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    full_name: "Aarav Sharma",
    date_of_birth: "1990-04-12",
    nationality: "NP",
    lists: ["sanctions", "pep"],
    match_threshold: 0.7,
  }),
});
const screen = await res.json();
console.log(screen.risk_level, screen.matches.length);
import requests

res = requests.post(
    f"{BASE_URL}/v3/tools/aml-check/",
    headers={"x-api-key": API_KEY},
    json={
        "full_name": "Aarav Sharma",
        "date_of_birth": "1990-04-12",
        "nationality": "NP",
        "lists": ["sanctions", "pep"],
        "match_threshold": 0.7,
    },
    timeout=30,
)
screen = res.json()
print(screen["risk_level"], len(screen["matches"]))
{
  "risk_level": "medium",
  "matches": [],
  "possible_matches": [
    {
      "entity_id": "NK-9x2",
      "name": "Aarav Sharma",
      "schema": "Person",
      "score": 0.63,
      "match": false,
      "lists": ["pep"],
      "topics": ["role.pep"],
      "datasets": ["everypolitician"],
      "birth_date": "",
      "countries": ["np"],
      "aliases": [],
      "nationalities": ["np"],
      "positions": ["Member of Parliament"],
      "gender": "male",
      "addresses": [],
      "id_numbers": [],
      "url": ""
    }
  ],
  "informational_matches": [],
  "lists_checked": ["pep", "sanctions"],
  "provider": "watchlist",
  "dataset": "default",
  "algorithm": "logic-v1",
  "threshold": 0.7
}

risk_level is high / medium / low / clear. A query with no name returns 400 name_required; an unreachable engine returns 503 screening_unavailable. Full field reference, list topics, scoring tiers and the corroboration rule: AML screening.

Ranks your tenant's enrolled identities whose selfie matches a probe photo — duplicate-account detection. Read-only (nothing is flagged); it mirrors the automatic duplicate check.

curl -X POST "$TF_BASE_URL/v3/tools/face-search/" \
  -H "x-api-key: $TF_API_KEY" -H "Content-Type: application/json" \
  -d '{"image_base64":"<jpg/png base64>","threshold":0.35,"top_k":12}'
const res = await fetch(`${process.env.TF_BASE_URL}/v3/tools/face-search/`, {
  method: "POST",
  headers: {
    "x-api-key": process.env.TF_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ image_base64: imageB64, threshold: 0.35, top_k: 12 }),
});
const { matches } = await res.json();
console.log(matches.map((m: any) => [m.external_user_id, m.score]));
import requests

res = requests.post(
    f"{BASE_URL}/v3/tools/face-search/",
    headers={"x-api-key": API_KEY},
    json={"image_base64": image_b64, "threshold": 0.35, "top_k": 12},
    timeout=30,
)
for m in res.json()["matches"]:
    print(m["external_user_id"], m["score"])
image_base64stringbodyrequired
Probe face photo (JPEG/PNG, base64).
thresholdnumberbody
Match cutoff, clamped to 0.2–0.95. Default 0.35.
top_knumberbody
Max results, clamped to 1–50. Default 12.
{
  "matches": [
    {
      "individual_id": "9f3a…",
      "name": "Aarav Sharma",
      "external_user_id": "user-123",
      "status": "active",
      "kyc_status": "clear",
      "score": 0.71,
      "thumb": "https://…/face.jpg"
    }
  ],
  "threshold": 0.35
}

422 no_face_detected if there's no usable face in the probe.

Signature verify

Compares a probe signature to a reference — structural similarity of stroke geometry (not pen dynamics). Both inputs are cropped to just the signature ink first, so a full document photo or a video frame is matched on its ink, not the surroundings.

curl -X POST "$TF_BASE_URL/v3/tools/signature-verify/" \
  -H "x-api-key: $TF_API_KEY" -H "Content-Type: application/json" \
  -d '{"reference_base64":"<base64>","probe_base64":"<base64>","threshold":0.62}'
const res = await fetch(`${process.env.TF_BASE_URL}/v3/tools/signature-verify/`, {
  method: "POST",
  headers: {
    "x-api-key": process.env.TF_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    reference_base64: referenceB64,
    probe_base64: probeB64,
    threshold: 0.62,
  }),
});
const out = await res.json();
console.log(out.verdict, out.score);
import requests

res = requests.post(
    f"{BASE_URL}/v3/tools/signature-verify/",
    headers={"x-api-key": API_KEY},
    json={"reference_base64": reference_b64, "probe_base64": probe_b64, "threshold": 0.62},
    timeout=30,
)
out = res.json()
print(out["verdict"], out["score"])
reference_base64stringbodyrequired
Reference signature (JPEG/PNG, base64).
probe_base64stringbodyrequired
Probe signature to compare.
thresholdnumberbody
Match cutoff, clamped to 0.2–0.95. Default 0.62.
{
  "score": 0.68,
  "verdict": "match",
  "matched": true,
  "threshold": 0.62
}

The verdict is derived against the requested threshold:

verdictCondition
matchscore ≥ threshold
reviewscore ≥ 0.75 × threshold
mismatchbelow the review floor

Note

If an input can't be read as a signature you get a 422no_signature_detected when no stroke-shaped ink is found, or not_a_signature when a human face is detected (a photo of a person is not a signature). The tool never returns a falsely-confident match.

Signature search (beta)

Ranks your tenant's enrolled signature specimens against a probe — a recall aid for operators, not a verdict. The probe is cropped to the signature ink first, the same treatment enrollment applies, so both sides of the search are built from the same kind of crop.

curl -X POST "$TF_BASE_URL/v3/tools/signature-search/" \
  -H "x-api-key: $TF_API_KEY" -H "Content-Type: application/json" \
  -d '{"image_base64":"<png base64>","threshold":0.6,"top_k":12}'
const res = await fetch(`${process.env.TF_BASE_URL}/v3/tools/signature-search/`, {
  method: "POST",
  headers: {
    "x-api-key": process.env.TF_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ image_base64: imageB64, threshold: 0.6, top_k: 12 }),
});
const { matches, beta } = await res.json();
console.log(beta, matches.length);
import requests

res = requests.post(
    f"{BASE_URL}/v3/tools/signature-search/",
    headers={"x-api-key": API_KEY},
    json={"image_base64": image_b64, "threshold": 0.6, "top_k": 12},
    timeout=30,
)
data = res.json()
print(data["beta"], len(data["matches"]))
image_base64stringbodyrequired
Probe signature (JPEG/PNG, base64).
thresholdnumberbody
Match cutoff, clamped to 0.2–0.95. Default 0.6.
top_knumberbody
Max results, clamped to 1–50. Default 12.
{
  "matches": [
    {
      "individual_id": "9f3a…",
      "name": "Aarav Sharma",
      "external_user_id": "user-123",
      "status": "active",
      "score": 0.64,
      "thumb": "https://…/signature.png"
    }
  ],
  "threshold": 0.6,
  "beta": true
}

422 no_signature_detected if the probe has no readable signature. Confirm any hit with the 1:1 signature verify.

Signature enroll

Attaches a reference signature to an identity so it becomes searchable by signature search. The input is cropped to the signature ink first, so a full video frame / document photo enrolls a clean specimen.

curl -X POST "$TF_BASE_URL/v3/tools/signature-enroll/" \
  -H "x-api-key: $TF_API_KEY" -H "Content-Type: application/json" \
  -d '{"external_user_id":"user-123","image_base64":"<png base64>","label":"account-opening"}'
const res = await fetch(`${process.env.TF_BASE_URL}/v3/tools/signature-enroll/`, {
  method: "POST",
  headers: {
    "x-api-key": process.env.TF_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    external_user_id: "user-123",
    image_base64: imageB64,
    label: "account-opening",
  }),
});
const out = await res.json(); // 201
console.log(out.sample_id, out.individual_id);
import requests

res = requests.post(
    f"{BASE_URL}/v3/tools/signature-enroll/",
    headers={"x-api-key": API_KEY},
    json={
        "external_user_id": "user-123",
        "image_base64": image_b64,
        "label": "account-opening",
    },
    timeout=30,
)
out = res.json()  # 201
print(out["sample_id"], out["individual_id"])
individual_idstringbody
Identify the person by internal id (use this or external_user_id).
external_user_idstringbody
Identify the person by your own user id (most recent identity wins).
image_base64stringbodyrequired
Signature specimen (JPEG/PNG, base64).
labelstringbody
Optional free-text label (e.g. account-opening), truncated to 120 chars.
{
  "sample_id": "c2b8…",
  "individual_id": "9f3a…"
}

On success returns 201. An unknown person returns 404 individual_not_found; an image with no readable signature returns 422 no_signature_detected.

Document extract

Identity-document OCR — the same read as ID_VERIFICATION, run standalone. It uses the platform document-OCR service, so it works on any account regardless of the tenant's Lens config.

curl -X POST "$TF_BASE_URL/v3/tools/document-extract/" \
  -H "x-api-key: $TF_API_KEY" -H "Content-Type: application/json" \
  -d '{"image_base64":"<base64>","document_type":"passport"}'
const res = await fetch(`${process.env.TF_BASE_URL}/v3/tools/document-extract/`, {
  method: "POST",
  headers: {
    "x-api-key": process.env.TF_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ image_base64: imageB64, document_type: "passport" }),
});
const doc = await res.json();
console.log(doc.detected_type, doc.fields);
import requests

res = requests.post(
    f"{BASE_URL}/v3/tools/document-extract/",
    headers={"x-api-key": API_KEY},
    json={"image_base64": image_b64, "document_type": "passport"},
    timeout=30,
)
doc = res.json()
print(doc["detected_type"], doc["fields"])
image_base64stringbodyrequired
Document photo (JPEG/PNG, base64).
document_typestringbody

Optional type hint to recover documents auto-detect misses: passport, drivers_license, citizenship, pan, disability_id. Auto-detect covers most cases.

fieldsobject
Extracted fields (full name, document number, DOB, expiry, nationality).
translated_fieldsobject
Romanised / English read of the same fields for non-Latin documents.
detected_typestring
The classified document type.
confidencenumber
Classification confidence.
is_document_validboolean
Whether the read looks like a valid document.
has_portraitboolean
Whether a portrait was found.
{
  "fields": {
    "full_name": "AARAV SHARMA",
    "document_number": "PA1234567",
    "date_of_birth": "1990-04-12",
    "expiry_date": "2032-01-10",
    "nationality": "NP"
  },
  "translated_fields": {},
  "detected_type": "passport",
  "confidence": 0.97,
  "is_document_valid": true,
  "has_portrait": true
}

Template-less documents (e.g. disability cards) run a generic extraction — auto-detect fields plus best-effort local OCR text, flagged with generic_extraction: true and a full_text string. Aadhaar cards, which the classifiers can only call "national id", are relabelled to aadhaar from OCR-text signals (display-level only). 503 ocr_not_configured if the OCR service isn't available; 422 ocr_no_fields if nothing readable was found.

Error codes

Statusdetail codeMeaning
400<field>_required / invalid_<field>Missing or undecodable image / name.
402insufficient_creditsTenant balance below the tool's rate.
403feature_not_enabledThe tool isn't on your plan (see features).
404individual_not_foundSignature enroll target doesn't exist.
415unsupported_image_typeNot JPEG/PNG (HEIC, WEBP, GIF, …).
422no_face_detected / no_signature_detected / not_a_signature / ocr_no_fieldsNothing usable in the input.
429Over the 120 req/min tools bucket.
503screening_unavailable / ocr_not_configuredThe backing engine isn't reachable/configured.

FAQ