Server-Side Verification
Every EU CAPTCHA token generated by the widget must be verified on your server before you trust the form submission. Client-side validation alone is insufficient — a bot can bypass it by submitting a form request directly to your endpoint.
How verification works
- The visitor's browser runs the challenge via
verify.jsand generates a token. - The widget injects a hidden
<input name="eu-captcha-response">into the form. When the user submits, this token reaches your server as part of the POST data. - Your server collects the token, the visitor's IP address, and their
User-Agent, then sends all of them to the EU CAPTCHA verification API. - The API returns
success: trueorfalse, and atrainfield that must also be checked. - Your server accepts or rejects the submission accordingly.
Verification request
POST https://api.eu-captcha.eu/v1/verify
Content-Type: application/json
All five fields are required:
| Field | Description |
|---|---|
sitekey |
Your public sitekey (from the dashboard) |
secret |
Your secret key (from the dashboard — server-side only) |
client_ip |
The visitor's IP address (IPv4 or IPv6). Use X-Forwarded-For or X-Client-IP to get the real IP when behind a CDN or proxy — do not forward the CDN's own address. |
client_token |
The value of the eu-captcha-response form field. May be an empty string — always submit it regardless. |
client_user_agent |
The visitor's User-Agent request header. |
Always submit
client_tokeneven when it is an empty string. The service uses all attempts — including incomplete ones — to improve detection accuracy.
Verification response
Normal — token valid:
{ "success": true, "train": false }
Normal — token invalid or already used:
{ "success": false, "train": false }
Bypass — validation was skipped:
{ "success": true, "train": true }
Always check train as well as success
When train is true, the API did not perform real validation and forced success to true. This happens when the sitekey does not exist, the secret is wrong, or the sitekey's protection is disabled. A train: true response in production means your verification is silently broken — check your credentials immediately.
Code examples
Official client libraries are available for Python, PHP, Ruby, and Java. The examples below show raw HTTP calls for reference and for languages without a dedicated library.
Node.js
async function verifyCaptcha({ token, clientIp, userAgent }) {
const res = await fetch(process.env.EUCAPTCHA_VERIFY_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sitekey: process.env.EUCAPTCHA_SITE_KEY,
secret: process.env.EUCAPTCHA_SECRET_KEY,
client_ip: clientIp,
client_token: token,
client_user_agent: userAgent,
}),
});
const data = await res.json();
// train: true means validation was bypassed — treat as failure
return data.success === true && data.train !== true;
}
// In your form handler:
app.post('/contact', async (req, res) => {
const valid = await verifyCaptcha({
token: req.body['eu-captcha-response'] ?? '',
clientIp: req.ip,
userAgent: req.headers['user-agent'] ?? '',
});
if (!valid) {
return res.status(400).json({ error: 'CAPTCHA verification failed' });
}
// process form...
});
PHP
An official PHP package (
myra-security-gmbh/eu-captcha) is available on Packagist and wraps this API with automatic IP detection and configurable fault-tolerance. See PHP Module for installation and usage.
function verifyCaptcha(string $token, string $clientIp, string $userAgent): bool {
$payload = json_encode([
'sitekey' => $_ENV['EUCAPTCHA_SITE_KEY'],
'secret' => $_ENV['EUCAPTCHA_SECRET_KEY'],
'client_ip' => $clientIp,
'client_token' => $token,
'client_user_agent' => $userAgent,
]);
$ch = curl_init($_ENV['EUCAPTCHA_VERIFY_URL']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
// train: true means validation was bypassed — treat as failure
return $response['success'] === true && $response['train'] !== true;
}
// In your form handler:
$token = $_POST['eu-captcha-response'] ?? '';
$clientIp = $_SERVER['REMOTE_ADDR'];
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
if (!verifyCaptcha($token, $clientIp, $userAgent)) {
http_response_code(400);
echo json_encode(['error' => 'CAPTCHA verification failed']);
exit;
}
Python
An official Python package (
myra-eucaptcha) is available on PyPI and wraps this API with async support, configurable timeouts, and fault-tolerance defaults. See Python Module for installation and usage.
import os
import requests
def verify_captcha(token: str, client_ip: str, user_agent: str) -> bool:
response = requests.post(
os.environ['EUCAPTCHA_VERIFY_URL'],
json={
'sitekey': os.environ['EUCAPTCHA_SITE_KEY'],
'secret': os.environ['EUCAPTCHA_SECRET_KEY'],
'client_ip': client_ip,
'client_token': token,
'client_user_agent': user_agent,
}
)
data = response.json()
# train: true means validation was bypassed — treat as failure
return data.get('success') is True and data.get('train') is not True
# Flask example:
@app.route('/contact', methods=['POST'])
def contact():
token = request.form.get('eu-captcha-response', '')
client_ip = request.remote_addr
user_agent = request.headers.get('User-Agent', '')
if not verify_captcha(token, client_ip, user_agent):
return jsonify({'error': 'CAPTCHA verification failed'}), 400
# process form...
Platform integrations
If you are using a reverse proxy or infrastructure-level bot protection, the verification described on this page is already handled for you:
- Traefik Plugin — Traefik middleware with CrowdSec and EU CAPTCHA
- CrowdSec Bouncer — standalone reverse proxy with CrowdSec and EU CAPTCHA
Security notes
Security checklist
- Store
EUCAPTCHA_VERIFY_URL,EUCAPTCHA_SITE_KEY, andEUCAPTCHA_SECRET_KEYin environment variables or a secrets manager — never hardcode them or expose them in frontend code. - Always verify server-side, even if you validate client-side.
- If
trainistruein a production response, your sitekey or secret is misconfigured. Check them in the dashboard immediately. - When behind a CDN or load balancer, ensure
client_ipis the real visitor IP, not the infrastructure IP — useX-Forwarded-FororX-Client-IPas appropriate.