Spam Protection & Captcha
Attri forms have two spam defenses: a honeypot field you add yourself, and Cloudflare Turnstile captcha you toggle per form. They solve different problems — the honeypot silently swallows dumb bots, Turnstile stops the sophisticated ones — and they stack fine.
Honeypot: the _gotcha field
Section titled “Honeypot: the _gotcha field”Add a hidden field named _gotcha and leave it empty:
<input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off" />Humans never see it. Bots that blindly fill every field reveal themselves, and Attri silently discards the submission — the bot still gets a success response, so it doesn’t learn to adapt. Nothing to configure in the app; the field name alone activates it.
Turnstile captcha
Section titled “Turnstile captcha”Enable captcha per form in the form’s settings in Attri. When it’s on, the server requires a valid Turnstile token with every submission — no token, no capture, regardless of how the form was submitted.
How the widget gets on your page
Section titled “How the widget gets on your page”You don’t add any captcha markup yourself. When the tracking snippet loads on a page, it:
- Finds every
<form>whoseactionpoints at your Attri form endpoint - Checks whether that form has captcha enabled (from your workspace config)
- Injects the Turnstile widget (an iframe served from
captcha.attri.io, 300×65px) just before the submit button, plus a hiddencf-turnstile-responseinput - Fills the hidden input with the token when the visitor passes the check
The iframe comes from Attri’s domain rather than yours, which is what makes the widget work on any customer site without per-domain Turnstile configuration.
The gotchas
Section titled “The gotchas”The snippet is mandatory for captcha. This is the one that catches people. The Turnstile widget is injected by the tracking script — there is no other mechanism. If the snippet isn’t on the page, no widget appears, visitors see nothing wrong, and every submission is rejected with 422 Captcha verification required. If you can’t install the snippet on a page, don’t enable captcha for forms on that page.
The form must exist when the script scans. The script looks for Attri forms at load time and once more after its config fetch. A form rendered later — by a client-side router, a modal, a lazily loaded component — never gets the widget (and won’t be intercepted at all). Render forms in the initial HTML, or keep them mounted from page load.
The action attribute is how forms are recognized. The scanner matches forms whose action contains form.attri.io/{key} or ends in /f/{key}. If you submit via your own JavaScript instead of a real <form action="...">, the scanner can’t see it — you’d need to obtain and send the token yourself, which mostly defeats the point of the managed widget. Use a real form element.
Testing usually happens with the snippet present, production sometimes doesn’t. The classic failure: the form is built and tested on a page with the snippet, captcha gets enabled later, and the form is also embedded somewhere the snippet was never installed. That embed silently breaks. If a form lives on multiple pages, every one of them needs the snippet.
What the server enforces
Section titled “What the server enforces”Turnstile verification happens server-side on every submission to a captcha-enabled form. Submissions without a token get 422 with {"ok": false, "error": "Captcha verification required"}; invalid or expired tokens get 422 with "Captcha verification failed". The snippet surfaces these as inline messages; a native form post just fails, which is another reason captcha requires the snippet.
Also active on every form
Section titled “Also active on every form”These run whether or not you configure anything:
- Rate limiting — 10 submissions per minute per IP per form, returning
429with aRetry-Afterheader. - Email validation — any field named
emailis format-checked server-side. - Bot filtering on analytics — obvious bot traffic is excluded from your form analytics regardless of capture.