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?
Section titled “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
Section titled “How It Works”- User loads your form → hCaptcha widget loads on the page
- User completes challenge (or invisible mode verifies automatically) → Token is generated
- User submits form → Token is sent via
h-captcha-response - Forminit verifies token → hCaptcha validates the token server-side
- Submission accepted or rejected → Invalid tokens are blocked
Prerequisites
Section titled “Prerequisites”- An hCaptcha account (free at hcaptcha.com)
- A Forminit form
- Access to your website’s HTML/JavaScript
Step 1: Get hCaptcha Keys
Section titled “Step 1: Get hCaptcha Keys”- Go to hCaptcha Dashboard and sign up or log in
- Navigate to Sites and click New Site
- Add your domain(s) (e.g.,
example.com,localhostfor testing) - Copy your Site Key from the site settings
- 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
Section titled “Step 2: Add Secret Key to Forminit”- Go to your Forminit Dashboard
- Select your form
- Navigate to Form Settings → CAPTCHA
- Select hCaptcha as the provider
- Paste your Secret Key in the designated field
- 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
Section titled “Step 3: Add hCaptcha to Your Frontend”Load the hCaptcha Script
Section titled “Load the hCaptcha Script”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.
Field Naming
Section titled “Field Naming”The hCaptcha token must be submitted with the field name h-captcha-response (no fi- prefix).
| Format | How to Include |
|---|---|
| FormData | formData.append('h-captcha-response', token) |
| JSON | Add as a text block: { type: 'text', name: 'h-captcha-response', value: token } |
Visible Challenge Mode
Section titled “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
Section titled “HTML / Static Website”<!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>
Next.js
Section titled “Next.js”'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
Nuxt.js
Section titled “Nuxt.js”<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 Mode
Section titled “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)
Section titled “HTML / Static Website (Invisible)”<!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>
Next.js (Invisible)
Section titled “Next.js (Invisible)”'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>
);
}
JSON Submission
Section titled “JSON Submission”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();
}
Customization
Section titled “Customization”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>
Language
Section titled “Language”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
Error Handling
Section titled “Error Handling”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();
}
Common Errors
Section titled “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
Section titled “Token Expiration”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)}
/>
hCaptcha API Reference
Section titled “hCaptcha API Reference”Methods
Section titled “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
Section titled “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
Section titled “Accessibility”hCaptcha provides accessibility features for users who cannot complete visual challenges:
- Audio challenges — Available by clicking the accessibility icon
- 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>
Testing
Section titled “Testing”Test Keys
Section titled “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
Section titled “Local Development”Add localhost to your site’s allowed domains in the hCaptcha dashboard for local testing.
Enterprise Features
Section titled “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 hCaptcha for Enterprise pricing.
Privacy & Compliance
Section titled “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:
<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
Section titled “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
Section titled “Summary”| Step | Action |
|---|---|
| 1 | Create hCaptcha account at hcaptcha.com |
| 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
Section titled “Quick Reference”FormData
Section titled “FormData”const token = hcaptcha.getResponse();
formData.append('h-captcha-response', token);
{
blocks: [
// ... other blocks
{
type: 'text',
name: 'h-captcha-response',
value: token,
},
],
}
Related Documentation
Section titled “Related Documentation”- reCAPTCHA v3 Integration - Alternative CAPTCHA provider
- Honeypot Protection - Additional spam protection
- Form Blocks Reference - Complete field reference
- File Uploads - File upload documentation