Verification
CAPTCHA
Bot protection for the hosted flow — Google reCAPTCHA v2 / v3, hCaptcha or Cloudflare Turnstile, verified server-side, placed at the start of a workflow, anywhere in it, or on any page of a form.
The CAPTCHA module makes the applicant pass a CAPTCHA before they can continue. The widget runs in the applicant's browser, but the decision is always made server-side: the solved token is sent to the provider's verification endpoint by ThirdFactor, and every step placed after an unsolved CAPTCHA is refused by the API — a client that skips the widget can neither progress nor obtain a decision.
Info
CAPTCHA is a platform feature that is off by default. Your ThirdFactor account manager enables it for your organisation; you then set up providers under Settings → Integrations → CAPTCHA.
Providers
| Provider | Key | Options |
|---|---|---|
| Google reCAPTCHA v2 | recaptcha_v2 | Checkbox (normal / compact) or invisible; google.com or recaptcha.net domain |
| Google reCAPTCHA v3 | recaptcha_v3 | Invisible, score-based: minimum score (0–1, default 0.5) and an action checked on verification |
| hCaptcha | hcaptcha | normal / compact / invisible |
| Cloudflare Turnstile | turnstile | flexible / normal / compact, optional action |
Each provider takes its site key (public, used by the widget) and secret key (used only by the server; encrypted at rest and never shown again). The verification endpoint can be overridden (e.g. an hCaptcha Enterprise endpoint), restricted to known provider hosts. One provider is the default.
The widget is shown at the provider's own size, centred in the step (or the form field). On a narrow screen — a small phone or an SDK embed — it is never squeezed or cut off: a full-size widget is scaled down slightly, and below that it switches to the provider's compact variant. Flexible Turnstile fills the width once there are at least 300 px.
Placing it in a workflow
- At the start — switch on CAPTCHA at the start in the workflow's Configurations. Every new session then begins with a CAPTCHA step (node id
captcha_start), right after the welcome screen. - Anywhere — add the CAPTCHA step from the module list, as many times as you need (before a questionnaire, before the selfie…). Each step can pin a provider (or use the default), cap failed attempts, and choose what happens if the provider is unreachable.
| Step setting | Default | Meaning |
|---|---|---|
provider | default | The tenant default, or one of the provider keys above. |
max_attempts | 0 | Failed server verifications before the session is declined. 0 = unlimited — the applicant simply can't continue until one passes. |
on_provider_error | inherit | block keeps the applicant on the step (fail closed — the integration default); review lets them continue and holds the session for manual review. Never a silent pass. |
Inside a form
A CAPTCHA can also go on any page of a questionnaire. In Settings → Forms, add a field of type CAPTCHA, then pick its provider. Default uses the integration's default provider. The field type is only offered while CAPTCHA is enabled for your organisation, and the forms API refuses new CAPTCHA fields otherwise.
- The applicant solves it on that page. Next and Submit stay disabled until ThirdFactor has verified the solve with the provider. Tokens are single-use here too.
- The submission is refused (
400 validation_failed) unless every CAPTCHA field the applicant saw was verified. The field's error iscaptcha_required,captcha_failed,captcha_reusedorcaptcha_unavailable. A field hidden by conditional logic isn't required. The form's require all fields setting never waives it. - The token is never stored as an answer. The questionnaire module keeps the verification result under
data.captcha.<field id>. It also appears as the field'squestions[]row, with adisplaysuch as"Verified — Cloudflare Turnstile". From there it reaches the console, the session API and webhooks:
"captcha": {
"human_check": {
"verified": true,
"provider": "turnstile",
"provider_label": "Cloudflare Turnstile",
"hostname": "acme.v3.thirdfactor.ai",
"challenge_ts": "2026-09-24T10:00:00Z",
"verified_at": "2026-09-24T10:00:02+00:00"
}
}- The integration's outage setting applies here too:
blockkeeps the applicant on the page.reviewlets the form through. The result is thenverified: falsewithfail_open: true, and the questionnaire isin_reviewwith a warning:captcha_provider_unavailable,captcha_misconfiguredif the provider rejects the keys, orcaptcha_not_configuredif no provider is set up.
- A custom front-end on the flow API can submit the provider token as the field's answer. It is verified once, when the form is submitted.
- A CAPTCHA field only protects its own form. To stop a bot before it reaches anything else, use a CAPTCHA step. CAPTCHA fields aren't billed on their own, and forms used in onboarding flows (outside a KYC workflow) don't include them.
Verification rules
A token is accepted only when the provider confirms it and:
- it has never been used before — tokens are single-use across all sessions (a replay is rejected even if the provider is down);
- for reCAPTCHA v3,
score ≥ min_scoreand the action matches; - for Turnstile with an action configured, the action matches;
- it was solved within the configured maximum token age (default 300 s);
- when allowed hostnames are configured, it was solved on one of them.
A rejected token resets the widget so the applicant can solve a fresh challenge.
Result in the session API and webhooks
A CAPTCHA node appears like every other module — in modules[] of GET /v3/session/<id>/ and of the session webhook's session object, keyed by node, in the raw decision snapshot, and in decision_v3.captcha_checks:
{
"node_id": "captcha_start",
"feature": "CAPTCHA",
"label": "CAPTCHA",
"status": "approved",
"score": 90.0,
"warnings": [],
"data": {
"node_id": "captcha_start",
"provider": "recaptcha_v3",
"provider_label": "Google reCAPTCHA v3",
"verified": true,
"placement": "start",
"score": 0.9,
"min_score": 0.5,
"action": "kyc",
"expected_action": "kyc",
"hostname": "acme.v3.thirdfactor.ai",
"challenge_ts": "2026-09-24T10:00:00Z",
"attempts": 1,
"failed_attempts": 0,
"error_codes": [],
"verified_at": "2026-09-24T10:00:02+00:00"
}
}score(module level) is the v3 score as a percentage;data.scoreis the provider's raw 0–1 score. Providers without a score leave both empty.- A session held because the provider was unreachable under the
reviewpolicy hasstatus: "in_review",data.fail_open: trueand the warningcaptcha_provider_unavailable. - A session declined after
max_attemptshasstatus: "declined"with the warningscaptcha_<reason>andmax_retries_exceeded.
Billing
CAPTCHA is free unless your plan sets a CAPTCHA rate; when one is set, each CAPTCHA step that ran is billed once per session.