Using hCaptcha in PHP
hCaptcha is a privacy-focused alternative to reCAPTCHA. The free tier shows image puzzles — like reCAPTCHA v2, but without routing data through Google. The API mimics reCAPTCHA's shape, so migration is mostly find-and-replace. Enterprise plans add invisible challenges and SLA guarantees.
Use hCaptcha if you want a visible challenge widget and prefer to keep Google out of the loop, but don't need the fully-invisible experience of Turnstile or reCAPTCHA v3. Sign up at hcaptcha.com, navigate to Sites → Add Site, and copy your site key and secret key before proceeding.
Front-End: Standard Widget
Load the hCaptcha script and drop the widget div inside your form. When the user completes the challenge, the widget writes a token into a hidden field named h-captcha-response that gets submitted with the form.
<!-- Load the hCaptcha API -->
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<form method="post" action="/contact">
<input type="text" name="name" placeholder="Your name" />
<input type="email" name="email" placeholder="Your email" />
<!-- hCaptcha widget — place before the submit button -->
<div class="h-captcha" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">Send</button>
</form>
No extra JavaScript needed. The widget renders and injects the token on its own once the script loads.
Server-Side Verification in PHP
POST the token to https://hcaptcha.com/siteverify. The response JSON contains a success boolean and, on failure, an error-codes array. Unlike reCAPTCHA v3, there's no score — hCaptcha free is pass or fail.
<?php
/**
* Verify an hCaptcha token.
*
* @param string $token The h-captcha-response POST value
* @param string $secretKey Your hCaptcha secret key
* @return bool
*/
function verifyHcaptcha(string $token, string $secretKey): bool
{
if (empty($token)) {
return false;
}
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => 'Content-Type: application/x-www-form-urlencoded',
'content' => http_build_query([
'secret' => $secretKey,
'response' => $token,
'remoteip' => $_SERVER['REMOTE_ADDR'] ?? '',
]),
'timeout' => 5,
],
]);
$response = @file_get_contents('https://hcaptcha.com/siteverify', false, $context);
if ($response === false) {
return false;
}
$data = json_decode($response, true);
return $data['success'] ?? false;
}
// Usage in your form processor:
$token = $_POST['h-captcha-response'] ?? '';
if (!verifyHcaptcha($token, 'YOUR_SECRET_KEY')) {
http_response_code(400);
exit('CAPTCHA verification failed. Please try again.');
}
// Proceed with form handling
If allow_url_fopen is disabled on your server, swap the file_get_contents call for a cURL request with the same POST parameters — the pattern is identical to the cURL examples in the Turnstile guide.
Invisible Mode (Enterprise)
hCaptcha's invisible mode requires an Enterprise subscription. It works like reCAPTCHA v3: most users see no puzzle, and the challenge resolves in the background. Add data-size="invisible" to the widget div and trigger the challenge programmatically before submission:
<div class="h-captcha"
data-sitekey="YOUR_ENTERPRISE_SITE_KEY"
data-size="invisible"
data-callback="onCaptchaSuccess">
</div>
<script>
function onCaptchaSuccess(token) {
document.getElementById('my-form').submit();
}
document.getElementById('my-form').addEventListener('submit', function (e) {
e.preventDefault();
hcaptcha.execute();
});
</script>
Server-side verification stays the same for invisible mode. Most free-tier users should stick with the standard puzzle widget.
Migrating from reCAPTCHA to hCaptcha
Migration means swapping four things: the script URL, the CSS class on the widget div, the POST field name, and the verification endpoint.
Front-end changes:
<!-- Before (reCAPTCHA v2) -->
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<div class="g-recaptcha" data-sitekey="OLD_GOOGLE_SITE_KEY"></div>
<!-- After (hCaptcha) -->
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<div class="h-captcha" data-sitekey="NEW_HCAPTCHA_SITE_KEY"></div>
PHP verification changes:
<?php
// Before: reCAPTCHA v2
$token = $_POST['g-recaptcha-response'] ?? '';
$verifyUrl = 'https://www.google.com/recaptcha/api/siteverify';
// Response required 'success' === true (no score on v2)
// After: hCaptcha
$token = $_POST['h-captcha-response'] ?? ''; // field name changed
$verifyUrl = 'https://hcaptcha.com/siteverify'; // endpoint changed
// Response still requires 'success' === true — logic is identical
The POST body parameters (secret, response, remoteip) are the same for both services. If you have a shared verification helper, update the $_POST field name and the endpoint URL — that's it.
Troubleshooting
- Widget not appearing: Check the browser console for script errors. Make sure the site key is correct and the domain is listed in your hCaptcha site settings — it must match exactly, including subdomains.
- "missing-input-response": The form was submitted before the user finished the CAPTCHA. Don't trigger the submit button programmatically before the widget has issued a token. Consider disabling the button until the success callback fires.
- "invalid-input-secret": The secret key sent from PHP doesn't match your hCaptcha dashboard. Check for extra whitespace or truncation when reading the key from environment variables or config files.
- Token already used: hCaptcha tokens are single-use. If your form processor runs twice for the same submission (e.g., a retry after validation failure), the second call gets rejected. Generate a fresh challenge each time.