Concepts
Workflows
KYC workflows as node graphs of feature modules, how enabled modules are server-enforced, and the GET /v3/workflows/ endpoint.
A KYC workflow is a named, versioned set of verification modules with their
configuration. It is built by an operator in Console → Config → KYC
Workflows, referenced by workflow_id from the API, and run by the hosted
flow when you create a session.
Every workflow starts from one of two mental models: a straight-line checklist
of steps, or a graph of repeatable feature nodes wired together. Both compile
down to the same runtime representation, so nothing about your integration
changes based on which one an operator used to build the workflow — but
understanding the graph model explains a few things you'll see in
decision_v3 and the module catalog that a flat step list can't.
Workflows are node graphs
Under the hood a workflow is a directed graph of feature-instance nodes. Each
node is one module (document verification, liveness, face match, AML screening,
…) with its own config, wired to the next node by typed edges. Because nodes are
instances, the same feature can appear more than once in a single workflow —
each instance carries its own node_id, and results, billing and decisions are
keyed by node_id.
For a simple linear workflow, node_id == feature, so the shape is unchanged
from a flat list of modules — the graph model is backward-compatible. Nothing
about a workflow authored as a flat step list changes behaviour; it's simply
the graph's simplest possible shape.
ID_VERIFICATION ─▶ LIVENESS ─▶ FACE_MATCH ─▶ AML_SCREENINGLinear vs graph workflows
The console builder offers both. Most workflows never need more than the linear model — reach for the graph editor specifically when you need a module to repeat, or when a step's input needs to come from a specific earlier node rather than "whatever ran most recently."
| Linear workflow | Graph workflow | |
|---|---|---|
| Setup speed | Fastest — add steps in order, done | Slower — place nodes, wire edges, configure ports |
| Branching / conditional steps | No — one fixed sequence | Yes — a DAG can route around nodes |
| Repeatable modules (two ID checks, two questionnaires) | No — one instance per feature | Yes — each instance gets its own node_id |
| Face-match wiring | Auto-wired from the first document + first selfie | Explicit — you wire a specific document node's face output into a specific face-match node's face-a port |
| Result keying | node_id == feature (results, billing, decisions all read the same as a flat list) | node_id per instance — decision_v3 arrays hold one entry per node |
| Best for | Most KYC workflows: one ID, one liveness, one face match, optional AML | Multi-document onboarding, split consumer/business identity checks, re-verification steps inside one workflow |
| Console structure field | SIMPLE | ADVANCED |
Tip
Start linear. Only move a workflow to the graph builder when you actually hit a case a flat list can't express — a repeated document capture, or a face-match that needs to pull from a specific one of two uploaded documents rather than "the most recent one."
Note
Switching a workflow from the graph builder back to the simple step list clears the authored graph — the linear list becomes authoritative again. The console builder confirms before doing this because it's a one-way structural change for that workflow version.
Node types
The catalog below is what an operator can drop onto the graph canvas (or add as a step in the linear builder). A subset — document, liveness, face match, NFC, AML, proof of address, contact OTP, questionnaire, video call and confirm details — cover the vast majority of real workflows:
| Node type | Feature key(s) | What it does |
|---|---|---|
| Document | ID_VERIFICATION | Authenticate government IDs across 220+ countries and 14,000+ document types. |
| Liveness | LIVENESS | Confirm a real, present human — defeating photos, masks and deepfakes. |
| Face match | FACE_MATCH | 1:1 biometric comparison of the document portrait against the selfie. |
| NFC | NFC_VERIFICATION | Read the chip in e-passports and biometric IDs for tamper-proof data. |
| AML | AML_SCREENING | Screen against global sanctions, PEP and criminal watchlists (auto-updated). |
| Proof of address | PROOF_OF_ADDRESS | Validate a utility bill or bank statement and extract the address. |
| Contact OTP | EMAIL_VERIFICATION, PHONE_VERIFICATION | Confirm ownership of an email address or phone number with a one-time code (SMS or voice). |
| Questionnaire | QUESTIONNAIRE | Collect structured answers with a form built in the console's questionnaire designer. |
| Video call | VIDEO_CALL | Live agent-attended video verification with server-side recording — the applicant talks to a trained agent who confirms identity on camera. |
| Confirm details | CONFIRM_DETAILS | Show the applicant the identity fields read from their ID and let them correct any OCR mistakes before continuing. |
Repeatable nodes
Only some features can appear more than once in the same workflow:
ID_VERIFICATION, DOCUMENT_AI, PROOF_OF_ADDRESS and QUESTIONNAIRE are
repeatable — useful for "front of ID + a second supporting document," or two
different questionnaires gating different steps. LIVENESS, AML_SCREENING,
IP_ANALYSIS and AGE_ESTIMATION each run once per session regardless of
how many nodes reference them — the engine mirrors the single run's verdict
onto any sibling nodes so the session never gets stranded waiting on a second
run that was never going to happen.
Typed face edges
Two node types produce a face: ID_VERIFICATION, NFC_VERIFICATION,
LIVENESS and AGE_ESTIMATION. FACE_MATCH consumes two faces, on two
distinct typed input ports — face-a (the document-side face) and face-b
(the selfie-side face). In the graph builder you wire a specific document
node's face output into face-a and a specific liveness node's face output
into face-b; in a linear workflow this wiring is synthesized automatically
from the first document node and the first liveness node. A face-match node
wired to a document node that was never uploaded withholds rather than
matching against an unrelated sibling's document — it never silently
cross-matches the wrong pair of nodes.
How a graph walk resolves node_id-keyed results
Every module result, billing debit and decision entry is keyed by the node's
own node_id, not by its feature name. This is what lets ID_VERIFICATION
appear twice in one workflow without one instance's result clobbering the
other's:
Workflow graph (authored once, in the console builder)
────────────────────────────────────────────────────────────────
[doc_front]──face──┐
ID_VERIFICATION │
├──▶ face-a ──┐
[doc_back] │ │
ID_VERIFICATION │ [fm_1] │
│ FACE_MATCH ──▶ [aml_1] AML_SCREENING
[selfie]──face───────┘ face-b │
LIVENESS │
▼
decision_v3
Hosted flow walk (one session)
────────────────────────────────────────────────────────────────
applicant screen 1 → capture doc_front → ModuleResult(session, node_id="doc_front")
applicant screen 2 → capture doc_back → ModuleResult(session, node_id="doc_back")
applicant screen 3 → selfie (liveness) → ModuleResult(session, node_id="selfie")
engine resolves fm_1's wired inputs:
face-a ← doc_front's face output
face-b ← selfie's face output
engine runs fm_1 → ModuleResult(session, node_id="fm_1")
engine runs aml_1 → ModuleResult(session, node_id="aml_1")
decision_v3 (grouped by category, each entry tagged with its node_id)
────────────────────────────────────────────────────────────────
id_verifications: [ {node_id: doc_front, …}, {node_id: doc_back, …} ]
liveness_checks: [ {node_id: selfie, …} ]
face_matches: [ {node_id: fm_1, …} ]
aml_screenings: [ {node_id: aml_1, …} ]The hosted flow walks the graph in topological order, rendering one screen
per node — a repeated feature (two document captures) gets its own screen each
time, and an INFORMATION node renders wherever it's placed in the order.
Anything downstream that reads results — decision_v3, the evidence
checklist, per-node credit billing, incomplete-module detection — keys off
node_id, so two instances of the same feature never overwrite each other's
outcome. See Decisions
for the shape each array entry takes.
Note
A legacy or simple linear workflow still produces exactly this shape — with
node_id == feature for every node — so reading decision_v3 never requires
knowing whether the workflow that ran was authored as a flat list or a graph.
Templates
The console builder offers pre-built starting points so operators aren't wiring a workflow from a blank canvas:
| Template | Features |
|---|---|
| KYC Verification | ID_VERIFICATION, LIVENESS, FACE_MATCH, IP_ANALYSIS |
| KYC + AML | ID_VERIFICATION, LIVENESS, FACE_MATCH, AML_SCREENING, IP_ANALYSIS |
| Age Verification | ID_VERIFICATION, LIVENESS, AGE_ESTIMATION |
| Proof of Address | ID_VERIFICATION, PROOF_OF_ADDRESS |
| Full Onboarding | ID_VERIFICATION, LIVENESS, FACE_MATCH, PROOF_OF_ADDRESS, AML_SCREENING, IP_ANALYSIS |
Every template is a linear starting point — an operator can still convert it to a graph later if they need repeatable nodes or custom face-match wiring.
Listing workflows
curl -H "x-api-key: $TF_API_KEY" "$BASE/v3/workflows/"The response is a bare array — one object per workflow the API key's tenant owns:
[
{
"id": "9c2f1c4e-9a2b-4f2f-8f0f-0f1f3f4f5f6f",
"name": "FreeKYC",
"status": "live",
"features": ["ID_VERIFICATION", "LIVENESS", "FACE_MATCH"]
},
{
"id": "b1d0f8a2-…",
"name": "Enhanced",
"status": "live",
"features": ["ID_VERIFICATION", "LIVENESS", "FACE_MATCH", "AML_SCREENING", "IP_ANALYSIS"]
}
]const res = await fetch(`${BASE}/v3/workflows/`, {
headers: { "x-api-key": process.env.TF_API_KEY },
});
const workflows = await res.json();
const live = workflows.filter((w) => w.status === "live");resp = requests.get(f"{BASE}/v3/workflows/", headers={"x-api-key": os.environ["TF_API_KEY"]})
workflows = resp.json()
live = [w for w in workflows if w["status"] == "live"]idstringworkflow_id on POST /v3/session/.namestringstatusstringlive, draft or archived.featuresarrayNote
GET /v3/workflows/ returns the flattened features list regardless of
whether the workflow was authored linearly or as a graph — it's a summary
view for choosing a workflow_id, not the graph itself. Node-level detail
(node_id, wiring, per-instance config) is a console-builder concern, not
something the partner API exposes directly.
Which workflow runs?
When you omit workflow_id on session create, resolution falls back, in order,
to the tenant's configured default workflow, then the workflow flagged as
default, then the most recent live one. A brand-new tenant with no workflow gets
a default (ID + liveness + face match) provisioned automatically, so a session
always resolves to a real workflow.
Tip
Pin workflow_id explicitly in production. Relying on "the tenant default"
means an operator changing a console setting silently changes what your
integration runs.
Enabled modules are server-enforced
The set of modules that run is decided server-side, from the workflow's enabled features — it is not something the client can add to, skip, or downgrade:
- The hosted flow runs exactly the workflow's enabled modules. A required evidence-driven module (document, liveness, face match, contact, questionnaire) that never produces a terminal result leaves the session incomplete — it will not silently approve. A skipped selfie cannot pass as a verified identity.
- The configured liveness method (passive / colour-flash / 3D-action) is enforced by the server and cannot be swapped for a weaker one from the client.
- Modules that have no real web implementation — currently NFC verification and database validation — are hidden from the builder. If a legacy or imported workflow still enables one, the engine routes it to manual review rather than fabricating a pass.
- The standalone tools and priced modules are gated by tenant
entitlement: a feature your tenant is not entitled to returns
403 feature_not_enabled.
Note
The features array on GET /v3/workflows/ is the authoritative list of what a
workflow will run. Read it to know which modules — and therefore which
credits — a session will exercise before you create
it.
Module catalog
Common feature keys you will see in features and decision_v3:
| Feature key | Module |
|---|---|
ID_VERIFICATION | Document verification |
LIVENESS | Liveness |
FACE_MATCH | Face match |
AML_SCREENING | AML / PEP / sanctions |
NFC_VERIFICATION | NFC chip read |
PROOF_OF_ADDRESS | Proof of address |
DATABASE_VALIDATION | Database validation |
IP_ANALYSIS | Device & IP analysis |
See KYC modules for the behaviour and configuration of each, and Credits for what each one costs.
Common pitfalls
Warning
Assuming features order is execution order for a graph workflow. For a
linear workflow, features is the execution order. For a workflow with
branches, the flattened array still lists every enabled feature, but the
actual per-applicant path depends on the graph's edges — don't hard-code an
assumption that step 2 always follows step 1.
Warning
Treating a node_id as if it were a stable feature name. Once a workflow
repeats a feature, the node_id values are workflow-specific labels, not a
fixed enum. Read them from decision_v3 at runtime rather than hard-coding
"ID_VERIFICATION" as the only possible key for a document result.