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

ProviderKeyOptions
Google reCAPTCHA v2recaptcha_v2Checkbox (normal / compact) or invisible; google.com or recaptcha.net domain
Google reCAPTCHA v3recaptcha_v3Invisible, score-based: minimum score (0–1, default 0.5) and an action checked on verification
hCaptchahcaptchanormal / compact / invisible
Cloudflare Turnstileturnstileflexible / 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 settingDefaultMeaning
providerdefaultThe tenant default, or one of the provider keys above.
max_attempts0Failed server verifications before the session is declined. 0 = unlimited — the applicant simply can't continue until one passes.
on_provider_errorinheritblock 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 is captcha_required, captcha_failed, captcha_reused or captcha_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's questions[] row, with a display such 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:
    • block keeps the applicant on the page.
    • review lets the form through. The result is then verified: false with fail_open: true, and the questionnaire is in_review with a warning: captcha_provider_unavailable, captcha_misconfigured if the provider rejects the keys, or captcha_not_configured if 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_score and 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.score is the provider's raw 0–1 score. Providers without a score leave both empty.
  • A session held because the provider was unreachable under the review policy has status: "in_review", data.fail_open: true and the warning captcha_provider_unavailable.
  • A session declined after max_attempts has status: "declined" with the warnings captcha_<reason> and max_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.