---
url: /panel/form-spam-protection.md
---
# Form Spam Protection &#x20;

Every form submission (contact forms and event registrations) passes two independent checks. The honeypot always runs. reCAPTCHA runs on top of it as soon as keys are configured. Both checks are part of the package, a project only has to render the matching fields in its theme.

| Layer | Active | Catches |
|---|---|---|
| Honeypot field | always | bots that fill in every field |
| Honeypot timing gate | always | bots that post faster than a person can type |
| reCAPTCHA | when `GOOGLE_RECAPTCHA_SECRET` is set | bots that render the page and behave like a browser |

A submission that fails any check is rejected with a validation error. Nothing is stored and no mail goes out.

## Honeypot

The form view renders two hidden inputs next to the CSRF token:

```blade
<input type="text" name="fax_number" value="" style="position: absolute; left: -9999px;" autocomplete="nope">
<input type="hidden" name="user_identification" value="{{ Crypt::encrypt(time()) }}">
```

1. **`fax_number`** must arrive empty. People never see it, form filling bots usually do.
2. **`user_identification`** carries the encrypted time the form was rendered. The submission must arrive at least `evolve.honeypot-threshold` seconds later (default 3). A missing or tampered value fails validation.

```php [config/evolve.php]
'honeypot-threshold' => env('EVOLVE_HONEYPOT_THRESHOLD', 3),
```

Projects that still set the older key `core.honeypot-threshold` keep their value, it takes precedence. Before 2.16 (legacy 1.95) the gate read only that older key, which no config defined, so it caught nothing but submissions within the same second.

With the [page cache](/site/page-cache) active the theme refreshes `user_identification` on every page load through `/api/csrf-token`, so cached pages do not carry a stale timestamp.

## reCAPTCHA

### Which key type

Google issues three kinds of reCAPTCHA keys. The key type is chosen when the key is created in the [reCAPTCHA admin console](https://www.google.com/recaptcha/admin) and cannot be changed later.

| Key type | Works with the Evolve theme | Server check |
|---|---|---|
| v3 (score based) | yes, recommended | `success` and score ≥ threshold |
| v2 Invisible | yes | `success` only, no score |
| v2 Checkbox ("I'm not a robot") | no | |

The theme binds reCAPTCHA to the submit button (`class="g-recaptcha"` with `data-callback`). That binding works for v3 and v2 Invisible keys. A v2 Checkbox key needs a visible widget, which the theme does not render. Visitors see no challenge with either supported type.

Each key is bound to a list of domains. Add the customer's domain (and the staging domain, if it should run there too) to the key in the console. A key used on a domain that is not listed fails every submission.

### Configuration

Set the keys in the panel under *Settings > Google* or in the `.env`:

```ini
GOOGLE_RECAPTCHA_SITEKEY=6Lc...
GOOGLE_RECAPTCHA_SECRET=6Lc...
GOOGLE_RECAPTCHA_SCORE=0.5
```

```php [config/services.php]
'google' => [
    'recaptcha' => [
        'secret' => env('GOOGLE_RECAPTCHA_SECRET'),
        'site-key' => env('GOOGLE_RECAPTCHA_SITEKEY'),
        'score' => env('GOOGLE_RECAPTCHA_SCORE', 0.5),
    ],
],
```

1. **Site key** switches the theme to the reCAPTCHA submit and makes the head load `recaptcha/api.js`.
2. **Secret** switches the server check on. From then on every submission needs a valid `g-recaptcha-response` token.
3. **Score** is the minimum a v3 token needs, from 0.0 (bot) to 1.0 (human). 0.5 is Google's suggested default. Lower it if real enquiries bounce, raise it if spam still gets through. Values outside 0 to 1 are clamped, anything non numeric falls back to 0.5.

Set site key and secret together. A secret without a site key rejects every submission, because the theme never sends a token.

If Google cannot be reached, the submission is rejected. Before 2.16 (legacy 1.95) the score was not checked at all: any token Google accepted passed, including those scored as bots.

::: warning Legacy projects with their own `google` block
Legacy 1.x merges `config/services.php` shallowly. A project that defines the whole `google` key in its published `config/services.php` loses the package's `score` key. The check then uses 0.5, but `GOOGLE_RECAPTCHA_SCORE` and the settings field have no effect until the project adds the `score` line shown above.
:::

### Theme integration

The starter theme ships with it. Projects with their own `components/form.blade.php` need the button switch and a callback that submits the form:

```blade
@php
$recaptchaCallback = 'submitForm' . preg_replace('/[^A-Za-z0-9]/', '', $form->name);
@endphp

<form id="form{{ $form->name }}" ...>
    ...
    @if(config('services.google.recaptcha.site-key'))
    <button
        class="g-recaptcha btn btn-primary"
        data-sitekey="{{ config('services.google.recaptcha.site-key') }}"
        data-callback="{{ $recaptchaCallback }}"
        data-action="submit">
        {{ __('evolve::messages.send') }}
    </button>
    @else
    <button type="submit" class="btn btn-primary">{{ __('evolve::messages.send') }}</button>
    @endif
</form>

@if(config('services.google.recaptcha.site-key'))
<script>
    function {{ $recaptchaCallback }}(token) {
        const form = document.getElementById(@js('form' . $form->name));
        let input = form.querySelector('input[name="g-recaptcha-response"]');

        if (! input) {
            input = Object.assign(document.createElement('input'), { type: 'hidden', name: 'g-recaptcha-response' });
            form.appendChild(input);
        }

        input.value = token;
        form.submit();
    }
</script>
@endif
```

Points to watch:

1. **The `id` belongs on the `<form>` only.** Older project themes put `id="form…"` on the wrapping `<div>` as well. `getElementById` then returns the div and `submit()` fails silently.
2. **The callback name must be a valid JavaScript identifier.** Form names may contain hyphens, hence the `preg_replace`.
3. **`api.js` must be on the page.** The Evolve theme loads it in `head.blade.php` when a site key is set. Projects with their own head (most legacy themes) do not. Without it the button submits nothing and nothing happens. Load it once in the form component:

```blade
@once
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
@endonce
```

4. **Show spam failures.** The spam fields have no visible input, so their errors are not rendered next to a field. Without a message the visitor only sees the form again:

```blade
@if($errors->hasAny(['g-recaptcha-response', 'user_identification', 'fax_number']))
<div class="text-sm text-red-500">{{ __('Your request could not be verified. Please try again.') }}</div>
@endif
```

### Privacy

reCAPTCHA sends visitor data to Google. Mentioning it in the site's privacy policy is the customer's responsibility.

## Environment variables

| Variable | Default | Effect |
|---|---|---|
| `EVOLVE_HONEYPOT_THRESHOLD` | `3` | Seconds a form has to be open before a submission is accepted. |
| `GOOGLE_RECAPTCHA_SITEKEY` | unset | Public key, switches the theme to the reCAPTCHA submit. Settings panel. |
| `GOOGLE_RECAPTCHA_SECRET` | unset | Secret key, switches the server check on. Settings panel. |
| `GOOGLE_RECAPTCHA_SCORE` | `0.5` | Minimum v3 score. Settings panel. |

## Troubleshooting

| Symptom | Likely cause |
|---|---|
| Every submission fails after adding keys | Key not valid for this domain, v2 Checkbox key, or only the secret set |
| Real enquiries bounce with the verification message | Score too strict, lower `GOOGLE_RECAPTCHA_SCORE` |
| Nothing happens on submit | Duplicate `id="form…"` in the theme, or `api.js` not loaded (site key missing, or a project head that does not include it) |
| Spam still arrives | No secret set (only the honeypot runs), or a v2 Invisible key (no score to check) |
