The checkbox is the memorable part of reCAPTCHA, but it is not the security boundary. A bot can post directly to your endpoint and skip every line of browser JavaScript. The decision that matters must happen on your server, after Google verifies the submitted token and your application checks that the response belongs to the site it expected.
Choose a key type before copying code
v2 checkbox provides an explicit user challenge and is the clearest integration for this example.
v2 invisible runs from an action but still uses a challenge-oriented flow.
v3 or score-based keys return risk signals that need action names, thresholds, monitoring, and business policy rather than a universal pass/fail score.
reCAPTCHA on Google Cloud is the current management platform; new Classic keys stopped being offered and Classic keys were migrated or scheduled for migration.
Existing migrated v2/v3 integrations can continue using
siteverify; new projects should review Cloud quotas, billing, ownership, and the recommended assessment API.
Create and restrict the key in Google Cloud
Open the reCAPTCHA page in the Google Cloud console and select an owned project.
Create a website key using the integration type required by the application.
Add the exact production domains and the development domains you genuinely use.
Keep localhost testing separate from production when operational policy requires it.
Record key ownership, quota alerts, billing state, and a rotation contact.
If a framework or plugin requires
siteverify, retrieve the legacy secret from the key’s Integration tab; otherwise prefer the current Cloud API documented for the key.
Render the v2 checkbox inside the form
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Contact support</title>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
</head>
<body>
<form method="post" action="/contact">
<label>
Email
<input type="email" name="email" autocomplete="email" required>
</label>
<label>
Message
<textarea name="message" maxlength="4000" required></textarea>
</label>
<div class="g-recaptcha" data-sitekey="YOUR_PUBLIC_SITE_KEY"></div>
<button type="submit">Send message</button>
</form>
</body>
</html>What the browser contributes
The HTTPS API script discovers the
g-recaptchaelement and renders the widget.data-sitekeycontains the public site key, not the secret.On submission, the integration adds a
g-recaptcha-responseform field containing the token.requiredandmaxlengthimprove UX but are not server-side validation.async deferavoids blocking HTML parsing; a strict Content Security Policy must explicitly allow the required Google origins.
Keep the secret outside source control
RECAPTCHA_SECRET=replace-with-the-server-side-secret
RECAPTCHA_EXPECTED_HOSTNAME=example.comTreat configuration as deployment state
Load
.envwith the application’s secret manager or environment tooling; do not serve this file.Add local secret files to
.gitignoreand provide a placeholder-only.env.example.The expected hostname binds an otherwise valid response to your application domain.
Use separate keys and secrets for unrelated environments to limit blast radius.
Verify the token from a Node.js backend
import express from "express";
const app = express();
app.use(express.urlencoded({ extended: false, limit: "16kb" }));
app.post("/contact", async (req, res) => {
const token = req.body["g-recaptcha-response"];
if (typeof token !== "string" || token.length === 0) {
return res.status(400).send("Complete the anti-spam check.");
}
const params = new URLSearchParams({
secret: process.env.RECAPTCHA_SECRET ?? "",
response: token,
});
let verification;
try {
const googleResponse = await fetch(
"https://www.google.com/recaptcha/api/siteverify",
{
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: params,
signal: AbortSignal.timeout(5000),
},
);
if (!googleResponse.ok) throw new Error("verification service failed");
verification = await googleResponse.json();
} catch {
return res.status(503).send("Verification is temporarily unavailable.");
}
const expectedHost = process.env.RECAPTCHA_EXPECTED_HOSTNAME;
if (verification.success !== true || verification.hostname !== expectedHost) {
return res.status(400).send("Verification failed. Please try again.");
}
const email = String(req.body.email ?? "").trim();
const message = String(req.body.message ?? "").trim();
if (!email || !message || message.length > 4000) {
return res.status(400).send("Check the form fields.");
}
// Enqueue or store the message here using parameterized APIs.
return res.status(202).send("Message accepted.");
});
app.listen(3001);The backend owns the decision
The handler rejects a missing token before contacting Google.
URLSearchParamsencodes the documented form fields forsiteverify.The secret travels only from your backend to Google over HTTPS.
A timeout and exception path fail closed for form acceptance while returning a retryable service response.
Checking
successalone is weaker than checking the expectedhostnameas well.The application still validates its own fields after reCAPTCHA; CAPTCHA does not sanitize, authorize, or store data safely.
Understand the verification response
{
"success": true,
"challenge_ts": "2026-08-14T10:00:00Z",
"hostname": "example.com"
}Fields carry context, not just a boolean
successreports whether Google accepted the token verification.challenge_tsidentifies when the challenge was loaded; monitor implausible timing when useful.hostnameshould match the configured web origin expected by this endpoint.Failure responses can include
error-codes; log normalized codes for operations without logging the token or secret.Enterprise assessment responses use a different schema and should be validated according to that API.
Token lifetime changes form UX
A response token expires after two minutes and is valid for one verification.
Do not verify a token in the browser and then attempt to reuse it on the server.
If server validation rejects another form field, reset or re-render the widget so the user can obtain a fresh token.
Disable accidental double submission, but also make the backend idempotent where duplicate side effects matter.
Never cache an accepted token as proof for a later request.
Test without weakening production
Use Google’s documented test keys or a dedicated development key rather than sharing the production secret.
Test missing, expired, duplicate, malformed, wrong-hostname, network-timeout, and provider-error paths.
Verify that the form causes no email, database write, webhook, or account action before CAPTCHA and field validation pass.
Exercise keyboard navigation, screen-reader labels, slow networks, script blockers, and a clear retry path.
Keep any bypass behind an explicit test environment and make startup fail if that bypass appears in production.
reCAPTCHA is one layer, not the spam strategy
Rate-limit by multiple signals while accounting for NATs and accessibility.
Use CSRF protection for authenticated or state-changing browser flows.
Apply length limits, schema validation, safe output encoding, and parameterized database operations.
Use a honeypot or minimum-fill-time signal cautiously as supporting evidence, not an accessibility trap.
Queue outbound email and cap downstream work so a burst cannot exhaust the application.
Monitor rejection and conversion rates; overly aggressive controls can silently block real people.
Privacy, consent, and availability deserve design time
Loading reCAPTCHA sends browser data to a third party and is subject to Google’s terms and privacy disclosures. Determine whether your jurisdiction and consent model permit loading it immediately. Offer a usable fallback for people blocked by network policy, accessibility needs, or provider outages, and avoid exposing the user’s full IP unless your chosen API and privacy assessment require it.
Common failures
Widget says invalid domain: add the actual hostname to the key configuration and confirm you used the matching site key.
`missing-input-secret`: the backend secret is absent; do not substitute the public site key.
`invalid-input-response`: the submitted token is malformed, expired, or not valid for this key.
`timeout-or-duplicate`: obtain a new token because the old one expired or was already verified.
Works in HTML but spam still arrives: the backend is probably not enforcing verification before side effects.
Valid token from the wrong site: verify hostname or action context instead of trusting
successalone.Sudden quota behavior: inspect the Google Cloud project’s ownership, quotas, billing, alerts, and migration state.
Current Google references
Display reCAPTCHA v2 documents the widget script, site key, rendering, and callbacks.
Verify a response defines
siteverify, request fields, token lifetime, single use, response fields, and errors.Classic migration overview explains the Google Cloud transition, compatibility, quotas, and ownership.
Create website keys documents current key creation and legacy-secret access.
Comments and corrections