hCaptcha
Protect a contact form with hCaptcha: create a site, add the widget to your HTML or React form and turn it on for the form.
hCaptcha shows a checkbox that, for most visitors, passes with one click, and a picture challenge when it is unsure. It is a common choice when you want a CAPTCHA that is not run by Google or Cloudflare. SilentForms verifies the token for you, so all you add to your site is the widget.
Do the steps in order: get keys, put the widget on your form, then turn it on in the dashboard. From the moment it is on, submissions without a valid token are refused.
1. Get a site key and secret#
- Sign up or log in at the hCaptcha dashboard.
- Open Sites. Your account comes with a site key; you can create more there, one per website is tidy. Add your site's hostname to the site key's domain list.
- For the secret, open your profile menu, choose Settings and generate a secret. Copy it straight away: hCaptcha does not show it again.
The site key is public and goes in your HTML. The secret is private and goes only into the SilentForms dashboard. hCaptcha's developer guide has the current screens if they have moved.
2. Add the widget to your form#
Paste the script and the widget <div> inside the <form>, above the submit button, and replace YOUR_SITE_KEY:
<form action="https://silentforms.com/api/submit" method="POST">
<input type="hidden" name="accessKey" value="YOUR_ACCESS_KEY_HERE">
<label for="name">Name</label>
<input id="name" type="text" name="name" required>
<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="checkbox" name="botcheck" style="display: none;" tabindex="-1" autocomplete="off">
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<div class="h-captcha" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">Send</button>
</form>Once the visitor passes, the widget adds an h-captcha-response field with the token to the form, and the browser submits it with the other fields. Keep the widget inside the <form>, or the field will not be included.
Optional attributes go on the <div>: data-size="compact" for a narrower widget in a sidebar, data-theme="dark" for dark pages.
3. Turn it on in SilentForms#
- Sign in to the dashboard and open the form.
- Under Spam protection, set Provider to hCaptcha.
- Paste the secret and select Save CAPTCHA.
SilentForms checks the secret with hCaptcha before saving, and tells you if it is not a valid secret (the most common mistake is pasting the site key). It is stored encrypted and never shown again.
Submitting with JavaScript#
With fetch, read the form with new FormData(form) so the token field is part of the body, and reset the widget after each attempt, because a token can only be verified once:
const form = document.querySelector('form');
form.addEventListener('submit', async (event) => {
event.preventDefault();
const data = Object.fromEntries(new FormData(form));
if (!data['h-captcha-response']) {
alert('Please complete the CAPTCHA.');
return;
}
const response = await fetch('https://silentforms.com/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(data),
});
const result = await response.json();
hcaptcha.reset();
alert(result.success ? 'Thanks, your message was sent.' : result.message);
if (result.success) form.reset();
});Checking for the token before sending saves a round trip and gives a clearer message than "Captcha is required."
React#
hCaptcha maintains an official React component, @hcaptcha/react-hcaptcha. It hands you the token in onVerify; add it to the body yourself under the name h-captcha-response:
import { useRef, useState } from 'react';
import HCaptcha from '@hcaptcha/react-hcaptcha';
export default function ContactForm() {
const captchaRef = useRef(null);
const [token, setToken] = useState('');
const [status, setStatus] = useState('');
async function handleSubmit(event) {
event.preventDefault();
const form = event.currentTarget;
const data = Object.fromEntries(new FormData(form));
const response = await fetch('https://silentforms.com/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ ...data, accessKey: 'YOUR_ACCESS_KEY_HERE', 'h-captcha-response': token }),
});
const result = await response.json();
captchaRef.current?.resetCaptcha();
setToken('');
setStatus(result.success ? 'Thanks, your message was sent.' : result.message);
if (result.success) form.reset();
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
<label htmlFor="message">Message</label>
<textarea id="message" name="message" required />
<input type="checkbox" name="botcheck" style={{ display: 'none' }} tabIndex={-1} autoComplete="off" />
<HCaptcha ref={captchaRef} sitekey="YOUR_SITE_KEY" onVerify={setToken} onExpire={() => setToken('')} />
<button type="submit" disabled={!token}>Send</button>
{status && <p role="status">{status}</p>}
</form>
);
}Install it with npm install @hcaptcha/react-hcaptcha. The button stays disabled until there is a token, and the token is cleared when it expires.
Testing and local development#
hCaptcha publishes test keys that always pass and provide no protection:
| Value | |
|---|---|
| Site key | 10000000-ffff-ffff-ffff-000000000001 |
| Secret | 0x0000000000000000000000000000000000000000 |
Save the test secret on a separate test form in the dashboard and use the test site key on your local page. Never use them on the live form.
hCaptcha's docs note that localhost and 127.0.0.1 are not accepted as hostnames. Their suggested workaround is a hosts-file entry such as 127.0.0.1 test.mydomain.com, then opening the page at that name.
Errors#
| Message | Cause and fix |
|---|---|
| Captcha is required. | No h-captcha-response arrived: the widget is outside the form, the visitor did not complete it, or your JavaScript did not include the token. |
| Captcha verification failed. Please try again. | The token expired (they are short-lived), was already used, or belongs to another site key. Reset the widget after every attempt. |
| 503: Captcha could not be verified right now. | hCaptcha did not answer, or the saved secret no longer works. Generate a new secret and save it in the dashboard. |
Next steps#
- Honeypot: keep it alongside hCaptcha.
- React: the full React contact form.
- Troubleshooting: more on CAPTCHA errors.