Skip to content

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.

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.

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.

You don’t add any captcha markup yourself. When the tracking snippet loads on a page, it:

  1. Finds every <form> whose action points at your Attri form endpoint
  2. Checks whether that form has captcha enabled (from your workspace config)
  3. Injects the Turnstile widget (an iframe served from captcha.attri.io, 300×65px) just before the submit button, plus a hidden cf-turnstile-response input
  4. 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 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.

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.

These run whether or not you configure anything:

  • Rate limiting — 10 submissions per minute per IP per form, returning 429 with a Retry-After header.
  • Email validation — any field named email is format-checked server-side.
  • Bot filtering on analytics — obvious bot traffic is excluded from your form analytics regardless of capture.