Troubleshooting: TURNSTILE_REQUIRED (422) on public form endpoints
Two public, unauthenticated endpoints carry the Cloudflare Turnstile
bot-protection gate — the marketing-lead capture used by Devotel-property
contact forms, and the self-service data-subject-request portal
(POST /compliance/public/dsar/begin). When the gate is armed and the
submitted token fails, the request is rejected at the bot-protection
step — before rate limits, before payload validation completes — with:
The 422 is fail-closed by design: a request without a challenge the
platform can verify reads as a bot submission, and the gate rejects it
rather than let it in unchallenged. An absent, expired, or forged token
never succeeds while the gate is armed.
error.details.error_codes carries the raw codes Cloudflare returned
from siteverify when it available — the most common entries are
missing-input-response (no token arrived at all), invalid-input-response
(token already used, expired, malformed, or from the wrong site), and
timeout-or-duplicate (the token was presented before, or too late).
The field is omitted when the verification itself was unreachable, in
which case the rejection is an availability fault on the platform side,
not a visitor error.
Cause table
The gate is armed server-side; the fix is always on the form side:
- Render the widget. Add the Cloudflare Turnstile script and render
the widget with the public site key in your form markup. The widget
solves the challenge and hands you a token via its callback.
- Attach the token to the submission. For the marketing-lead
capture endpoint this is the
turnstile_token body field; on a raw
HTML form it is the cf-turnstile-response field. Whichever name,
it is the same token.
- Reset after every attempt. Reset the widget after each
submission — success included — so every attempt carries a fresh
token. A reused token is the single most common cause of this 422.
Retry posture
Retry only after solving a fresh token — never blind-retry. A 422
TURNSTILE_REQUIRED is a deterministic gate: replaying the identical
body rejects identically, and the rate limit behind the gate makes a
tight retry loop expensive. Re-render or reset the widget, get a new
token, and submit once. If a fresh token still rejects with a site-key
mismatch or with no error_codes in the response, work the cause table
above before trying again.
What NOT to do
- Do not retry with the same token. Single-use means single-use;
a replayed token is the most rejected class and always fails.
- Do not cache tokens client-side or server-side “to save solves.”
A cached token is already dead by the time you use it.
- Do not strip the field to see if the gate is off. The gate is
fail-closed — an absent token rejects identically to a bad one. The
absence test tells you nothing.
- Do not open the form wider to route around the gate. A public
endpoint with no abuse defense fills your lead pipeline with
synthetic submissions; the gate exists because it is the cheaper
failure of the two.
Escalation bundle
Work the cause table first — the four causes above account for the
rejection almost always. If a fresh, in-window token with the correct
site-key pairing still rejects, open a ticket with:
- The request id from the response envelope’s
meta.request_id.
- The public site key the widget was rendered with (the public key,
never the secret).
- The full error envelope, including
error.details.error_codes.
- The endpoint you hit (marketing-lead capture, or the DSA-request
portal’s
begin) and one confirmation that a fresh token was used.
See also