Frequently Asked Question
Overview
This CAPTCHA system is designed for external websites to embed a powerful yet trustworthy captcha solution with server-side validation. When enrolled, an endpoint will be provided from our CDN pool.
From the client website point of view, the flow is:
- Add a container to the form.
- Load the hosted script from
/captcha/sdk.js. - Render the CAPTCHA into the container.
- Wait until the CAPTCHA reports that submission is allowed.
- Submit the form with the generated token.
- Verify that token from your backend using
/captcha/verify.php.
The browser should never be treated as the final authority. The final decision must be made by your server after verifying the token.
Prerequisites
Before a client site can use the CAPTCHA:
- the client domain must be enrolled - contact GEN for enrolment.
- the
CHALLENGEmust be chosen - we support multiple challenges. - the client site must be served from the registered domain
If the domain is not registered, the widget will refuse to operate.
The CHALLENGE value is used to choose which server-side challenge provider to load. It must be a valid provider name understood by the service, for example math for the simple test challenge or dice for the image-based dice challenge.
If the configured challenge name is missing, malformed, or does not map to a real provider, the widget returns a clear configuration error instead of failing silently.
Client-side integration
1. Add a container to your form
id="captcha-widget"
2. Load the hosted SDK
3. Render the CAPTCHA
What the SDK does
The hosted SDK in captcha/sdk.js:
- creates an iframe pointing at the CAPTCHA widget
- resizes that iframe to fit the challenge UI
- stores the returned token in a hidden form field
- tracks the current state of the widget
- exposes helper methods so your form can ask whether submission is allowed
Available client methods
The current SDK exposes:
CaptchaWidget.render(config)CaptchaWidget.invoke(instanceId)CaptchaWidget.isSolved(instanceId)CaptchaWidget.getState(instanceId)CaptchaWidget.getToken(instanceId)CaptchaWidget.canSubmit(instanceId)CaptchaWidget.reset(instanceId)CaptchaWidget.destroy(instanceId)
Recommended usage
For most forms, the main method you need is:
CaptchaWidget.canSubmit('signup-captcha') That gives a simple true or false answer for whether the form should be allowed to submit.
Hidden token field
If you pass:
hiddenInput: 'captcha_token' the SDK will automatically create or populate a hidden form field named captcha_token.
That means when the form submits, your backend will receive:
captcha_token= Server-side verification
Your backend must verify the token with captcha/verify.php.
CORS
Yes, CORS needs to be considered.
What does and does not need CORS
- Loading
captcha/sdk.jswith a normal does not require CORS headers. - Loading the iframe from
captcha/widget.phpdoes not use XHR orfetch, so normal iframe embedding is not controlled by CORS in the same way. - Server-to-server verification against
captcha/verify.phpdoes not rely on browser CORS at all.
Where CORS matters
CORS matters if you decide to make browser-side fetch() or XHR calls directly to CAPTCHA API endpoints from the client page.
For the current implementation, the main browser flow is:
- host page loads
captcha/sdk.js - SDK embeds
captcha/widget.php - the iframe posts messages back to the parent window
That means the primary widget flow avoids most CORS complexity.
Recommended CORS policy
If browser API endpoints are later added for direct cross-origin fetch() requests, the service should:
- return
Access-Control-Allow-Originonly for registered domains - avoid using
*for protected endpoints - optionally vary the response by
Origin - support
OPTIONSpreflight where needed - only allow the methods actually required, such as
POSTandOPTIONS
Example policy approach:
- if
Originmatches a domain in theCAPTCHAtable, echo that exact origin inAccess-Control-Allow-Origin - set
Vary: Origin - set
Access-Control-Allow-Methods: POST, OPTIONS - set
Access-Control-Allow-Headers: Content-Type
Recommended position for this CAPTCHA
- keep browser interaction inside the iframe and SDK
- keep trust decisions on the server side
- use server-to-server verification from the client site's backend
- only add CORS to browser API endpoints if direct cross-origin AJAX becomes necessary
So, from a client integration point of view, the normal embed flow should work without the client needing to do anything special for CORS.
Example PHP verification request
$token,
'domain' => 'www.example.com'
]);
$ch = curl_init('https://endpoint/captcha/verify.php');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Content-Length: ' . strlen($payload),
],
CURLOPT_TIMEOUT => 15,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$result = json_decode($response, true);
if ($httpCode === 200 && !empty($result['valid'])) {
// allow the protected action
} else {
// reject the submission
}
Verify request body
Send JSON like:
{
"token": "",
"domain": "www.example.com"
} Successful verify response
{
"status": "success",
"valid": true,
"domain": "www.example.com",
"captid": 123,
"userid": 0,
"created": "2026-03-28 15:00:00",
"expires": "2026-03-28 15:05:00",
"result": "Y",
"caller_ip": "203.0.113.10"
} Failed verify response
{
"status": "fail",
"valid": false,
"reason": "token_not_valid"
} Important client rules
1. Do not trust only the browser result
Even if the widget appears solved, always verify the token from your backend.
2. Keep the submit button disabled until solved
Use CaptchaWidget.canSubmit() or CaptchaWidget.isSolved() to control submit availability.
3. Expect short-lived tokens
Tokens are stored in the CAPTCHA_TOKENS store expire quickly.
If a token is too old, verification will fail and the user should solve a fresh CAPTCHA.
4. Expect one-time-use verification
Once a valid token is verified, it is consumed and cannot be reused.
Lockouts and failures
Current behaviour:
- the default challenge policy allows up to 5 wrong answers
- after 5 failures, the CAPTCHA locks
- the lock is stored in the session
- refreshing the page does not clear that lock for the current session
Client sites should treat a locked or expired challenge as a failed submission state and instruct the user to start a new browser session if required.
Dice challenge
The recommended next visual provider is dice.
How it works
- the widget shows an image containing two rendered dice
- each die value is represented by dots
- the dot placement is intentionally not perfectly regular, so the pips do not always line up like a textbook die face
- the die faces also contain extra line work or surface markings to make simple automated recognition harder
- the background is randomised for each challenge
- the user enters the total shown across both dice
Why use it
- it is still simple for a person to understand quickly
- it keeps the answer format easy to validate
- it is harder to treat as a trivial standard dice-recognition problem
- it fits the existing iframe and token model without any client integration changes
Client integration impact
No special client-side code is required beyond choosing the challenge name configured for the domain.
From the embedding page point of view, the flow stays the same:
- render the widget
- wait for
CaptchaWidget.canSubmit()to returntrue - submit the returned token
- verify it on the backend with
captcha/verify.php
Example challenge selection
If your registered domain is configured to use the dice provider, your render call can use:
window.CaptchaWidget.render({
container: '#captcha-widget',
instanceId: 'signup-captcha',
challenge: 'dice',
action: 'signup',
hiddenInput: 'captcha_token',
hidden: false
}); The final trust rule does not change: even after the dice challenge appears solved in the browser, your backend must still verify the token.
Form-age protections
The example test form in test/captchatest.php also demonstrates extra form protection using session timing:
- reject submissions that happen too quickly
- reject submissions that happen too long after form load
- reject submissions where the original form session is missing
These checks are not part of the CAPTCHA token itself, but they are recommended for sensitive forms.
Example end-to-end client pattern
Browser
- render CAPTCHA
- disable submit until solved
- submit form with
captcha_token
Backend
- receive posted form data
- read
captcha_token - call
captcha/verify.php - allow or reject based on the verify response
