---
title: "Captcha"
description: "reCAPTCHA and Turnstile verification and lazy loading."
requested_language: sk
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/sk/next/payload/forms/captcha
version: unreleased (main)
docs_index: https://docs.systhema.app/sk/next/llms.txt
---
> This page isn't translated yet. Showing English.


Google reCAPTCHA v2 and Cloudflare Turnstile are configured per provider under the form-builder options. Both can be enabled at once (editors then pick which one a given form uses); a form accepts at most one captcha field. Keys are forwarded through the option from your own environment — the package never reads env vars itself.

```ts
forms: {
  fields: {
    recaptcha: {
      siteKey: process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY!,
      secretKey: process.env.RECAPTCHA_SECRET_KEY!,
    },
    turnstile: {
      siteKey: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!,
      secretKey: process.env.TURNSTILE_SECRET_KEY!,
      lazy: false, // opt out of deferred loading (default: true)
    },
  },
}
```

## Options

| Key         | Type      | Default | Description                                                    |
| ----------- | --------- | ------- | -------------------------------------------------------------- |
| `siteKey`   | `string`  | —       | Client-side key for the widget.                                |
| `secretKey` | `string`  | —       | Server-side key used to verify the submitted token.            |
| `enabled`   | `boolean` | `true`  | Set `false` to keep the config but hide the field from admins. |
| `lazy`      | `boolean` | `true`  | Defer the provider script until the form is actually needed.   |

## Deferred loading

The provider scripts are heavy — a Turnstile widget pulls roughly half a megabyte of challenge-platform payload — which is pure waste on a page whose form sits below the fold and is never reached. With `lazy` the script and widget initialise on the first of three signals:

1. the form **approaches the viewport** (an `IntersectionObserver` with a 300px margin — a form that is already on screen when the page hydrates loads immediately, so above-the-fold forms behave exactly as before);
2. the visitor **focuses, clicks or edits any field** of that form;
3. the visitor **submits** — the submit handler opens the gate and waits for the widget before validating, so a keyboard user tabbing straight to the button never sees a spurious "please complete the captcha".

Each form on a page tracks its own trigger (an untouched form never loads a widget because a sibling did), while the provider script itself is fetched once and shared. The widget's skeleton placeholder reserves its final size from first paint, so deferring never shifts the layout. Browsers without `IntersectionObserver` fall back to eager loading automatically.

Set `lazy: false` on a provider to restore the previous behaviour (script fetched during hydration).
