Using hCaptcha in PHP

hCaptcha is a privacy-respecting CAPTCHA service that positions itself as a direct replacement for Google reCAPTCHA. On the free tier, users are shown image-puzzle challenges — similar to the reCAPTCHA v2 experience but without routing data through Google's infrastructure. The API shape is intentionally close to reCAPTCHA's, which makes migration straightforward. Enterprise plans add invisible challenge modes and SLA guarantees.

hCaptcha is a reasonable choice if you want a traditional challenge widget, prefer to avoid Google's data collection, and 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 place the widget div inside your form. The widget renders automatically and, once the user completes the challenge, writes a token into a hidden field named h-captcha-response that is 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 additional JavaScript is required for the basic flow. The widget handles rendering and token injection automatically once the API script loads.

Server-Side Verification in PHP

Send the token to https://hcaptcha.com/siteverify via POST. The response JSON contains a success boolean and, on failure, an error-codes array. Unlike reCAPTCHA v3, there is no score — hCaptcha free returns a simple 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, replace the file_get_contents call with a cURL request using 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. When enabled, it works similarly to reCAPTCHA v3: no puzzle is shown to most users, and the challenge is resolved in the background. To activate it, add data-size="invisible" to the widget div and execute the challenge programmatically before form 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>

The server-side verification function remains unchanged for invisible mode. For most users on the free tier the standard puzzle widget is the correct choice.

Migrating from reCAPTCHA to hCaptcha

The two services share a similar API design, so migration is a matter of swapping out 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, updating the field name read from $_POST and the endpoint URL is all that's needed.

Troubleshooting

Further Reading