# How to Add reCAPTCHA to a Contact Form (v2 and v3)

Add Google reCAPTCHA v2 or v3 to an HTML or React contact form, verify every token without running your own server, and choose the version that suits your visitors.

Published 2026-10-30.

**Short answer:** create a reCAPTCHA key in the Google Cloud console, load `https://www.google.com/recaptcha/api.js` and put `<div class="g-recaptcha" data-sitekey="...">` inside your form for the v2 checkbox. Then make sure something holding your secret key checks each token with Google's siteverify API, because the checkbox alone blocks nothing. On a site without a server, post the form to a form backend that does the check. Nisuform verifies v2 and v3 tokens on every plan.

```html title="contact.html"
<script src="https://www.google.com/recaptcha/api.js" async defer></script>

<form action="https://api.nisuform.com/s/YOUR_FORM_KEY" method="POST">
  <label for="email">Email</label>
  <input id="email" type="email" name="email" required>

  <label for="message">Message</label>
  <textarea id="message" name="message" required></textarea>

  <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="display:none">
  <div class="g-recaptcha" data-sitekey="YOUR_RECAPTCHA_SITE_KEY"></div>
  <button type="submit">Send</button>
</form>
```

## v2 checkbox or v3 score

reCAPTCHA comes in two versions that behave very differently on a form:

|                                    | v2 checkbox                                              | v3 score                                   |
| ---------------------------------- | -------------------------------------------------------- | ------------------------------------------ |
| What visitors see                  | "I'm not a robot", sometimes followed by an image puzzle | Nothing, apart from a badge in the corner  |
| What Google returns                | Pass or fail                                             | A score from 0.0 to 1.0                    |
| A real person who looks suspicious | Solves a puzzle and sends the message                    | Gets blocked, unless you build a fallback  |
| Code                               | A `<div>` and a script tag                               | JavaScript that asks for a token on submit |

For a contact form, the checkbox is the safer choice. With v3, a real person who gets a low score is rejected and may not try again. v3 suits forms where you'd rather let a few bots through than show anyone a checkbox, and where you watch the rejected count.

## What changed with reCAPTCHA keys

Google has moved reCAPTCHA into Google Cloud. Keys now live in Google Cloud projects, and keys from the old admin console that nobody moved by hand are moved automatically. Their site keys, secret keys and the siteverify endpoint keep working as before.

Two things are different for a new form:

* **Usage is metered.** The free tier is 10,000 checks a month, counted across your whole Google Cloud organization. Above that, Google asks you to turn on billing for the project.
* **The secret key is hidden.** The console shows the site key as the key's **Key ID**. The secret is behind **Use legacy key** on the key's **Integration** tab, or **Reveal** when you edit the key.

## The checkbox doesn't stop bots on its own

When someone ticks the box, the widget adds a token to the form in a hidden field named `g-recaptcha-response`. A bot can skip your page and post to your form's endpoint directly, with no token at all. The token only matters if the server that receives the form sends it to `https://www.google.com/recaptcha/api/siteverify` with your secret key and rejects the submission when Google says no.

Each token expires after two minutes and can be verified once. For v3, the response also carries the score, and picking the cut-off is up to you. Google suggests starting at 0.5.

Your secret key can't go in HTML or browser JavaScript, so a static site has no place to make that call. Either run a small server function that verifies the token and then sends the email, or post the form to a form backend that holds the secret and checks every token. Nisuform does that on every plan, and rejects v3 tokens that score below 0.5.

## Add the reCAPTCHA checkbox to a form

### Create a checkbox key

In the [Google Cloud console](https://console.cloud.google.com/), open **Fraud Defense**, then the **Keys** tab, and create a website key. Choose the checkbox type, add every domain the form runs on, and save. Copy the **Key ID**, which is your site key.

### Copy the secret key

On the key's **Integration** tab, click **Use legacy key**, or **Integrate with a third-party service or plugin**, and copy the secret key.

### Add both keys to your form

[Create a form](https://app.nisuform.com) in Nisuform. In its **Settings**, **Spam protection**, turn on **Require a CAPTCHA**, choose **Google reCAPTCHA**, paste the site key and the secret key, and save.

### Put the checkbox inside the form

Use the code at the top, with your endpoint from the **Setup** tab in `action` and your site key in `data-sitekey`. The `<div>` has to sit inside the `<form>` element so its hidden field is sent. If you use the styled embed from the **Setup** tab, copy it again and the checkbox is already in it.

### Send a test

Tick the box, wait two seconds and send. The message should reach your Nisuform inbox and your email.



## A reCAPTCHA v3 contact form

A v3 key has no widget. Load the script with your site key in the URL, ask for a token when the visitor submits, and send it as `g-recaptcha-response`. This version posts with `fetch` and shows the result on the page:

```html title="contact.html"
<script src="https://www.google.com/recaptcha/api.js?render=YOUR_RECAPTCHA_SITE_KEY"></script>

<form id="contact">
  <label for="email">Email</label>
  <input id="email" type="email" name="email" required>
  <label for="message">Message</label>
  <textarea id="message" name="message" required></textarea>
  <button type="submit">Send</button>
  <p id="status" role="status"></p>
</form>

<script>
  const SITE_KEY = 'YOUR_RECAPTCHA_SITE_KEY'
  const form = document.getElementById('contact')
  const status = document.getElementById('status')

  form.addEventListener('submit', (event) => {
    event.preventDefault()
    grecaptcha.ready(async () => {
      const data = new FormData(form)
      data.set('g-recaptcha-response', await grecaptcha.execute(SITE_KEY, { action: 'submit' }))
      const response = await fetch('https://api.nisuform.com/s/YOUR_FORM_KEY', {
        method: 'POST',
        headers: { Accept: 'application/json' },
        body: data,
      })
      const result = await response.json()
      status.textContent = result.ok ? 'Thanks! We got your message.' : result.message
      if (result.ok) form.reset()
    })
  })
</script>
```

Ask for the token on submit, not when the page loads. Google's docs say so, and a token fetched on page load expires two minutes later. When a visitor scores below 0.5, the response is `captcha_failed` and the message above shows Nisuform's error text. If that happens to people you know are real, switch the form to a checkbox key.

## The checkbox in React

Google's script renders every `.g-recaptcha` element once, when it loads. A React form that mounts later needs to render its checkbox itself:

```jsx
useEffect(() => {
  const el = widget.current
  if (window.grecaptcha?.render) {
    if (!el.childElementCount) window.grecaptcha.render(el, { sitekey: el.dataset.sitekey })
  } else if (!document.querySelector('script[src="https://www.google.com/recaptcha/api.js"]')) {
    const script = document.createElement('script')
    script.src = 'https://www.google.com/recaptcha/api.js'
    script.async = true
    document.head.append(script)
  }
}, [])
```

Put `<div ref={widget} className="g-recaptcha" data-sitekey="..." />` inside the form, post `new FormData(event.currentTarget)` so the token goes along, and call `grecaptcha.reset()` after a failed submit, because the old token is spent. With reCAPTCHA on, the **Setup** tab generates the full component for React, Next.js, Vue, Svelte and Astro, with your site key filled in.

## Hiding the v3 badge

v3 puts a reCAPTCHA badge in the bottom corner of every page that loads the script. Google allows hiding it if the branding stays visible in the form itself. Hide the badge with CSS:

```css
.grecaptcha-badge { visibility: hidden; }
```

Then add this line near the submit button, with links to Google's [Privacy Policy](https://policies.google.com/privacy) and [Terms of Service](https://policies.google.com/terms): "This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply."

## Testing reCAPTCHA

For the v2 checkbox, Google publishes a test key pair that always passes and shows a warning in the widget:

| Key        | Value                                      |
| ---------- | ------------------------------------------ |
| Site key   | `6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI` |
| Secret key | `6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe` |

Use them on a separate test form in Nisuform, never the live one. For v3 there are no test keys. Google recommends a separate key for your test environment and warns that scores there may be inaccurate, because v3 learns from real traffic.

## When every submission fails

Nisuform answers `403` with `captcha_failed` when the token is missing, expired, already used, rejected by Google or, for v3, scored below 0.5. Nothing is stored. If it happens every time:

* **"Invalid key type" in the widget.** The key type doesn't match the code. A checkbox key needs the `g-recaptcha` div, and a score key needs `grecaptcha.execute`.
* **"Invalid domain for site key".** Add the page's domain to the key's domain list in Google Cloud.
* **Wrong keys.** The site key on the page and the keys in the form's settings have to come from the same reCAPTCHA key.
* **Missing token.** The checkbox is outside the `<form>`, or your `fetch` builds its own body and leaves `g-recaptcha-response` out.
* **Expired token.** The visitor ticked the box more than two minutes before sending. The widget asks them to tick it again.

If you don't need to keep reCAPTCHA, [Cloudflare Turnstile](/docs/protection/turnstile) works the same way in Nisuform, without a Google Cloud project or a monthly cap. A low-traffic contact form may need only the default checks every Nisuform form runs.

## Frequently asked questions

### Is reCAPTCHA still free?

Up to 10,000 checks a month. The free tier is counted across your whole Google Cloud organization, all sites and keys together, and Google asks you to turn on billing for the project above that.

### Should a contact form use reCAPTCHA v2 or v3?

The v2 checkbox, for most contact forms. A visitor who looks suspicious gets an image puzzle and can still send the message. v3 shows nothing, but it only returns a score, so a real person with a low score is blocked with no way through unless you build a fallback.

### Where do I find my reCAPTCHA secret key?

In the Google Cloud console, open Fraud Defense, then the Keys tab, and open your key. On its Integration tab, click Use legacy key, or Integrate with a third-party service or plugin, and copy the secret. You can also click Edit key on the key details page and reveal the secret key there. The Key ID is your site key.

### Can I use reCAPTCHA on a static site?

The widget works anywhere, but every token has to be verified with your secret key through Google's siteverify API, and the secret can't go in browser code. On a static site, post the form to a form backend that does the check. Nisuform verifies reCAPTCHA v2 and v3 tokens on every plan.

### What score threshold should I use for reCAPTCHA v3?

Google suggests starting at 0.5, on a scale from 0.0 (very likely a bot) to 1.0 (very likely a person). Nisuform rejects v3 tokens below 0.5.

### Can I hide the reCAPTCHA badge?

Yes, if the reCAPTCHA branding stays visible in the form. Hide the badge with .grecaptcha-badge { visibility: hidden; } and add "This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply." near the submit button, with links to both.

### Will my Contact Form 7 reCAPTCHA keys work?

Contact Form 7 uses reCAPTCHA v3, so its keys are score keys. They work with Nisuform through the v3 code in this guide. The checkbox and the styled embed need a separate checkbox key.
