Skip to content

reCAPTCHA

Protect your forms from spam and abuse using Google reCAPTCHA v3. Unlike v2, reCAPTCHA v3 runs in the background without user interaction, providing a seamless experience while scoring each submission for suspicious activity.


  1. User loads your form → reCAPTCHA script generates a token in the background
  2. User submits form → Token is sent along with form data via g-recaptcha-response
  3. Forminit verifies token → Google validates the token and returns a risk score (0.0 - 1.0)
  4. Submission accepted or rejected → Scores below threshold are blocked

  • A Google account
  • Access to your website’s HTML/JavaScript

  1. Go to the Google reCAPTCHA Admin Console
  2. Click Create (+ icon)
  3. Fill in the form:
    • Label: Your site name (e.g., “My Website Contact Form”)
    • reCAPTCHA type: Select Score based (v3)
    • Domains: Add your domain(s) (e.g., example.com, localhost for testing)
  4. Accept the Terms of Service and click Submit
  5. Copy both keys:
    • Site Key (public) → Used in your frontend code
    • Secret Key (private) → Added to Forminit

Important: Keep your Secret Key confidential. Never expose it in client-side code.


  1. Go to your Forminit Dashboard
  2. Select your form
  3. Navigate to Form Settings → CAPTCHA
  4. Select reCAPTCHA v3 as the provider
  5. Paste your Secret Key in the designated field
  6. Click Save

Once configured, Forminit will automatically verify the g-recaptcha-response token with Google on every submission.


Add the reCAPTCHA v3 script to your HTML, replacing YOUR_SITE_KEY with your actual site key:

<script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>

Place this in the <head> or before your form submission logic.


The reCAPTCHA token must be submitted with the field name g-recaptcha-response (no fi- prefix).

FormatHow to Include
FormDataformData.append('g-recaptcha-response', token)
JSONAdd as a text block: { type: 'text', name: 'g-recaptcha-response', value: token }

<!DOCTYPE html>
<html>
<head>
  <title>Contact Form</title>
  <script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>
</head>
<body>
  <form id="contact-form">
    <input type="text" name="fi-sender-firstName" placeholder="First name" required />
    <input type="text" name="fi-sender-lastName" placeholder="Last name" required />
    <input type="email" name="fi-sender-email" placeholder="Email" required />
    <textarea name="fi-text-message" placeholder="Message" required></textarea>
    
    <button type="submit">Send</button>
  </form>

  <p id="form-result"></p>

  <script src="https://forminit.com/sdk/v1/forminit.js"></script>
  <script>
    const SITE_KEY = 'YOUR_SITE_KEY';
    const FORM_ID = 'YOUR_FORM_ID';
    
    const forminit = new Forminit();
    const form = document.getElementById('contact-form');
    
    form.addEventListener('submit', async function(event) {
      event.preventDefault();
      
      try {
        // Generate reCAPTCHA token
        const token = await grecaptcha.execute(SITE_KEY, { action: 'submit' });
        
        // Create FormData and append reCAPTCHA token (no fi- prefix)
        const formData = new FormData(form);
        formData.append('g-recaptcha-response', token);
        
        // Submit to Forminit
        const { data, redirectUrl, error } = await forminit.submit(FORM_ID, formData);
        
        if (error) {
          document.getElementById('form-result').textContent = error.message;
          return;
        }
        
        document.getElementById('form-result').textContent = 'Message sent successfully!';
        form.reset();
      } catch (err) {
        document.getElementById('form-result').textContent = 'An error occurred. Please try again.';
        console.error(err);
      }
    });
  </script>
</body>
</html>

'use client';

import { useState, useEffect } from 'react';
import { Forminit } from 'forminit';
import Script from 'next/script';

const SITE_KEY = 'YOUR_SITE_KEY';
const FORM_ID = 'YOUR_FORM_ID';

declare global {
  interface Window {
    grecaptcha: {
      ready: (callback: () => void) => void;
      execute: (siteKey: string, options: { action: string }) => Promise<string>;
    };
  }
}

export function ContactForm() {
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
  const [error, setError] = useState<string | null>(null);
  const [recaptchaReady, setRecaptchaReady] = useState(false);
  
  const forminit = new Forminit({ proxyUrl: '/api/forminit' });

  useEffect(() => {
    if (window.grecaptcha) {
      window.grecaptcha.ready(() => setRecaptchaReady(true));
    }
  }, []);

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    
    if (!recaptchaReady) {
      setError('reCAPTCHA not loaded. Please refresh the page.');
      return;
    }
    
    setStatus('loading');
    setError(null);

    try {
      // Generate reCAPTCHA token
      const token = await window.grecaptcha.execute(SITE_KEY, { action: 'submit' });
      
      // Create FormData and append token (no fi- prefix)
      const form = e.currentTarget;
      const formData = new FormData(form);
      formData.append('g-recaptcha-response', token);

      const { data, redirectUrl, error } = await forminit.submit(FORM_ID, formData);

      if (error) {
        setStatus('error');
        setError(error.message);
        return;
      }

      setStatus('success');
      form.reset();
    } catch (err) {
      setStatus('error');
      setError('An error occurred. Please try again.');
    }
  }

  return (
    <>
      <Script
        src={`https://www.google.com/recaptcha/api.js?render=${SITE_KEY}`}
        onLoad={() => {
          window.grecaptcha.ready(() => setRecaptchaReady(true));
        }}
      />
      
      <form onSubmit={handleSubmit}>
        <input type="text" name="fi-sender-firstName" placeholder="First name" required />
        <input type="text" name="fi-sender-lastName" placeholder="Last name" required />
        <input type="email" name="fi-sender-email" placeholder="Email" required />
        <textarea name="fi-text-message" placeholder="Message" required />
        
        {status === 'error' && <p className="error">{error}</p>}
        {status === 'success' && <p className="success">Message sent!</p>}
        
        <button type="submit" disabled={status === 'loading' || !recaptchaReady}>
          {status === 'loading' ? 'Sending...' : 'Send'}
        </button>
      </form>
    </>
  );
}

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { Forminit } from 'forminit';

const SITE_KEY = 'YOUR_SITE_KEY';
const FORM_ID = 'YOUR_FORM_ID';

const status = ref<'idle' | 'loading' | 'success' | 'error'>('idle');
const errorMessage = ref<string | null>(null);
const formRef = ref<HTMLFormElement | null>(null);
const recaptchaReady = ref(false);

const forminit = new Forminit({ proxyUrl: '/api/forminit' });

onMounted(() => {
  const script = document.createElement('script');
  script.src = `https://www.google.com/recaptcha/api.js?render=${SITE_KEY}`;
  script.onload = () => {
    window.grecaptcha.ready(() => {
      recaptchaReady.value = true;
    });
  };
  document.head.appendChild(script);
});

async function handleSubmit() {
  if (!formRef.value || !recaptchaReady.value) return;
  
  status.value = 'loading';
  errorMessage.value = null;

  try {
    // Generate reCAPTCHA token
    const token = await window.grecaptcha.execute(SITE_KEY, { action: 'submit' });
    
    // Create FormData and append token (no fi- prefix)
    const formData = new FormData(formRef.value);
    formData.append('g-recaptcha-response', token);

    const { data, redirectUrl, error } = await forminit.submit(FORM_ID, formData);

    if (error) {
      status.value = 'error';
      errorMessage.value = error.message;
      return;
    }

    status.value = 'success';
    formRef.value.reset();
  } catch (err) {
    status.value = 'error';
    errorMessage.value = 'An error occurred. Please try again.';
  }
}
</script>

<template>
  <form ref="formRef" @submit.prevent="handleSubmit">
    <input type="text" name="fi-sender-firstName" placeholder="First name" required />
    <input type="text" name="fi-sender-lastName" placeholder="Last name" required />
    <input type="email" name="fi-sender-email" placeholder="Email" required />
    <textarea name="fi-text-message" placeholder="Message" required />
    
    <p v-if="status === 'error'" class="error">{{ errorMessage }}</p>
    <p v-if="status === 'success'" class="success">Message sent!</p>
    
    <button type="submit" :disabled="status === 'loading' || !recaptchaReady">
      {{ status === 'loading' ? 'Sending...' : 'Send' }}
    </button>
  </form>
</template>

When using JSON format instead of FormData, include the token as a text block:

const forminit = new Forminit({ proxyUrl: '/api/forminit' });
const SITE_KEY = 'YOUR_SITE_KEY';
const FORM_ID = 'YOUR_FORM_ID';

async function submitForm() {
  // Generate reCAPTCHA token
  const token = await grecaptcha.execute(SITE_KEY, { action: 'submit' });
  
  const { data, redirectUrl, error } = await forminit.submit(FORM_ID, {
    blocks: [
      {
        type: 'sender',
        properties: {
          email: 'john@example.com',
          firstName: 'John',
          lastName: 'Doe',
        },
      },
      {
        type: 'text',
        name: 'message',
        value: 'Hello world',
      },
      // Include reCAPTCHA token as a text block
      {
        type: 'text',
        name: 'g-recaptcha-response',
        value: token,
      },
    ],
  });

  if (error) {
    console.error('Submission failed:', error.message);
    return;
  }

  console.log('Submission successful:', data.hashId);
}

reCAPTCHA v3 allows you to specify an “action” for each token request. This helps Google better analyze user behavior and provides more detailed analytics in your reCAPTCHA admin console.

// Use descriptive action names for different forms
const contactToken = await grecaptcha.execute(SITE_KEY, { action: 'contact_form' });
const signupToken = await grecaptcha.execute(SITE_KEY, { action: 'signup' });
const checkoutToken = await grecaptcha.execute(SITE_KEY, { action: 'checkout' });

Action naming best practices:

  • Use lowercase with underscores
  • Be descriptive but concise
  • Use unique actions for different forms
  • Avoid dynamic values in action names

When reCAPTCHA verification fails, Forminit returns specific error codes:

const { data, error } = await forminit.submit(FORM_ID, formData);

if (error) {
  switch (error.error) {
    case 'RECAPTCHA_VERIFICATION_FAILED':
      console.error('reCAPTCHA verification failed. Possible bot detected.');
      break;
    case 'RECAPTCHA_TOKEN_MISSING':
      console.error('reCAPTCHA token was not provided.');
      break;
    case 'RECAPTCHA_TOKEN_EXPIRED':
      console.error('reCAPTCHA token has expired. Generate a new one.');
      break;
    default:
      console.error('Submission error:', error.message);
  }
}
Error CodeDescriptionSolution
RECAPTCHA_VERIFICATION_FAILEDToken verification failed or score too lowUser may be a bot; consider showing a fallback
RECAPTCHA_TOKEN_MISSINGg-recaptcha-response not includedEnsure token is appended to FormData or JSON blocks
RECAPTCHA_TOKEN_EXPIREDToken is older than 2 minutesGenerate a fresh token before submission
RECAPTCHA_INVALID_SECRETSecret key is incorrectVerify secret key in Form Settings

reCAPTCHA v3 tokens expire after 2 minutes. Generate the token immediately before form submission, not when the page loads:

// ❌ Wrong - token may expire before submission
const token = await grecaptcha.execute(SITE_KEY, { action: 'submit' });
// ... user fills out form ...
formData.append('g-recaptcha-response', token); // Token might be expired

// ✅ Correct - generate token at submission time
form.addEventListener('submit', async (e) => {
  e.preventDefault();
  const token = await grecaptcha.execute(SITE_KEY, { action: 'submit' });
  formData.append('g-recaptcha-response', token);
  // Submit immediately
});

To test reCAPTCHA on localhost:

  1. Add localhost to your domains in the reCAPTCHA Admin Console
  2. Use your actual site key (test keys don’t exist for v3)
  3. Scores on localhost may be lower than production

Tip: Monitor your reCAPTCHA analytics in the admin console to understand score distributions and adjust your threshold if needed.


reCAPTCHA v3 loads Google’s scripts on your page. Consider:

  1. Update your privacy policy to mention reCAPTCHA usage
  2. Add appropriate disclosures near your forms
  3. Consider GDPR compliance for EU users

Example disclosure:

<p class="recaptcha-notice">
  This site is protected by reCAPTCHA and the Google
  <a href="https://policies.google.com/privacy">Privacy Policy</a> and
  <a href="https://policies.google.com/terms">Terms of Service</a> apply.
</p>

Google allows hiding the default badge if you include proper attribution. Add this CSS:

.grecaptcha-badge {
  visibility: hidden;
}

Required: When hiding the badge, you must include visible attribution:

<p class="recaptcha-branding">
  This site is protected by reCAPTCHA and the Google
  <a href="https://policies.google.com/privacy">Privacy Policy</a> and
  <a href="https://policies.google.com/terms">Terms of Service</a> apply.
</p>

StepAction
1Create reCAPTCHA v3 keys at Google Admin Console
2Add Secret Key to Forminit: Form Settings → CAPTCHA
3Load reCAPTCHA script with your Site Key
4Generate token with grecaptcha.execute() on form submit
5Include token as g-recaptcha-response in your submission

const token = await grecaptcha.execute(SITE_KEY, { action: 'submit' });
formData.append('g-recaptcha-response', token);
{
  blocks: [
    // ... other blocks
    {
      type: 'text',
      name: 'g-recaptcha-response',
      value: token,
    },
  ],
}