Skip to content

hCaptcha

Protect your forms from bots and abuse using hCaptcha. As a privacy-focused alternative to reCAPTCHA, hCaptcha doesn’t sell user data and is GDPR-compliant out of the box.


  • Privacy-first — Doesn’t sell personal data or use it for ad targeting
  • GDPR compliant — Built with European privacy regulations in mind
  • Accessibility — Offers audio challenges and accessibility cookies
  • Flexible modes — Choose between visible challenge or invisible verification

  1. User loads your form → hCaptcha widget loads on the page
  2. User completes challenge (or invisible mode verifies automatically) → Token is generated
  3. User submits form → Token is sent via h-captcha-response
  4. Forminit verifies token → hCaptcha validates the token server-side
  5. Submission accepted or rejected → Invalid tokens are blocked

  • An hCaptcha account (free at hcaptcha.com)
  • A Forminit form
  • Access to your website’s HTML/JavaScript

  1. Go to hCaptcha Dashboard and sign up or log in
  2. Navigate to Sites and click New Site
  3. Add your domain(s) (e.g., example.com, localhost for testing)
  4. Copy your Site Key from the site settings
  5. Navigate to Settings to find your Secret Key

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 hCaptcha as the provider
  5. Paste your Secret Key in the designated field
  6. Click Save

Once configured, Forminit will automatically verify the h-captcha-response token with hCaptcha on every submission.


Add the hCaptcha script to your HTML:

<script src="https://js.hcaptcha.com/1/api.js" async defer></script>

Place this in the <head> or before your closing </body> tag.


The hCaptcha token must be submitted with the field name h-captcha-response (no fi- prefix).

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

The default mode displays a checkbox that users click to verify. Some users may need to complete an image challenge.

<!DOCTYPE html>
<html>
<head>
  <title>Contact Form</title>
  <script src="https://js.hcaptcha.com/1/api.js" async defer></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>
    
    <!-- hCaptcha widget -->
    <div class="h-captcha" data-sitekey="YOUR_SITE_KEY"></div>
    
    <button type="submit">Send</button>
  </form>

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

  <script src="https://forminit.com/sdk/v1/forminit.js"></script>
  <script>
    const forminit = new Forminit();
    const FORM_ID = 'YOUR_FORM_ID';
    
    const form = document.getElementById('contact-form');
    
    form.addEventListener('submit', async function(event) {
      event.preventDefault();
      
      // Get hCaptcha response token from the widget
      const token = hcaptcha.getResponse();
      
      if (!token) {
        document.getElementById('form-result').textContent = 'Please complete the captcha.';
        return;
      }
      
      // Create FormData and append hCaptcha token (no fi- prefix)
      const formData = new FormData(form);
      formData.append('h-captcha-response', token);
      
      const { data, redirectUrl, error } = await forminit.submit(FORM_ID, formData);
      
      if (error) {
        document.getElementById('form-result').textContent = error.message;
        hcaptcha.reset();
        return;
      }
      
      document.getElementById('form-result').textContent = 'Message sent successfully!';
      form.reset();
      hcaptcha.reset();
    });
  </script>
</body>
</html>

'use client';

import { useState, useRef } from 'react';
import { Forminit } from 'forminit';
import HCaptcha from '@hcaptcha/react-hcaptcha';

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

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

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    
    if (!token) {
      setError('Please complete the captcha.');
      return;
    }
    
    setStatus('loading');
    setError(null);

    // Create FormData and append hCaptcha token (no fi- prefix)
    const form = e.currentTarget;
    const formData = new FormData(form);
    formData.append('h-captcha-response', token);

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

    if (error) {
      setStatus('error');
      setError(error.message);
      captchaRef.current?.resetCaptcha();
      setToken(null);
      return;
    }

    setStatus('success');
    form.reset();
    captchaRef.current?.resetCaptcha();
    setToken(null);
  }

  return (
    <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 />
      
      <HCaptcha
        sitekey={SITE_KEY}
        onVerify={(token) => setToken(token)}
        onExpire={() => setToken(null)}
        ref={captchaRef}
      />
      
      {status === 'error' && <p className="error">{error}</p>}
      {status === 'success' && <p className="success">Message sent!</p>}
      
      <button type="submit" disabled={status === 'loading'}>
        {status === 'loading' ? 'Sending...' : 'Send'}
      </button>
    </form>
  );
}

Install the React component:

npm install @hcaptcha/react-hcaptcha

<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 token = ref<string | null>(null);
const captchaId = ref<string | null>(null);

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

onMounted(() => {
  const script = document.createElement('script');
  script.src = 'https://js.hcaptcha.com/1/api.js?onload=onHcaptchaLoad&render=explicit';
  script.async = true;
  script.defer = true;
  document.head.appendChild(script);
  
  window.onHcaptchaLoad = () => {
    captchaId.value = window.hcaptcha.render('hcaptcha-container', {
      sitekey: SITE_KEY,
      callback: (t: string) => { token.value = t; },
      'expired-callback': () => { token.value = null; },
    });
  };
});

async function handleSubmit() {
  if (!formRef.value) return;
  
  if (!token.value) {
    errorMessage.value = 'Please complete the captcha.';
    return;
  }
  
  status.value = 'loading';
  errorMessage.value = null;

  // Create FormData and append hCaptcha token (no fi- prefix)
  const formData = new FormData(formRef.value);
  formData.append('h-captcha-response', token.value);

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

  if (error) {
    status.value = 'error';
    errorMessage.value = error.message;
    window.hcaptcha.reset(captchaId.value);
    token.value = null;
    return;
  }

  status.value = 'success';
  formRef.value.reset();
  window.hcaptcha.reset(captchaId.value);
  token.value = null;
}
</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 />
    
    <div id="hcaptcha-container"></div>
    
    <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'">
      {{ status === 'loading' ? 'Sending...' : 'Send' }}
    </button>
  </form>
</template>

Invisible hCaptcha runs in the background without showing a checkbox. It only presents a challenge when suspicious activity is detected.

<!DOCTYPE html>
<html>
<head>
  <title>Contact Form</title>
  <script src="https://js.hcaptcha.com/1/api.js" async defer></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>
    
    <!-- Invisible hCaptcha widget -->
    <div class="h-captcha" 
         data-sitekey="YOUR_SITE_KEY" 
         data-size="invisible"
         data-callback="onCaptchaVerify">
    </div>
    
    <button type="submit">Send</button>
  </form>

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

  <script src="https://forminit.com/sdk/v1/forminit.js"></script>
  <script>
    const forminit = new Forminit();
    const FORM_ID = 'YOUR_FORM_ID';
    
    const form = document.getElementById('contact-form');
    let pendingSubmit = false;
    
    // Called when invisible captcha is verified
    window.onCaptchaVerify = async function(token) {
      if (!pendingSubmit) return;
      pendingSubmit = false;
      
      // Create FormData and append hCaptcha token (no fi- prefix)
      const formData = new FormData(form);
      formData.append('h-captcha-response', token);
      
      const { data, redirectUrl, error } = await forminit.submit(FORM_ID, formData);
      
      if (error) {
        document.getElementById('form-result').textContent = error.message;
        hcaptcha.reset();
        return;
      }
      
      document.getElementById('form-result').textContent = 'Message sent successfully!';
      form.reset();
      hcaptcha.reset();
    };
    
    form.addEventListener('submit', function(event) {
      event.preventDefault();
      pendingSubmit = true;
      
      // Trigger invisible captcha
      hcaptcha.execute();
    });
  </script>
</body>
</html>

'use client';

import { useState, useRef } from 'react';
import { Forminit } from 'forminit';
import HCaptcha from '@hcaptcha/react-hcaptcha';

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

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

  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus('loading');
    setError(null);
    
    // Trigger invisible captcha
    captchaRef.current?.execute();
  }

  async function onVerify(token: string) {
    if (!formRef.current) return;
    
    // Create FormData and append hCaptcha token (no fi- prefix)
    const formData = new FormData(formRef.current);
    formData.append('h-captcha-response', token);

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

    if (error) {
      setStatus('error');
      setError(error.message);
      captchaRef.current?.resetCaptcha();
      return;
    }

    setStatus('success');
    formRef.current.reset();
    captchaRef.current?.resetCaptcha();
  }

  return (
    <form ref={formRef} 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 />
      
      <HCaptcha
        sitekey={SITE_KEY}
        size="invisible"
        onVerify={onVerify}
        ref={captchaRef}
      />
      
      {status === 'error' && <p className="error">{error}</p>}
      {status === 'success' && <p className="success">Message sent!</p>}
      
      <button type="submit" disabled={status === 'loading'}>
        {status === 'loading' ? 'Sending...' : 'Send'}
      </button>
    </form>
  );
}

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

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

async function submitForm() {
  // Get token from hCaptcha widget
  const token = hcaptcha.getResponse();
  
  if (!token) {
    console.error('Please complete the captcha.');
    return;
  }
  
  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 hCaptcha token as a text block
      {
        type: 'text',
        name: 'h-captcha-response',
        value: token,
      },
    ],
  });

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

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

hCaptcha supports light and dark themes:

<!-- Light theme (default) -->
<div class="h-captcha" data-sitekey="YOUR_SITE_KEY" data-theme="light"></div>

<!-- Dark theme -->
<div class="h-captcha" data-sitekey="YOUR_SITE_KEY" data-theme="dark"></div>

React:

<HCaptcha sitekey={SITE_KEY} theme="dark" onVerify={onVerify} />

Choose between normal and compact widget sizes:

<!-- Normal size (default) -->
<div class="h-captcha" data-sitekey="YOUR_SITE_KEY" data-size="normal"></div>

<!-- Compact size -->
<div class="h-captcha" data-sitekey="YOUR_SITE_KEY" data-size="compact"></div>

<!-- Invisible -->
<div class="h-captcha" data-sitekey="YOUR_SITE_KEY" data-size="invisible"></div>

Set the widget language:

<script src="https://js.hcaptcha.com/1/api.js?hl=fr" async defer></script>

Common language codes: en, fr, de, es, pt, it, ja, ko, zh


When hCaptcha verification fails, Forminit returns specific error codes:

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

if (error) {
  switch (error.error) {
    case 'HCAPTCHA_VERIFICATION_FAILED':
      console.error('hCaptcha verification failed.');
      break;
    case 'HCAPTCHA_TOKEN_MISSING':
      console.error('hCaptcha token was not provided.');
      break;
    case 'HCAPTCHA_TOKEN_EXPIRED':
      console.error('hCaptcha token has expired.');
      break;
    default:
      console.error('Submission error:', error.message);
  }
  
  // Always reset captcha on error
  hcaptcha.reset();
}
Error CodeDescriptionSolution
HCAPTCHA_VERIFICATION_FAILEDToken verification failedUser may be a bot; reset and retry
HCAPTCHA_TOKEN_MISSINGh-captcha-response not includedEnsure token is appended to FormData or JSON blocks
HCAPTCHA_TOKEN_EXPIREDToken has expiredReset captcha and have user verify again
HCAPTCHA_INVALID_SECRETSecret key is incorrectVerify secret key in Form Settings

hCaptcha tokens expire after approximately 2 minutes. Handle expiration with the expired-callback:

<div class="h-captcha" 
     data-sitekey="YOUR_SITE_KEY"
     data-callback="onVerify"
     data-expired-callback="onExpire">
</div>

<script>
  let captchaToken = null;
  
  function onVerify(token) {
    captchaToken = token;
  }
  
  function onExpire() {
    captchaToken = null;
    console.log('Captcha expired. Please verify again.');
  }
</script>

React:

<HCaptcha
  sitekey={SITE_KEY}
  onVerify={(token) => setToken(token)}
  onExpire={() => setToken(null)}
/>

MethodDescription
hcaptcha.render(container, params)Render widget in a container
hcaptcha.execute(widgetId?)Trigger invisible captcha
hcaptcha.reset(widgetId?)Reset the widget
hcaptcha.getResponse(widgetId?)Get the current token
hcaptcha.remove(widgetId)Remove a widget
AttributeValuesDescription
data-sitekeyYour site keyRequired
data-sizenormal, compact, invisibleWidget size
data-themelight, darkColor theme
data-callbackFunction nameCalled on successful verification
data-expired-callbackFunction nameCalled when token expires
data-error-callbackFunction nameCalled on error
data-tabindexNumberTab index for accessibility

hCaptcha provides accessibility features for users who cannot complete visual challenges:

  1. Audio challenges — Available by clicking the accessibility icon
  2. Accessibility cookie — Users can set a cookie at accounts.hcaptcha.com/accessibility to automatically pass challenges

To improve accessibility:

<div class="h-captcha" 
     data-sitekey="YOUR_SITE_KEY"
     data-tabindex="0">
</div>

hCaptcha provides test keys for development:

TypeSite KeySecret Key
Always pass10000000-ffff-ffff-ffff-0000000000010x0000000000000000000000000000000000000000
Always fail10000000-ffff-ffff-ffff-0000000000010x0000000000000000000000000000000000000001

Note: Remember to switch to your real keys before deploying to production.

Add localhost to your site’s allowed domains in the hCaptcha dashboard for local testing.


hCaptcha Enterprise offers additional features:

  • Risk scoring — Get detailed bot scores (0.0 - 1.0)
  • Custom challenge difficulty — Adjust based on your needs
  • No CAPTCHA experience — Passive verification for trusted users
  • Analytics dashboard — Detailed traffic insights

Contact hCaptcha for Enterprise pricing.


hCaptcha is designed with privacy in mind:

  • GDPR compliant — No personal data sold or used for advertising
  • CCPA compliant — Meets California privacy requirements
  • Privacy-first — Minimal data collection
  • No tracking — Doesn’t track users across sites

Example privacy disclosure:

<p class="captcha-notice">
  This site is protected by hCaptcha and its
  <a href="https://www.hcaptcha.com/privacy">Privacy Policy</a> and
  <a href="https://www.hcaptcha.com/terms">Terms of Service</a> apply.
</p>

FeaturehCaptchareCAPTCHA v3
Privacy focus✅ Strong⚠️ Google tracking
GDPR compliant✅ Built-in⚠️ Requires consent
Visible challenge✅ Optional❌ No
Invisible mode✅ Yes✅ Yes
Free tier✅ Yes✅ Yes
Audio challenges✅ Yes✅ Yes
Risk scoringEnterprise only✅ All tiers

StepAction
1Create hCaptcha account at hcaptcha.com
2Add Secret Key to Forminit: Form Settings → CAPTCHA
3Load hCaptcha script on your page
4Add widget with your Site Key
5Include token as h-captcha-response in your submission
6Reset captcha after submission (success or error)

const token = hcaptcha.getResponse();
formData.append('h-captcha-response', token);
{
  blocks: [
    // ... other blocks
    {
      type: 'text',
      name: 'h-captcha-response',
      value: token,
    },
  ],
}