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 error | Status | Meaning |
|---|---|---|
<field>_required (e.g. image_required, reference_required, probe_required) | 400 | The base64 field was empty or missing. |
invalid_<field> (e.g. invalid_image) | 400 | Not decodable base64, or zero-length / over 8 MB. |
unsupported_image_type | 415 | Decoded 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
| Endpoint | Body | Billed 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_namestringbodyrequiredfirst_name + last_name.)date_of_birthstringbodyYYYY-MM-DD, to corroborate and disambiguate matches.nationalitystringbodycountrystringbodydocument_numberstringbodylistsarraybodymatch_thresholdnumberbodycurl -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_levelstringhigh (a confirmed hit on a blocking list), medium (only sub-threshold
possible matches), low (only informational notices), or clear.
matchesarraypossible_matchesarrayinformational_matchesarraylists_checkedarrayErrors
| Status | Code | When |
|---|---|---|
400 | name_required | No full_name and no first_name/last_name. |
402 | insufficient_credits | Balance can't cover the AML_CHECK rate. |
403 | feature_not_enabled | AML_CHECK not enabled for the tenant. |
429 | rate-limited | Over the kyc_v3_tools bucket. |
503 | screening_unavailable | The screening backend is unreachable. |
Face search
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_base64stringbodyrequiredthresholdnumberbodyDefault: 0.350.2..0.95.top_kintegerbodyDefault: 121..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
}matchesarrayscore first.scorenumber0..1 (rounded to 4 decimals).thresholdnumberErrors
| Status | Code | When |
|---|---|---|
400 / 415 | image decode error | See Image inputs. |
402 | insufficient_credits | Balance can't cover the FACE_SEARCH rate. |
403 | feature_not_enabled | FACE_SEARCH not enabled. |
422 | no_face_detected | No usable face in the probe image. |
429 | rate-limited | Over 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_base64stringbodyrequiredprobe_base64stringbodyrequiredthresholdnumberbodyDefault: 0.620.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": {}
}scorenumber0..1.verdictstringmatch (score >= threshold), review (score in the [threshold × 0.75, threshold) grey band), or mismatch.
matchedbooleantrue only for match.Errors
| Status | Code | When |
|---|---|---|
400 / 415 | image decode error | See Image inputs. |
402 | insufficient_credits | Balance can't cover the SIGNATURE_VERIFY rate. |
403 | feature_not_enabled | SIGNATURE_VERIFY not enabled. |
422 | not_a_signature / no_signature_detected | A human face was detected, or no signature ink was found. |
429 | rate-limited | Over the kyc_v3_tools bucket. |
Signature search
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_base64stringbodyrequiredthresholdnumberbodyDefault: 0.60.2..0.95.top_kintegerbodyDefault: 121..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
| Status | Code | When |
|---|---|---|
400 / 415 | image decode error | See Image inputs. |
402 | insufficient_credits | Balance can't cover the SIGNATURE_SEARCH rate. |
403 | feature_not_enabled | SIGNATURE_SEARCH not enabled. |
422 | no_signature_detected | No signature ink found in the probe. |
429 | rate-limited | Over 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_idstringbodyexternal_user_id.external_user_idstringbodyindividual_id.image_base64stringbodyrequiredlabelstringbodycurl -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() # 201Response Example
{
"sample_id": "7a8b9c0d-1e2f-4a3b-5c6d-7e8f9a0b1c2d",
"individual_id": "0c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f"
}Errors
| Status | Code | When |
|---|---|---|
400 / 415 | image decode error | See Image inputs. |
404 | individual_not_found | Neither individual_id nor external_user_id resolves for this tenant. |
422 | no_signature_detected | No signature ink found in the image. |
429 | rate-limited | Over 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_base64stringbodyrequiredcurl -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
}fieldsobjecttranslated_fieldsobjectdetected_typestringpassport, driving-license).confidencenumberis_document_validbooleanhas_portraitbooleanNote
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
| Status | Code | When |
|---|---|---|
400 / 415 | image decode error | See Image inputs. |
422 | ocr_no_fields | No fields could be extracted (unsupported type or unreadable photo). |
429 | rate-limited | Over the kyc_v3_tools bucket. |
503 | ocr_not_configured | The 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 for | Back-office / ad-hoc lookups | End-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.