Flows
The named composition of checks that a verification runs
A flow is a saved, ordered list of checks with a name and an id. It is the unit you version, reuse
and reason about. A full KYC onboarding is one flow. An age gate is another. A liveness-only
anti-cheat check is a third. Flow ids start with flw_.
Separating the flow from the verification means you can change what a check-out journey asks for without shipping backend code: update the flow in your dashboard, and the next verification created against that id runs the new composition.
Steps
Each step names a check type and says whether it is required.
{
"name": "ID and liveness",
"steps": [
{ "type": "document", "required": true },
{ "type": "liveness", "required": true },
{ "type": "face_match", "required": true }
]
}required is what drives the roll-up. A required check that rejects rejects the whole verification.
An optional check that rejects does not, though it still appears in the result with its own verdict
and reason codes so you can act on it yourself. See Verdicts.
Sensitivity
sensitivity is a single number from 0 to 1 that decides how readily a flow hands a borderline
result to a human instead of settling it automatically. Lower means more verifications are decided
without you; higher means more of the uncertain middle is held back for manual review.
{ "name": "ID and liveness", "sensitivity": 0.35, "steps": [] }It moves that uncertain middle and nothing else. A verification that fails outright fails at every setting, so no value you choose here can decline an applicant that a different value would have approved. What changes is the volume of manual review you take on, and the number of weak passes you accept in exchange for a smaller queue.
Every flow starts at 0.5, which behaves the way the platform did before the setting existed. If
your review queue is larger or smaller than you want, this is the control to move first, and moving
it a little at a time beats moving it a lot.
A single check can be held to a different standard than the rest of its flow by giving that step its
own sensitivity. Leave it off and the step follows the flow, which is what you want almost always.
{
"sensitivity": 0.25,
"steps": [
{ "type": "age_estimation", "required": true, "sensitivity": 0.7 },
{ "type": "document", "required": false }
]
}A step can also carry a config object of its own settings. The one this platform validates today
is the age policy on an age_estimation step, which is covered in
Checks. Anything else you put in config is stored and ignored,
so a key this version has never heard of will not fail your request.
The older per-step threshold field is deprecated. It is still accepted so existing integrations
keep working, and it is no longer read by anything. It is not the same quantity as sensitivity, so
do not copy a stored threshold across; set the sensitivity you actually want instead.
Using a flow
Reference the id when you create a verification:
{ "flow": "flw_id_and_liveness", "applicant": { "reference_id": "your-user-123" } }The flow's steps and its sensitivity are snapshotted onto the verification at creation time. Editing the flow afterwards does not retroactively change verifications that are already running, so a user midway through a session never sees the ground shift under them.
Inline checks
For a one-off composition that does not deserve a saved flow, send checks instead of flow:
{
"checks": [
{ "type": "liveness", "required": true },
{ "type": "age_estimation", "required": true }
],
"applicant": { "reference_id": "your-user-123" }
}Send one or the other, never both.
Prefer saved flows in production
Inline checks are convenient for experiments, but a saved flow gives you one id to audit, one place to change, and a stable label on every verification that ran it.
Your default flow
Mark one flow as your default in your dashboard, and a create call that names neither flow nor
checks runs it. The flow object reports this back as is_default. That is the shortest possible
integration: your backend posts an empty body, and what runs is whatever you last configured, with no
deploy.
Resolution runs in a fixed order. An inline checks array wins over everything. Failing that, a
flow id you named wins. Failing that, your default flow runs. Only if you have no default at all
does a built-in preset run, and that preset is document, liveness and face match.
The flow field on the verification tells you which of those actually happened, so a call that
named nothing still comes back saying which composition it resolved to.
You have at most one default. Marking a flow as the default moves the marker off whichever flow held it before, in the same write, so the two can never disagree.
Archiving
A flow you no longer want to run is archived rather than deleted. Archiving is the off switch, and it is reversible.
An archived flow stops accepting new verifications, so naming it on a create call returns
FLOW_ARCHIVED, and it can no longer be your default. Everything it already ran is untouched: the
flow stays readable, stays in your list, and still explains why past applicants were approved or
rejected. That record is the reason archiving exists, and it is why you should reach for archive and
not delete on anything that has seen traffic.
Deleting is possible only for a flow that has never been used. One with verifications behind it cannot be removed, because doing so would take the explanation for those decisions with it.
Branding
A flow carries its own presentation, so a co-branded partner journey can look different from your own without a second account:
{
"brand_name": "Nordvik Bank",
"logo_url": "https://cdn.example.com/nordvik.svg",
"accent_color": "#2f80ff",
"support_url": "https://help.example.com",
"languages": ["en", "nb", "pt-BR"]
}Anything you leave unset falls back to your account-level branding, so setting nothing here is a
perfectly good answer. languages is the set of languages the flow offers the applicant; an empty
array means offer every language we support, which is the default and is not the same as offering
none.
default_email_locale is separate, and is the language of the outcome email your applicant
receives. An applicant may well complete the flow in one language while you write to them in
another.
Acceptance policy
Three optional policy objects decide the borderline cases, per flow rather than per account, because an age gate for one market and full onboarding for another rarely want the same rules.
| Policy | What it decides |
|---|---|
country_policy | Which countries you accept, block, or always send to manual review, and whether to ask before capture starts |
network_policy | What to do when an applicant arrives through a proxy or changes address mid-session |
expiry_policy | What happens to an expired document, per issuing country and document type |
Each is null by default, which means no policy of that kind. expiry_policy is the one worth a
second look: its rules let you accept an expired document from a country where reissue is genuinely
unavailable while still rejecting expired documents everywhere else.
{
"expiry_policy": {
"default": "reject",
"rules": [{ "country": "NG", "document_type": "passport", "disposition": "allow" }]
}
}The most specific rule wins, and * matches any country or any document type.
Walking your own flow
Before you point real traffic at a flow, open it yourself. Your dashboard has a Test button on every flow that does this, and there is an endpoint for the same thing when you want it in a script:
curl -X POST https://api.trustwix.com/v1/flows/flw_id_and_liveness/preview \
-H "Authorization: Bearer $TRUSTWIX_SECRET_KEY"You get back a url that is ready to open in a browser. The session behind it is always sandbox,
whichever key you called with, and it expires in 30 minutes. Because it is always sandbox, previewing
a live flow costs you nothing and cannot pollute your live records.
Managing flows
Flows are built in your dashboard, under Flows, not over the API. That is where the editor, the step validation and the whole lifecycle live, and it is the only surface that has all of it. Authoring a flow is configuration work you do once and change rarely, so it does not belong in your deploy pipeline.
What you do there:
| Action | What it does |
|---|---|
| Create | Name the flow and compose its ordered, configured steps |
| Save | Edit the steps, the sensitivity, the branding and the acceptance policies |
| Set as default | Make this the flow a create call resolves when it names neither flow nor checks |
| Test | Mint a sandbox session and walk your own flow in a browser before real applicants do |
| Copy to live | Promote a flow you built and tested in sandbox into your live environment |
| Archive and restore | The reversible off switch, and the way back |
| Delete | Only offered for a flow no verification has ever referenced |
Two of those have no API equivalent at all. Copy to live is how a flow crosses environments: a flow belongs to one environment and is promoted by an explicit copy, never edited into the other, so you can change a sandbox flow freely without touching what your live traffic runs. Restore undoes an archive.
Your integration reads flows rather than writes them. GET /v1/flows lists them and
GET /v1/flows/{id} retrieves one, which is what you want if your backend resolves a flow id at
runtime instead of hard-coding it. Both need the settings:read scope, which sandbox keys carry by
default and live keys do not, so check Permissions before you build a
production path on them.
Engine tiers
Which engine tier runs your checks is an account-level entitlement, not a per-flow setting. Standard
is the default. Pro and Pro Max raise document coverage and accuracy on the same API surface, so
moving between tiers never changes your integration. The tier that ran a check is reported back on
the check as provider_tier.