Any public form — contact, signup, login, comment — will eventually attract bots. The simplest, most widely deployed defense is still Google's No CAPTCHA reCAPTCHA: the "I'm not a robot" checkbox (reCAPTCHA v2) that most users clear with a single click while automated submissions get stopped or challenged. This guide shows you how to add reCAPTCHA to a PHP form correctly in 2026 — get your keys, drop in the widget, and, most importantly, verify the response server-side, which is the step people most often get wrong.
We will do it the secure way from the start. An older, common tutorial pattern proxied the verification call through a public PHP file that echoed Google's response back to the browser — that is both unnecessary and insecure. The correct approach keeps your secret key on the server and makes verification a single server-to-server call. Here is the whole thing, step by step.
reCAPTCHA and the alternatives in 2026
Before you wire anything up, know your options, because "reCAPTCHA" now means several products:
- reCAPTCHA v2 (checkbox) — the classic "I'm not a robot" box, sometimes followed by an image challenge. Simple, familiar, and the focus of this guide.
- reCAPTCHA v2 (invisible) — no checkbox; the challenge only appears for suspicious users. Bound to a button or triggered programmatically.
- reCAPTCHA v3 — no user interaction at all. It returns a score (0.0–1.0) for each request, and you decide the threshold and what to do below it. Great for silent risk scoring, but it never blocks on its own — you must act on the score.
- reCAPTCHA Enterprise — Google Cloud's paid tier with granular risk analysis and reason codes, for high-value flows.
- Alternatives. Cloudflare Turnstile (free, privacy-friendly, no image puzzles) and hCaptcha are popular drop-in replacements with nearly identical server-side verification flows. If you dislike sending form traffic to Google, Turnstile is the leading swap.
For most PHP sites, reCAPTCHA v2 checkbox is the right default: minimal friction, trivial to add, and effective against unsophisticated bots. The rest of this guide implements it; the same server-side pattern applies to v3, hCaptcha, and Turnstile with only the endpoint and response fields changing.
Step 1: Register your site and get your keys
Go to the reCAPTCHA admin console (part of Google Cloud), register your domain, and choose reCAPTCHA v2 → "I'm not a robot" Checkbox. You receive two keys:
- Site key — public. It goes in your HTML and identifies your site to the widget.
- Secret key — private. It lives only on your server and is used for the verification call to Google. Never put the secret key in HTML or client-side JavaScript.
Add localhost to the allowed domains while developing.
Step 2: Add the widget to your form
Load Google's script and place the widget div inside your form. The script does the work of watching the user and, on success, injecting a hidden field named g-recaptcha-response into the form:
<!DOCTYPE html>
<html>
<head>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
</head>
<body>
<form action="verify.php" method="POST">
<label>Name <input type="text" name="name" required></label>
<label>Email <input type="email" name="email" required></label>
<textarea name="message" required></textarea>
<!-- replace with your real site key -->
<div class="g-recaptcha" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">Send</button>
</form>
</body>
</html>
When the user ticks the box (and clears any challenge), the widget populates g-recaptcha-response with a token. That token is submitted with the form and is what your server verifies. If the user never solves it, the field is empty — an easy first thing to reject.
Step 3: Verify the response server-side (the important part)
This is where security lives. On submit, your PHP takes the g-recaptcha-response token and makes a server-to-server POST to Google's siteverify endpoint, sending your secret key. Google replies with JSON; you trust success. The browser never sees the secret and never sees Google's raw answer.
Use cURL and a POST body — do not build a GET URL with the secret in the query string, and do not route this through a public proxy file:
<?php
// verify.php
declare(strict_types=1);
const RECAPTCHA_SECRET = 'YOUR_SECRET_KEY'; // keep this server-side only
function verify_recaptcha(string $token, string $remoteIp): bool
{
$ch = curl_init('https://www.google.com/recaptcha/api/siteverify');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'secret' => RECAPTCHA_SECRET,
'response' => $token,
'remoteip' => $remoteIp, // optional but recommended
]),
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true,
]);
$raw = curl_exec($ch);
if ($raw === false) {
error_log('reCAPTCHA: ' . curl_error($ch));
curl_close($ch);
return false; // fail closed on network error
}
curl_close($ch);
$data = json_decode($raw, true);
return is_array($data) && ($data['success'] ?? false) === true;
}
// --- handle the form submission ---
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_POST['g-recaptcha-response'] ?? '';
if ($token === '') {
http_response_code(400);
exit('Please complete the CAPTCHA.');
}
if (!verify_recaptcha($token, $_SERVER['REMOTE_ADDR'] ?? '')) {
http_response_code(403);
exit('CAPTCHA verification failed. Try again.');
}
// Verified as human — now process the form.
// (sanitize inputs, send mail, write to DB, etc.)
echo 'Thanks! Your message was sent.';
}
That is the entire integration: widget in the HTML, one server-side POST to verify. A few details make it robust:
- Fail closed. If the network call errors or the JSON is malformed, treat it as not verified. Never let a failed check fall through to accepting the submission.
- A token is single-use and short-lived. It expires after a couple of minutes and cannot be replayed, so verify it immediately on submit.
- Check
successon the server only. The browser response is advisory; the authoritative answer is the JSON your server receives fromsiteverify. - Keep processing the rest of your validation. reCAPTCHA proves "probably human," not "valid input." Still sanitize and validate every field.
If you use the JSON response fields
For reCAPTCHA v3, the same call returns a score and an action; branch on them instead of a boolean:
$data = json_decode($raw, true);
if (($data['success'] ?? false) && ($data['score'] ?? 0) >= 0.5) {
// likely human — proceed
} else {
// likely bot — block, challenge, or flag for review
}
Tune the threshold to your traffic. v3 also returns hostname and a challenge_ts timestamp you can sanity-check.
A note on the "insert reCAPTCHA" walkthrough
If you learned this from an older screencast that added a proxy.php file to bounce the verification request from the browser, skip that step entirely — it was a workaround for calling Google from client-side JavaScript, and it needlessly exposed the flow. The modern, correct walkthrough is exactly the three steps above: register for keys, add the g-recaptcha div, and verify the g-recaptcha-response token with one server-side cURL POST. Everything the old video demonstrated is captured here, minus the insecure proxy hop.
Invisible reCAPTCHA and button binding
To make the challenge invisible, register a v2 Invisible key and bind it to your submit button:
<button class="g-recaptcha"
data-sitekey="YOUR_SITE_KEY"
data-callback="onSubmit">Send</button>
The data-callback fires with the token once the (usually invisible) check passes; submit the form from there. Server-side verification is identical to Step 3 — nothing changes in your PHP.
Common errors and how to fix them
| Symptom | Cause | Fix |
|---|---|---|
| Widget shows "Invalid domain for site key" | Domain not registered | Add the domain (and localhost) in the admin console |
| Always fails verification | Swapped site/secret keys | Site key in HTML, secret key in PHP — never the reverse |
success: false, timeout-or-duplicate |
Token expired or reused | Verify immediately; each token is single-use |
success: false, missing-input-secret |
Empty/incorrect secret in POST | Check the secret key and that you are POSTing it |
| cURL returns false | TLS or network issue | Ensure the CA bundle is present; keep SSL_VERIFYPEER on |
Security best practices
- Secret key server-side only. If it ever appears in HTML, JS, or a public endpoint, rotate it immediately.
- Verify every submission, including AJAX ones. Client-side success is not proof.
- Combine defenses. CAPTCHA is one layer. Add rate limiting, a honeypot field, and input validation. Determined bots use human-powered CAPTCHA-solving services, so no single control is absolute.
- Log failures to spot abuse patterns, but do not leak whether it was the CAPTCHA or another rule that blocked a request.
FAQ
Which reCAPTCHA version should I use for a PHP form? v2 checkbox for most sites — one click for users, trivial to integrate. Choose v3 when you want silent scoring and are willing to act on a threshold, or Turnstile/hCaptcha if you would rather not use Google.
Do I need the JavaScript/AJAX and proxy approach from older tutorials?
No. The clean method is the server-side POST to siteverify shown above. The old client-side proxy pattern is obsolete and less secure.
Can I verify without cURL?
Yes — file_get_contents with a POST stream context works, but cURL gives you timeouts and error handling. Guzzle is a good choice inside a framework.
Will reCAPTCHA stop every bot? No control does. It blocks the easy majority and raises the cost for the rest. Layer it with rate limits and a honeypot. Understanding how CAPTCHAs are solved helps you set realistic expectations.
Wrapping up
Adding reCAPTCHA to a PHP form is three steps: get your site and secret keys, insert the g-recaptcha widget, and verify the g-recaptcha-response token with a single server-side cURL call to Google's siteverify. Keep the secret key off the client, fail closed on errors, and treat the CAPTCHA as one layer among several. Get those right and you filter out the overwhelming majority of automated submissions with almost no friction for real users.
Working the other side of this problem — building scrapers that must operate against CAPTCHA-protected sites? scraping.pro handles CAPTCHA-heavy targets as part of its managed web scraping service.