# Spam protection

Stop bots and spam on public forms with a captcha, content rules, a review queue and daily limits, without writing code.

Every public form is protected in layers. Two are always on, and the rest are
switches in the console. None of them need code: turn a captcha on, and the
hosted page and the embed render it and BuildBase verifies it.

| Layer               | On by default | What it stops                                                  |
| ------------------- | ------------- | -------------------------------------------------------------- |
| Hidden bot trap     | Yes           | Scripts that fill in every field they find                     |
| Duplicate guard     | Yes           | Double clicks, retry loops, the same message pasted repeatedly |
| Rate limits         | Yes           | Floods from one address, and a flood against one organization  |
| **Captcha**         | Per form      | Bots and headless scripts                                      |
| **Content rules**   | Per form      | People, and AI agents in a real browser, who pass the captcha  |
| **Held for review** | Per form      | Lets you see a suspicious submission before anything runs      |
| **Daily cap**       | Per form      | A form taking more than you planned for                        |

> **Note:**
  Choose a captcha provider once in the BuildBase console under **Forms >
  Spam protection**. Then open a form's **Protect** button (the shield in the
  forms table) to turn the captcha and the rules on for that form.


## Choose a captcha provider

The provider applies to every form that has **Require a captcha** turned on.

| Option                             | Cost to you                                                                           | Setup                                     |
| ---------------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------- |
| **Managed by BuildBase** (default) | Free                                                                                  | None                                      |
| **Built-in check**                 | Free                                                                                  | None                                      |
| **Your Cloudflare Turnstile keys** | Free, unlimited checks                                                                | A site key and secret key from Cloudflare |
| **Your hCaptcha keys**             | Free plan available; paid plans add passive mode                                      | A site key and secret key from hCaptcha   |
| **Your Google reCAPTCHA keys**     | Free up to 10,000 checks a month per Google Cloud organization, then billed by Google | A v2 Checkbox site key and secret key     |

**Managed by BuildBase** uses Cloudflare Turnstile run on BuildBase's own
account. Most visitors see a small "Success" check and never a puzzle. On a
self-hosted install without Turnstile keys, Managed uses the built-in check.

**Built-in check** runs a short proof-of-work calculation in the visitor's
browser, typically under a second. It loads no third-party script and sets no
cookie, so it suits sites with strict privacy requirements. It makes every
submission cost a bot CPU time, but it does not tell a person from a headless
browser on its own: pair it with content rules.

**Your own keys** route verification through your own provider account, so its
analytics and settings are yours.

### Add your own keys

1. Create a widget at the provider:
   - **Cloudflare Turnstile**: Cloudflare dashboard > Turnstile > Add widget, Managed mode.
   - **hCaptcha**: dashboard.hcaptcha.com > Sites > New site. The secret key is under your account settings.
   - **Google reCAPTCHA**: google.com/recaptcha/admin > Create, type **v2 "I'm not a robot" Checkbox**. v3 and Invisible keys do not render.
2. Add the hostname the console shows in the dialog to the widget's allowed
   domains. That is where the hosted page and the embed run.
3. Paste the site key and secret key into **Forms > Spam protection** and
   choose **Check keys and save**.

For Turnstile, Cloudflare confirms the secret before it is saved, so a
mistyped key is reported in the dialog. hCaptcha and Google only confirm a
secret against a real visitor's token, so after saving keys for either, open
the hosted form and submit it once.

> **Note:**
  **The secret key is write-only.** It is encrypted at rest and never sent back
  to the console or any browser. The dialog shows that a key is saved and its
  last four characters. To rotate it, paste the new one and save. Switching to
  Managed or Built-in deletes the stored keys.


If a stored key later becomes unreadable, or a provider rejects it, forms fall
back to Managed rather than accepting submissions without a check.

## Turn protection on for a form

The **Protect** dialog holds every per-form setting. The shield icon is filled
when any of them is on.

| Setting                         | Default  | Effect                                                                 |
| ------------------------------- | -------- | ---------------------------------------------------------------------- |
| Require a captcha               | Off      | Submissions must carry a verified token from your provider             |
| Most links allowed              | No limit | Counts links across all answers. Fields of type `link` are not counted |
| Blocked words and phrases       | None     | Whole words or phrases, any capitalization, up to 200 entries          |
| When a submission breaks a rule | Hold     | **Hold for review** or **Reject it**                                   |
| Block throwaway email addresses | Off      | Mailinator, Guerrilla Mail, 10 Minute Mail and similar                 |
| Also block these domains        | None     | Your own list, matched with subdomains                                 |
| Most submissions per day        | No limit | Counted over a rolling 24 hours                                        |

> **Note:**
  Leave **Require a captcha** off for a form that your own backend or your own
  markup posts to, unless that code sends a token. See [Custom
  forms](/forms/custom-forms#captcha-in-your-own-ui).


The rules are never sent to the public form page, so a spammer cannot read
which words or limits to avoid.

## Held for review

With **Hold for review**, a submission that breaks a link or word rule is
saved but fires nothing: no `form.submitted` webhook, no workflow, no email.
The visitor sees the normal success message.

On the form's page in the console, held submissions show a **Held for review**
badge. Hover it for the reasons, for example `3 links (limit 1)` or
`Contains blocked word "casino"`. Then:

- **Approve** releases it. `form.submitted` and the record-created event fire
  as if it had just arrived, so workflows run normally.
- **Delete** discards it. Nothing it would have triggered runs.

Email domain rules never hold: the visitor is asked for a different address,
since a submission from a throwaway address cannot be followed up.

## Test before going live

Each provider publishes test keys that run the real widget and the real
verification, and always pass. Save them in **Forms > Spam protection** to try
the whole flow on a draft form, then replace them with your own keys.

| Provider         | Test site key                              | Test secret key                              |
| ---------------- | ------------------------------------------ | -------------------------------------------- |
| Turnstile        | `1x00000000000000000000AA`                 | `1x0000000000000000000000000000000AA`        |
| hCaptcha         | `10000000-ffff-ffff-ffff-000000000001`     | `0x0000000000000000000000000000000000000000` |
| Google reCAPTCHA | `6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI` | `6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe`   |

Test widgets show a "for testing only" line, so never leave them on a live
form: with test keys, every visitor passes.

After saving your real keys:

1. Open the form's hosted page from the console and complete the captcha.
2. Submit it. The submission should appear on the form's page.
3. If the visitor sees "Could not verify the captcha", the provider refused
   your secret key or hostname. Check both in the provider's dashboard.

| Situation                                | Status | Message                                           |
| ---------------------------------------- | ------ | ------------------------------------------------- |
| Captcha missing or failed                | `400`  | `Captcha verification failed. Please try again.`  |
| Captcha provider unreachable             | `503`  | `Could not verify the captcha. Please try again.` |
| Blocked or throwaway email domain        | `400`  | `Please use a different email address.`           |
| Content rule, set to **Reject it**       | `400`  | `Your submission could not be accepted.`          |
| Content rule, set to **Hold for review** | `200`  | The normal success message                        |
| Any rate limit, daily cap or plan limit  | `429`  | `Too many submissions. Please try again later.`   |
| Hidden bot trap filled, or a duplicate   | `200`  | The normal success message; nothing is stored     |

The hosted page shows each message in a toast and resets the captcha so the
visitor can try again. The quiet answers are deliberate: telling a bot which
check caught it tells it what to change.

## Self-hosting

Nothing is required: Managed uses the built-in check until the platform has
Turnstile keys. To run Managed on your own Cloudflare account, set both:

| Variable                     | Purpose                                                                |
| ---------------------------- | ---------------------------------------------------------------------- |
| `FORMS_TURNSTILE_SITE_KEY`   | Public site key handed to the hosted page                              |
| `FORMS_TURNSTILE_SECRET_KEY` | Verifies tokens on submit. Keep it secret                              |
| `FORMS_CHALLENGE_SECRET`     | Optional. Signs the built-in check; derived from `JWT_PASS` when empty |

## Next Steps

- [Share and embed](/forms/embedding) - put a protected form on any site.
- [Custom forms](/forms/custom-forms) - send a captcha token from your own UI.
- [Workflows](/workflows/overview) - act on `form.submitted`.
