# Spam protection
Form Plume stops form spam with a honeypot, time-trap tokens, rate limits, email hygiene, blocked keywords, and optional CAPTCHA.
Most forms don't need to make a visitor prove they're human. Form Plume
layers a few lightweight, invisible checks that catch almost all bot
traffic on their own.
Add a [honeypot](/docs/spam-protection/honeypot) and a [time-trap token](/docs/spam-protection/time-trap) and you're covered for the vast
majority of forms. Everything else here is either automatic or for the
rare form that gets targeted by an actual person.
If you want an extra layer, or your form already uses one, [add CAPTCHA](#captcha-if-you-need-it). Form Plume supports Cloudflare Turnstile, hCaptcha, and Google reCAPTCHA.
## Honeypot
Add a hidden input named `_gotcha` to your form:
```html
```
Real visitors never see or fill this in, it's off-screen and skipped in tab
order. Bots that blindly fill every input on the page usually fill it in
anyway.
If `_gotcha` arrives non-empty, Form Plume responds exactly as if the
submission succeeded, so the bot gets no signal that anything went wrong.
The submission itself is dropped before it reaches your inbox or triggers a
webhook.
## Time-trap token
Bots that skip the honeypot trick still tend to submit **fast**, often under
a second after the page loads. A time-trap token catches that.
Fetch a token right before you render the form:
```js
const res = await fetch("https://api.formplume.com/f/{public_slug}/time-trap");
const { field, token } = await res.json();
// field === "_fp_ts"
```
Then put it in a hidden field named by the response:
```html
```
The token is signed (HMAC-SHA256) and encodes when it was issued, so the
server can check timing without keeping any session state. A submission
comes back `validation_failed` if the token is:
- Used **less than 3 seconds** after it was issued, too fast for a human to
have actually read the form
- **Older than an hour** (see `expires_in_seconds` in the response)
- Reused, each token is good for one submission only
> Rotating the signing secret on your end doesn't break anything mid-flight.
> Tokens issued before the rotation keep working for a grace period.
## Rate limiting
[Rate limiting](/docs/spam-protection/rate-limits) is on by default, no setup needed:
| Window | Limit |
| --- | --- |
| Strict | 1 submission every 30 seconds |
| Burst | 20 submissions every 10 minutes |
Limits are keyed per form, per submitter (IP address plus user agent), so
one visitor hammering refresh can't take a form down for everyone else.
Going over either limit returns `429 Too Many Requests` with a
`Retry-After` header telling you how long to wait. Both windows are
configurable per form if the defaults don't fit your traffic.
## Email hygiene
Configure this per form under **Settings → Spam**. [Email hygiene](/docs/spam-protection/email-hygiene) checks that
the submitter's email field looks real: valid syntax, a domain you haven't
blocked, and optionally a working mail server (MX record) and no disposable
addresses.
Failed checks can either **flag** the submission (it still arrives, marked
for the [spam scorer](/docs/spam-protection/scoring)) or **block** it outright with `email_rejected`. Flagging
is available on every plan; blocking, MX checks, and disposable-address
blocking are Pro features.
## Blocked keywords
[Blocked keywords](/docs/spam-protection/blocklists) let you add words or phrases under **Settings → Spam → Blocked keywords**, one per
line. Any submission containing one of them anywhere in its content goes
straight to the [spam folder](/docs/spam-protection/spam-inbox), no matter what the spam score says. Matching is
case-insensitive and works on multi-word phrases like `backlink packages`.
Quarantined submissions land in the spam folder rather than being deleted,
so a false positive is always recoverable, and marking it "not spam" also
trains your form's filter.
## CAPTCHA, if you need it
For the small number of forms that attract targeted, human-operated spam
rather than simple bots, you can add [CAPTCHA verification](/docs/spam-protection/captcha) as an extra layer.
Create keys with the provider of your choice, then your page renders the
widget with the **site key** and Form Plume verifies the submitted token
with the encrypted **secret key** before accepting the submission.
> The secret key is private. Paste it into Form Plume settings, but never put
> it in your HTML, JavaScript bundle, or public repository.
| Provider | Best for | Token field | Plan |
| --- | --- | --- | --- |
| hCaptcha | Free, challenge-based CAPTCHA with broad support | `h-captcha-response` | Free |
| Cloudflare Turnstile | Privacy-friendly, low-friction checks | `cf-turnstile-response` | Pro |
| Google reCAPTCHA v2 | Checkbox or invisible challenge flows | `g-recaptcha-response` | Pro |
| Google reCAPTCHA v3 | Invisible, score-based risk checks | `g-recaptcha-response` | Pro |
Each provider has its own setup guide with the full widget snippet, token
field, and verification details:
- [Cloudflare Turnstile](/docs/spam-protection/captcha/turnstile), privacy-friendly and often puzzle-free (Pro)
- [hCaptcha](/docs/spam-protection/captcha/hcaptcha), the free-plan option with broad support
- [Google reCAPTCHA](/docs/spam-protection/captcha/recaptcha), covering both the v2 checkbox flow and invisible v3 scoring (Pro)
## What to actually turn on
Honeypot and the time-trap token together stop the overwhelming majority of
automated spam for the cost of two extra fields and zero user-facing
friction. Rate limiting and email hygiene run regardless of what you
choose.
Reach for CAPTCHA only if you're still seeing spam after that, it's usually
a sign of a human spammer, not a script. Blocked keywords are a quick lever
too if the spam always mentions the same things.