Reference

Standalone tools

One-shot /v3/tools/* utilities — AML, face search, signature verify/search/enroll, document extract — with full request/response and errors.

The /v3/tools/* endpoints are one-shot utilities that mirror the console Manual Tools, tenant-scoped by the API key. They run a single check without creating a verification session — useful for ad-hoc screening, back-office review, dedup lookups, and enrichment. Images are passed as base64-encoded strings in the body (a data: URI prefix is accepted and stripped).

Info

These endpoints are on the /v3 surface and authenticate with x-api-key. Errors carry { "detail": "<code>" }. They share the inference-heavy kyc_v3_tools rate-limit bucket — expect 429 under bursty load and back off.

Image inputs

Every image argument funnels through one decode chokepoint. Only JPEG and PNG are accepted — HEIC (iPhone), WEBP, GIF, BMP, and TIFF are rejected with 415 unsupported_image_type rather than failing confusingly downstream. Images must be ≤ 8 MB after base64 decoding.

Decode errorStatusMeaning
<field>_required (e.g. image_required, reference_required, probe_required)400The base64 field was empty or missing.
invalid_<field> (e.g. invalid_image)400Not decodable base64, or zero-length / over 8 MB.
unsupported_image_type415Decoded bytes aren't JPEG or PNG.

Billing & entitlements

The chargeable tools (aml-check, face-search, signature-verify, signature-search) carry the same entitlement gate and per-use billing as the console Manual Tools:

  • If the feature isn't enabled for the tenant → 403 { "detail": "feature_not_enabled", "features": ["<KEY>"] }.
  • If the balance can't cover the per-use rate → 402 { "detail": "insufficient_credits" }.
  • On success, one use is debited to the credit ledger with reason: "tool_usage".

signature-enroll and document-extract are not gated or charged.

Endpoints

EndpointBodyBilled feature
POST /v3/tools/aml-check/{ full_name, date_of_birth?, nationality?, country?, document_number?, lists?, match_threshold? }AML_CHECK
POST /v3/tools/face-search/{ image_base64, threshold?, top_k? }FACE_SEARCH
POST /v3/tools/signature-verify/{ reference_base64, probe_base64, threshold? }SIGNATURE_VERIFY
POST /v3/tools/signature-search/{ image_base64, threshold?, top_k? } (beta)SIGNATURE_SEARCH
POST /v3/tools/signature-enroll/{ individual_id | external_user_id, image_base64, label? }201
POST /v3/tools/document-extract/{ image_base64 }

AML check

Screen a name against sanctions / PEP / watchlist datasets. Same engine as the AML_SCREENING module, run standalone.

POST /v3/tools/aml-check/
full_namestringbodyrequired
Name to screen. (Or supply first_name + last_name.)
date_of_birthstringbody
YYYY-MM-DD, to corroborate and disambiguate matches.
nationalitystringbody
ISO-2 nationality.
countrystringbody
ISO-2 country.
document_numberstringbody
Optional identifier to match on.
listsarraybody
Restrict to specific watchlists; defaults to the tenant's enabled lists.
match_thresholdnumberbody
Minimum score to count as a confirmed hit; defaults to the configured AML threshold.
curl -X POST https://acme.thirdfactor.ai/v3/tools/aml-check/ \
  -H "x-api-key: $OBSIDIAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "full_name": "Aarav Sharma", "date_of_birth": "1990-04-12", "nationality": "NP" }'
const resp = await fetch("https://acme.thirdfactor.ai/v3/tools/aml-check/", {
  method: "POST",
  headers: {
    "x-api-key": process.env.OBSIDIAN_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    full_name: "Aarav Sharma",
    date_of_birth: "1990-04-12",
    nationality: "NP",
  }),
});
const result = await resp.json();
import requests

resp = requests.post(
    "https://acme.thirdfactor.ai/v3/tools/aml-check/",
    headers={"x-api-key": OBSIDIAN_API_KEY},
    json={"full_name": "Aarav Sharma", "date_of_birth": "1990-04-12", "nationality": "NP"},
)
result = resp.json()

Response Example

{
  "risk_level": "clear",
  "matches": [],
  "possible_matches": [],
  "informational_matches": [],
  "lists_checked": ["pep", "sanctions"],
  "provider": "watchlist",
  "dataset": "default",
  "algorithm": "logic-v1",
  "threshold": 0.85
}

A confirmed hit looks like this (abbreviated match object):

{
  "risk_level": "high",
  "matches": [
    {
      "entity_id": "NK-abc123",
      "name": "Aarav Sharma",
      "schema": "Person",
      "score": 0.97,
      "match": true,
      "lists": ["sanctions"],
      "topics": ["sanction"],
      "datasets": ["us_ofac_sdn"],
      "birth_date": "1990-04-12",
      "countries": ["np"],
      "aliases": [],
      "positions": [],
      "url": "https://…/entity/NK-abc123"
    }
  ],
  "possible_matches": [],
  "informational_matches": [],
  "lists_checked": ["pep", "sanctions"],
  "provider": "watchlist"
}
risk_levelstring

high (a confirmed hit on a blocking list), medium (only sub-threshold possible matches), low (only informational notices), or clear.

matchesarray
Confirmed hits at/above the threshold.
possible_matchesarray
Sub-threshold or uncorroborated candidates for a reviewer.
informational_matchesarray
Non-blocking notices (e.g. person-of-interest datasets).
lists_checkedarray
The watchlists actually screened.

Errors

StatusCodeWhen
400name_requiredNo full_name and no first_name/last_name.
402insufficient_creditsBalance can't cover the AML_CHECK rate.
403feature_not_enabledAML_CHECK not enabled for the tenant.
429rate-limitedOver the kyc_v3_tools bucket.
503screening_unavailableThe screening backend is unreachable.

1:N search over the tenant's enrolled identities — rank identities whose enrolled selfie best matches the probe photo. Read-only (nothing is flagged).

POST /v3/tools/face-search/
image_base64stringbodyrequired
Base64-encoded probe image (JPEG/PNG).
thresholdnumberbodyDefault: 0.35
Minimum similarity to return, clamped to 0.2..0.95.
top_kintegerbodyDefault: 12
Maximum matches to return, clamped to 1..50.
curl -X POST https://acme.thirdfactor.ai/v3/tools/face-search/ \
  -H "x-api-key: $OBSIDIAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "image_base64": "'"$(base64 -i selfie.jpg)"'", "top_k": 5 }'
import { readFileSync } from "node:fs";

const image_base64 = readFileSync("selfie.jpg").toString("base64");
const resp = await fetch("https://acme.thirdfactor.ai/v3/tools/face-search/", {
  method: "POST",
  headers: {
    "x-api-key": process.env.OBSIDIAN_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ image_base64, top_k: 5 }),
});
const { matches } = await resp.json();
import base64, requests

image_base64 = base64.b64encode(open("selfie.jpg", "rb").read()).decode()
resp = requests.post(
    "https://acme.thirdfactor.ai/v3/tools/face-search/",
    headers={"x-api-key": OBSIDIAN_API_KEY},
    json={"image_base64": image_base64, "top_k": 5},
)
matches = resp.json()["matches"]

Response Example

{
  "matches": [
    {
      "individual_id": "0c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
      "name": "Aarav Sharma",
      "external_user_id": "your-user-reference-123",
      "status": "active",
      "kyc_status": "approved",
      "score": 0.9142,
      "thumb": "https://acme.thirdfactor.ai/media/…/thumb.jpg"
    }
  ],
  "threshold": 0.35
}
matchesarray
Ranked candidates, highest score first.
scorenumber
Similarity in 0..1 (rounded to 4 decimals).
thresholdnumber
The effective threshold after clamping.

Errors

StatusCodeWhen
400 / 415image decode errorSee Image inputs.
402insufficient_creditsBalance can't cover the FACE_SEARCH rate.
403feature_not_enabledFACE_SEARCH not enabled.
422no_face_detectedNo usable face in the probe image.
429rate-limitedOver the kyc_v3_tools bucket.

Signature verify

1:1 comparison — compare a probe signature against a reference. Both inputs are first cropped to the signature ink, so a full ID photo or a video frame is matched on its signature, not the surrounding scene.

POST /v3/tools/signature-verify/
reference_base64stringbodyrequired
Base64-encoded reference signature.
probe_base64stringbodyrequired
Base64-encoded probe signature.
thresholdnumberbodyDefault: 0.62
Decision threshold, clamped to 0.2..0.95.
curl -X POST https://acme.thirdfactor.ai/v3/tools/signature-verify/ \
  -H "x-api-key: $OBSIDIAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reference_base64": "<b64>", "probe_base64": "<b64>" }'
const resp = await fetch("https://acme.thirdfactor.ai/v3/tools/signature-verify/", {
  method: "POST",
  headers: {
    "x-api-key": process.env.OBSIDIAN_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ reference_base64: refB64, probe_base64: probeB64 }),
});
const { verdict, matched } = await resp.json();
resp = requests.post(
    "https://acme.thirdfactor.ai/v3/tools/signature-verify/",
    headers={"x-api-key": OBSIDIAN_API_KEY},
    json={"reference_base64": ref_b64, "probe_base64": probe_b64},
)
verdict = resp.json()["verdict"]

Response Example

{
  "score": 0.9,
  "verdict": "match",
  "matched": true,
  "threshold": 0.62,
  "signals": {}
}
scorenumber
Similarity in 0..1.
verdictstring

match (score >= threshold), review (score in the [threshold × 0.75, threshold) grey band), or mismatch.

matchedboolean
Convenience boolean, true only for match.

Errors

StatusCodeWhen
400 / 415image decode errorSee Image inputs.
402insufficient_creditsBalance can't cover the SIGNATURE_VERIFY rate.
403feature_not_enabledSIGNATURE_VERIFY not enabled.
422not_a_signature / no_signature_detectedA human face was detected, or no signature ink was found.
429rate-limitedOver the kyc_v3_tools bucket.

Reverse (1:N) search over the tenant's enrolled signature specimens — a recall aid for operators, not a verdict.

POST /v3/tools/signature-search/
image_base64stringbodyrequired
Base64-encoded probe signature.
thresholdnumberbodyDefault: 0.6
Minimum similarity to return, clamped to 0.2..0.95.
top_kintegerbodyDefault: 12
Maximum matches, clamped to 1..50.

Response Example

{
  "matches": [
    {
      "individual_id": "0c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
      "name": "Aarav Sharma",
      "external_user_id": "your-user-reference-123",
      "status": "active",
      "score": 0.8321,
      "thumb": "https://acme.thirdfactor.ai/media/…/sig.png"
    }
  ],
  "threshold": 0.6,
  "beta": true
}

Note

Signature search is beta ("beta": true in the response). Treat hits as a recall aid and confirm with the 1:1 signature verify comparison before acting.

Errors

StatusCodeWhen
400 / 415image decode errorSee Image inputs.
402insufficient_creditsBalance can't cover the SIGNATURE_SEARCH rate.
403feature_not_enabledSIGNATURE_SEARCH not enabled.
422no_signature_detectedNo signature ink found in the probe.
429rate-limitedOver the kyc_v3_tools bucket.

Signature enroll

Add a reference signature specimen to an identity's gallery, growing what reverse search can find. Returns 201. Not gated or charged.

POST /v3/tools/signature-enroll/
individual_idstringbody
Enrolled individual UUID. Provide this or external_user_id.
external_user_idstringbody
Your external user reference. Provide this or individual_id.
image_base64stringbodyrequired
Base64-encoded signature to enroll.
labelstringbody
Optional label for the enrolled sample (truncated to 120 chars).
curl -X POST https://acme.thirdfactor.ai/v3/tools/signature-enroll/ \
  -H "x-api-key: $OBSIDIAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "external_user_id": "your-user-reference-123", "image_base64": "<b64>", "label": "account-opening" }'
resp = requests.post(
    "https://acme.thirdfactor.ai/v3/tools/signature-enroll/",
    headers={"x-api-key": OBSIDIAN_API_KEY},
    json={"external_user_id": "your-user-reference-123", "image_base64": sig_b64, "label": "account-opening"},
)
sample = resp.json()  # 201

Response Example

{
  "sample_id": "7a8b9c0d-1e2f-4a3b-5c6d-7e8f9a0b1c2d",
  "individual_id": "0c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f"
}

Errors

StatusCodeWhen
400 / 415image decode errorSee Image inputs.
404individual_not_foundNeither individual_id nor external_user_id resolves for this tenant.
422no_signature_detectedNo signature ink found in the image.
429rate-limitedOver the kyc_v3_tools bucket.

Document extract

Run OCR / field extraction on a single identity-document image via the platform document-OCR service. Works on any account (independent of the tenant's Lens config). Not gated or charged.

POST /v3/tools/document-extract/
image_base64stringbodyrequired
Base64-encoded document image (JPEG/PNG).
curl -X POST https://acme.thirdfactor.ai/v3/tools/document-extract/ \
  -H "x-api-key: $OBSIDIAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "image_base64": "'"$(base64 -i passport.jpg)"'" }'
import { readFileSync } from "node:fs";

const image_base64 = readFileSync("passport.jpg").toString("base64");
const resp = await fetch("https://acme.thirdfactor.ai/v3/tools/document-extract/", {
  method: "POST",
  headers: {
    "x-api-key": process.env.OBSIDIAN_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ image_base64 }),
});
const doc = await resp.json();
import base64, requests

image_base64 = base64.b64encode(open("passport.jpg", "rb").read()).decode()
resp = requests.post(
    "https://acme.thirdfactor.ai/v3/tools/document-extract/",
    headers={"x-api-key": OBSIDIAN_API_KEY},
    json={"image_base64": image_base64},
)
doc = resp.json()

Response Example

{
  "fields": {
    "full_name": "AARAV SHARMA",
    "document_number": "PA1234567",
    "date_of_birth": "1990-04-12",
    "expiry_date": "2030-04-11",
    "nationality": "NPL"
  },
  "translated_fields": {},
  "detected_type": "passport",
  "confidence": 0.96,
  "is_document_valid": true,
  "has_portrait": true
}
fieldsobject
Extracted document fields (as printed).
translated_fieldsobject
Romanised / English read of the same fields for non-Latin documents.
detected_typestring
The document type the OCR identified (e.g. passport, driving-license).
confidencenumber
Extraction confidence.
is_document_validboolean
Whether the document passed structural validity checks.
has_portraitboolean
Whether a portrait face was found on the document.

Note

For document types with no trained template (e.g. disability cards) the tool falls back to a generic extraction: it still returns fields plus a raw full_text, and adds generic_extraction: true with an extraction_note. Treat those fields as best-effort.

Errors

StatusCodeWhen
400 / 415image decode errorSee Image inputs.
422ocr_no_fieldsNo fields could be extracted (unsupported type or unreadable photo).
429rate-limitedOver the kyc_v3_tools bucket.
503ocr_not_configuredThe platform document-OCR service isn't configured.

Tools vs. a session

/v3/tools/*Session
Runs a full pipeline❌ — one check✅ — document + liveness + face + AML
Produces a decision❌ — raw result only✅ — decision + decision_v3
Audit trail / webhook✅ — logs, PDF, signed webhook
Hosted UI for the applicant❌ — you supply images✅ — hosted flow
Best forBack-office / ad-hoc lookupsEnd-to-end identity verification

Tip

For a full document + liveness + face-match pipeline with a decision and audit trail, create a session instead of stitching these tools together.

Common pitfalls

Warning

Sending HEIC/WEBP images. Phone cameras default to HEIC and Chrome to WEBP — both are rejected with 415 unsupported_image_type. Transcode to JPEG or PNG before base64-encoding.

Warning

Treating face/signature search as a verdict. Search is 1:N recall — a hit is a candidate, not a decision. Confirm identity with a 1:1 comparison or a full session before acting on a match.

FAQ