How do you integrate hCaptcha with PHP?#
Render the official hCaptcha widget inside the form, read h-captcha-response in the PHP POST handler, and verify the token using the official hCaptcha server-verification documentation before performing the protected action.
The hCaptcha catalog links to the older first-party Using hCaptcha with PHP article. Its basic three-step flow remains correct, but the complete sample uses a different endpoint hostname than our current server-verification documentation, has no connection or total timeout, and combines unescaped user input into an HTML email. This guide provides a current PHP 8.5 verification path and keeps the application-specific action separate.
Reduce interruptions on protected PHP forms#
- Keep visitors focused on their task. hCaptcha Pro's 99.9% Passive mode challenges fewer than 0.1% of legitimate users, reducing interruptions on contact forms and other submissions verified by your PHP handler.
- Apply verification in proportion to risk. Pro increases challenge difficulty for suspicious interactions, helping ordinary visitors complete their forms with less friction while retaining stronger checks against abuse.
New Pro sitekeys use 99.9% Passive by default. For an existing sitekey upgraded to Pro, select that mode under Behavior in the hCaptcha dashboard.
Before you start#
You need:
- A server-rendered PHP form and POST handler to protect.
- The PHP cURL extension.
- An hCaptcha account with a sitekey and matching secret.
- Protected server configuration for both values.
The same Siteverify flow works on supported PHP branches, but test the exact runtime and extensions used in production. Review the official Plain PHP catalog entry, the existing hCaptcha PHP article, and the server verification contract.
Create your hCaptcha credentials#
- Start with hCaptcha Pro for fewer challenges and adaptive protection on protected PHP form submissions, or use existing compatible hCaptcha credentials.
- Create a sitekey and allow every hostname that will render the widget.
- Store the secret in an environment variable or secret manager.
- Render only the public sitekey in the form.
Never place the secret in HTML, browser JavaScript, committed PHP files, logs, or error responses.
Load the PHP configuration#
Read and validate the credentials during application bootstrap:
<?php
declare(strict_types=1);
$sitekey = getenv('HCAPTCHA_SITEKEY');
$secret = getenv('HCAPTCHA_SECRET');
if (!is_string($sitekey) || $sitekey === '' ||
!is_string($secret) || $secret === '') {
throw new RuntimeException('hCaptcha configuration is missing');
}
Do not display the exception details to visitors. Handle startup configuration failures through the application's protected operational logging.
Add the widget to the PHP form#
Escape the sitekey when inserting it into HTML and preserve the application's CSRF field:
<form method="post" action="/signup.php">
<input type="hidden" name="csrf_token"
value="<?= htmlspecialchars($csrfToken, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>">
<!-- Application fields go here. -->
<div class="h-captcha"
data-sitekey="<?= htmlspecialchars($sitekey, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"></div>
<button type="submit">Create account</button>
</form>
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
Confirm Content Security Policy allows the hCaptcha resources required by the deployed configuration.
Verify the response with PHP cURL#
Use http_build_query to encode every form value. Set finite connection and total timeouts, require HTTP 200, decode JSON with exceptions enabled, and check boolean success:
function verifyHCaptcha(string $token, string $secret, string $sitekey): bool
{
if ($token === '') {
return false;
}
$handle = curl_init('https://api.hcaptcha.com/siteverify');
if ($handle === false) {
return false;
}
$body = http_build_query([
'secret' => $secret,
'response' => $token,
'sitekey' => $sitekey,
], '', '&', PHP_QUERY_RFC3986);
curl_setopt_array($handle, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_TIMEOUT => 5,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
],
]);
try {
$responseBody = curl_exec($handle);
$status = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
if (!is_string($responseBody) || $status !== 200) {
return false;
}
$result = json_decode(
$responseBody,
true,
512,
JSON_THROW_ON_ERROR
);
return is_array($result) && ($result['success'] ?? false) === true;
} catch (JsonException) {
return false;
}
}
PHP releases the CurlHandle when it leaves scope; curl_close is deprecated in PHP 8.5 because it has had no effect since PHP 8. TLS certificate verification is enabled by default; do not disable it. Do not log the secret, response token, encoded form body, or complete response. The remoteip parameter is optional. We recommend sending it for improved verification accuracy and Enterprise risk scores when the application derives the visitor's IP address from a reviewed, trusted proxy configuration; otherwise omit it.
Gate the protected PHP action#
Read the submitted token as untrusted input and keep the protected action after all checks succeed:
$token = filter_input(INPUT_POST, 'h-captcha-response', FILTER_UNSAFE_RAW);
$captchaValid = is_string($token)
&& verifyHCaptcha(trim($token), $secret, $sitekey);
if (!$captchaValid) {
http_response_code(400);
$formError = 'Complete the anti-bot check again.';
} else {
// Validate the CSRF token and other fields first.
// Perform the protected action only after every check succeeds.
}
Enforce the expected POST method and a suitable request-body limit. Validate CSRF and every application field independently. Escape output by context and use reviewed mail or database APIs; hCaptcha verification does not make other submitted values safe. Tokens are short-lived and single-use, so render a new challenge after a rejected submission.
Review the existing PHP example#
The first-party article correctly explains that the sitekey is public, the secret stays on the server, the widget produces h-captcha-response, and Siteverify must run on the backend. It also advises against GET verification.
The article remains useful for its clear explanation of the browser token, server-held secret, and backend verification flow. Its complete contact-form example uses https://hcaptcha.com/siteverify; our current server-verification documentation specifies https://api.hcaptcha.com/siteverify for new integrations. Keep the basic flow while updating the sample to the documented endpoint, adding cURL timeouts and HTTP-status handling, and using the application's established validation, mail, and output-encoding controls. The original example suppresses mail errors, accepts the visitor's address in a mail header, and inserts form values into HTML without contextual escaping.
Test the PHP integration#
- Confirm a valid token permits the protected action exactly once.
- Reject missing, invalid, expired, reused, and wrong-sitekey tokens.
- Reject timeouts, cURL failures, non-200 responses, malformed JSON, and
success: false. - Confirm the secret never appears in HTML, browser requests, logs, traces, or error responses.
- Test CSRF handling, request limits, CSP, accessibility, keyboard behavior, and every allowed hostname.
- Test the exact PHP runtime, cURL build, TLS trust store, web server, and reverse-proxy configuration.
Troubleshoot common PHP problems#
The widget does not appear
Confirm the page receives the sitekey, the script loads once, the widget is inside the form, and CSP permits the required resources.
Every submission fails verification
Confirm PHP reads h-captcha-response, cURL sends an encoded POST body, the secret matches the sitekey, and the server can reach api.hcaptcha.com over HTTPS.
cURL reports a certificate error
Update the server's CA trust store and PHP cURL configuration. Do not disable peer or hostname verification.
Frequently asked questions#
Does PHP need an hCaptcha package?
No. PHP cURL and json_decode can call Siteverify directly. A framework HTTP client is also suitable when it preserves form encoding, timeouts, TLS verification, and fail-closed behavior.
Can I use the existing hCaptcha PHP article?
Use it to understand the basic flow. Review and update its complete sample before reuse because its endpoint differs from current server-verification documentation and its application code needs additional security controls.
Is a browser-side check enough?
No. Browser checks improve the form experience, but the PHP server must verify the token before the protected action.
Should the secret go in the PHP file?
No. Load it from environment-backed configuration or a secret manager. Only the sitekey belongs in the rendered page.
Should I send the user's IP address?
remoteip is optional. We recommend it for improved verification accuracy and Enterprise risk scores. Send it only when the application has a reviewed trusted-proxy configuration and a clear reason to send the visitor's IP address; otherwise omit it.
Sources and references
- hCaptcha Pro product overview hCaptcha
- hCaptcha integrations — Plain PHP hCaptcha
- Verify the hCaptcha response server-side hCaptcha
- Using hCaptcha with PHP hCaptcha
- PHP supported versions The PHP Group
- PHP cURL functions The PHP Group
- hCaptcha Pro hCaptcha
- hCaptcha integrations list source hCaptcha