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

Further Reading