---
title: "hCaptcha"
description: "Protect your forms from bots and abuse using hCaptcha. A privacy-focused alternative to reCAPTCHA that is GDPR-compliant out of the box."
url: https://forminit.com/docs/hcaptcha/
---

# 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.

---

## Why hCaptcha?

- **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

---

## How It Works

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

---

## Prerequisites

- An hCaptcha account (free at <a href="https://www.hcaptcha.com" target="_blank" rel="nofollow noopener noreferrer">hcaptcha.com</a>)
- A Forminit form
- Access to your website's HTML/JavaScript

---

## Step 1: Get hCaptcha Keys

1. Go to [hCaptcha Dashboard](https://dashboard.hcaptcha.com) 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.

---

## Step 2: Add Secret Key to Forminit

1. Go to your [Forminit Dashboard](https://app.forminit.com)
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.

---

## Step 3: Add hCaptcha to Your Frontend

### Load the hCaptcha Script

Add the hCaptcha script to your HTML:

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

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

---

## Field Naming

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

| Format | How to Include |
|--------|----------------|
| **FormData** | `formData.set('h-captcha-response', token)` (use `set`, not `append`: the widget injects a hidden input with the same name) |
| **JSON** | Add as a text block: `{ type: 'text', name: 'h-captcha-response', value: token }` |

---

## Visible Challenge Mode

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

### HTML / Static Website

```html
<!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;
      }
      
      // Set the hCaptcha token (no fi- prefix). set() rather than append():
      // the widget injects a hidden h-captcha-response input inside the form
      const formData = new FormData(form);
      formData.set('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>
```

---

### Next.js

```tsx
'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);

    // Set the hCaptcha token (no fi- prefix). set() rather than append():
    // the widget injects a hidden h-captcha-response input inside the form
    const form = e.currentTarget;
    const formData = new FormData(form);
    formData.set('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:**

```bash
npm install @hcaptcha/react-hcaptcha
```

---

### Nuxt.js

```vue
<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;

  // Set the hCaptcha token (no fi- prefix). set() rather than append():
  // the widget injects a hidden h-captcha-response input inside the form
  const formData = new FormData(formRef.value);
  formData.set('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 Mode

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

### HTML / Static Website (Invisible)

```html
<!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;
      
      // Set the hCaptcha token (no fi- prefix). set() rather than append():
      // the widget injects a hidden h-captcha-response input inside the form
      const formData = new FormData(form);
      formData.set('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>
```

---

### Next.js (Invisible)

```tsx
'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;
    
    // Set the hCaptcha token (no fi- prefix). set() rather than append():
    // the widget injects a hidden h-captcha-response input inside the form
    const formData = new FormData(formRef.current);
    formData.set('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>
  );
}
```

---

## JSON Submission

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

```js
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();
}
```

---

## Customization

### Theme

hCaptcha supports light and dark themes:

```html
<!-- 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:

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

### Size

Choose between normal and compact widget sizes:

```html
<!-- 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>
```

### Language

Set the widget language:

```html
<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`

---

## Error Handling

When hCaptcha verification fails, Forminit returns specific error codes:

```js
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();
}
```

### Common Errors

| Error Code | Description | Solution |
|------------|-------------|----------|
| `HCAPTCHA_VERIFICATION_FAILED` | Token verification failed | User may be a bot; reset and retry |
| `HCAPTCHA_TOKEN_MISSING` | `h-captcha-response` not included | Ensure token is appended to FormData or JSON blocks |
| `HCAPTCHA_TOKEN_EXPIRED` | Token has expired | Reset captcha and have user verify again |
| `HCAPTCHA_INVALID_SECRET` | Secret key is incorrect | Verify secret key in Form Settings |

---

## Token Expiration

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

```html
<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:

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

---

## hCaptcha API Reference

### Methods

| Method | Description |
|--------|-------------|
| `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 |

### Data Attributes

| Attribute | Values | Description |
|-----------|--------|-------------|
| `data-sitekey` | Your site key | Required |
| `data-size` | `normal`, `compact`, `invisible` | Widget size |
| `data-theme` | `light`, `dark` | Color theme |
| `data-callback` | Function name | Called on successful verification |
| `data-expired-callback` | Function name | Called when token expires |
| `data-error-callback` | Function name | Called on error |
| `data-tabindex` | Number | Tab index for accessibility |

---

## 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 <a href="https://www.hcaptcha.com/accessibility" target="_blank" rel="nofollow noopener noreferrer">hcaptcha.com/accessibility</a> to automatically pass challenges

To improve accessibility:

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

---

## Testing

### Test Keys

hCaptcha provides test keys for development:

| Type | Site Key | Secret Key |
|------|----------|------------|
| Always pass | `10000000-ffff-ffff-ffff-000000000001` | `0x0000000000000000000000000000000000000000` |
| Always fail | `10000000-ffff-ffff-ffff-000000000001` | `0x0000000000000000000000000000000000000001` |

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

### Local Development

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

---

## Enterprise Features

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 <a href="https://www.hcaptcha.com/enterprise" target="_blank" rel="nofollow noopener noreferrer">hCaptcha</a> for Enterprise pricing.

---

## Privacy & Compliance

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:

```html
<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>
```

---

## Comparison: hCaptcha vs reCAPTCHA

| Feature | hCaptcha | reCAPTCHA 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 scoring | Enterprise only | ✅ All tiers |

---

## Summary

| Step | Action |
|------|--------|
| 1 | Create hCaptcha account at <a href="https://www.hcaptcha.com" target="_blank" rel="nofollow noopener noreferrer">hcaptcha.com</a> |
| 2 | Add **Secret Key** to Forminit: Form Settings → CAPTCHA |
| 3 | Load hCaptcha script on your page |
| 4 | Add widget with your **Site Key** |
| 5 | Include token as `h-captcha-response` in your submission |
| 6 | Reset captcha after submission (success or error) |

---

## Quick Reference

### FormData

```js
const token = hcaptcha.getResponse();
// set() rather than append(): the widget injects a hidden input with the same name
formData.set('h-captcha-response', token);
```

### JSON

```js
{
  blocks: [
    // ... other blocks
    {
      type: 'text',
      name: 'h-captcha-response',
      value: token,
    },
  ],
}
```

---

## Related Documentation

- [Cloudflare Turnstile Integration](https://forminit.com/docs/cloudflare-turnstile/) - Alternative CAPTCHA provider
- [reCAPTCHA v3 Integration](https://forminit.com/docs/recaptcha/) - Alternative CAPTCHA provider
- [Honeypot Protection](https://forminit.com/docs/honeypot/) - Additional spam protection
- [Form Blocks Reference](https://forminit.com/docs/form-blocks/) - Complete field reference
- [File Uploads](https://forminit.com/docs/file-upload/) - File upload documentation
