A CAPTCHA field is the part of a web form responsible for producing evidence that the submitter is a human, not an automated script. It is not a single HTML input — it is the combination of a widget rendered into the DOM, a token that widget produces, a hidden field carrying that token in the form payload, and a server-side verification call that ratifies the token before your business logic runs.
If any of those four pieces is missing, the field offers no protection — the form simply has a decoration on it.
Anatomy of a CAPTCHA field
A modern CAPTCHA field has four moving parts:
| Part | Lives in | What it does |
|---|---|---|
| Widget container | Browser DOM | Renders the user-visible (or invisible) challenge |
| Token | JavaScript memory | Single-use string returned by the widget after the user passes |
| Hidden input | The submitted form | Carries the token to your server with the rest of the form |
| Verify call | Your backend | POSTs the token to the CAPTCHA provider to confirm authenticity |
A common bug is to render the widget but never read its token, or to read it on the client but never verify it on the server. In both cases the form looks protected and is not.
A minimal HTML example
<form id="signup" method="post" action="/signup">
<input name="email" type="email" required>
<input name="password" type="password" required>
<!-- Widget container; the SDK injects the iframe here -->
<div class="captchala" data-app-key="YOUR_PUBLIC_KEY"></div>
<!-- Hidden field populated by the widget callback -->
<input type="hidden" name="captcha_token" id="captcha_token">
<button type="submit">Create account</button>
</form>
<script src="https://cdn.captcha-cdn.net/captchala-loader.js" defer></script>
<script>
// The SDK exposes a callback when the user passes the challenge.
window.captchalaOnSuccess = (token) => {
document.getElementById('captcha_token').value = token;
};
</script>The hidden input is the actual "field" submitted to your server. Everything above it is plumbing whose only job is to populate that input correctly.
What the server does with the field
The token in the hidden field is meaningless to your server on its own — it is a reference, not a proof. Your server has to ask the CAPTCHA provider whether the token is legitimate, fresh, and bound to the right domain.
For CaptchaLa, that looks like this in PHP:
$token = $_POST['captcha_token'] ?? '';
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
$ch = curl_init('https://apiv1.captcha.la/v1/validate');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-App-Key: ' . getenv('CAPTCHALA_APP_KEY'),
'X-App-Secret: ' . getenv('CAPTCHALA_APP_SECRET'),
],
CURLOPT_POSTFIELDS => json_encode([
'pass_token' => $token,
'client_ip' => $ip,
]),
]);
$result = json_decode(curl_exec($ch), true);
if (empty($result['success'])) {
http_response_code(400);
exit('Verification failed');
}If success is true, the field has done its job and the form is safe to process. If not, reject the submission and force the user to re-solve.
Where CAPTCHA fields commonly break
A few patterns we see often in support tickets:
- Token reused across submissions. Tokens are single-use. If your front-end retries the same submit, the second call will fail. Reset the widget on every new attempt.
- Token expired. Most providers expire tokens after 2–5 minutes. If your form has a long pre-submit step (e.g., uploading a large file), generate the token immediately before submit, not at page load.
- Missing on dynamic forms. SPAs that swap out the form via client-side routing sometimes lose the widget. Re-render it after navigation, not just on initial mount.
- Validation skipped on the server. This is the worst case: a token is generated, passed through, and ignored. The form is unprotected. Make the verify call mandatory in your route handler.
Where to go next
If you are wiring a CAPTCHA field into a new project, start from the Web SDK quickstart — it shows the exact HTML, the loader script, and the verify call in five minutes. If you are debugging an existing field, walk the four parts above in order: widget rendered? token generated? hidden input populated? server verify called? The break is almost always in one of those four spots.