Fun & Secure PHP Captcha — Single-file, no database, no dependencies.
CaptchaFun Lite is a single PHP file. No Composer, no framework, no database required.
Upload captchafun.php to your project directory.
Open the file and change the secret_key value in the configuration array at the top:
'secret_key' => 'your-unique-random-string-here',
That's it. Include the file in your project and start using it.
The minimal code to protect a form:
<?php require_once 'captchafun.php'; ?>
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!CaptchaFun::verify($_POST, 'my_form')) {
$error = CaptchaFun::getLastError();
} else {
$success = true;
}
}
?>
<form method="post">
<input type="text" name="name" placeholder="Name" required>
<input type="email" name="email" placeholder="Email" required>
<?= CaptchaFun::render('my_form') ?>
<button type="submit">Submit</button>
</form>
Call CaptchaFun::render() inside your form, passing a unique form identifier:
<?= CaptchaFun::render('contact_form') ?>
The form ID must be unique per form on the page. It links the rendered challenge to the verification step.
In your form processing logic, call CaptchaFun::verify():
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!CaptchaFun::verify($_POST, 'contact_form')) {
// CAPTCHA failed
echo CaptchaFun::getLastError();
} else {
// CAPTCHA passed — process the form
// send email, save data, etc.
}
}
After a failed verification, get the human-readable error message:
$error = CaptchaFun::getLastError();
// Returns translated string like "Incorrect answer. Try again."
CaptchaFun::verify() and returns JSONCaptchaFunJS.refresh()<?php
require_once 'captchafun.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'error' => 'Invalid method']);
exit;
}
// Verify CAPTCHA first
if (!CaptchaFun::verify($_POST, 'ajax_form')) {
echo json_encode([
'success' => false,
'error' => CaptchaFun::getLastError(),
'refresh_captcha' => true
]);
exit;
}
// CAPTCHA passed — process form data
$name = htmlspecialchars(trim($_POST['name'] ?? ''));
$email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);
if (!$name || !$email) {
echo json_encode(['success' => false, 'error' => 'Invalid form data.']);
exit;
}
// Do something: send email, save to DB, etc.
echo json_encode([
'success' => true,
'message' => 'Form submitted successfully!'
]);
exit;
<?php require_once 'captchafun.php'; ?>
<!DOCTYPE html>
<html>
<head>
<title>AJAX Form with CaptchaFun</title>
</head>
<body>
<form id="ajaxForm">
<input type="text" name="name" placeholder="Name" required>
<input type="email" name="email" placeholder="Email" required>
<textarea name="message" placeholder="Message"></textarea>
<?= CaptchaFun::render('ajax_form') ?>
<button type="submit">Send</button>
<div id="formMessage"></div>
</form>
<script>
document.getElementById('ajaxForm').addEventListener('submit', function(e) {
e.preventDefault();
var form = this;
var formData = new FormData(form);
var messageEl = document.getElementById('formMessage');
// Disable button during submission
var btn = form.querySelector('button[type="submit"]');
btn.disabled = true;
btn.textContent = 'Sending...';
fetch('ajax-handler.php', {
method: 'POST',
body: formData
})
.then(function(response) { return response.json(); })
.then(function(data) {
if (data.success) {
messageEl.innerHTML = '<p style="color:green">' + data.message + '</p>';
form.reset();
// Refresh CAPTCHA for next submission
CaptchaFunJS.refresh('ajax_form');
} else {
messageEl.innerHTML = '<p style="color:red">' + data.error + '</p>';
// Refresh CAPTCHA on failure (challenge is single-use)
if (data.refresh_captcha) {
CaptchaFunJS.refresh('ajax_form');
}
}
})
.catch(function(err) {
messageEl.innerHTML = '<p style="color:red">Network error. Try again.</p>';
})
.finally(function() {
btn.disabled = false;
btn.textContent = 'Send';
});
});
</script>
</body>
</html>
CaptchaFunJS.refresh('form_id')refresh_captcha flag so the client knows when to refreshCaptchaFun has a built-in AJAX refresh handler. When CaptchaFunJS.refresh() is called, it requests:
GET captchafun.php?action=refresh&form_id=ajax_form
This returns a JSON response with new challenge HTML that replaces the current widget contents.
captchafun.php file. Make sure it's accessible via HTTP (not just included via PHP).
If you prefer sending JSON instead of FormData, convert the form data first:
var formData = new FormData(form);
var jsonData = {};
formData.forEach(function(value, key) { jsonData[key] = value; });
fetch('ajax-handler.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(jsonData).toString()
});
$_POST, so send data as application/x-www-form-urlencoded or multipart/form-data (FormData does this automatically). If you send application/json, you'll need to decode it manually with json_decode(file_get_contents('php://input'), true).
Simple arithmetic questions. Difficulty configurable: addition, subtraction, or multiplication.
Click the correct emoji from a selection. Fun and visual challenge.
Select the named color from colored circles. Quick and intuitive.
Drag a slider to the end. Validated server-side with timing checks.
Invisible trap field. Catches bots that auto-fill all fields.
Randomly selects from available types on each page load.
// In the config array:
'captcha_type' => 'emoji', // Always show emoji CAPTCHA
// Rotates between configured types:
'captcha_type' => 'random',
'available_types' => ['math', 'emoji', 'slider', 'color'],
All settings are in the $CAPTCHAFUN_CONFIG array at the top of captchafun.php:
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable/disable CAPTCHA globally |
captcha_type |
string | 'random' |
Type to show: math, emoji, color, slider, random |
available_types |
array | ['math','emoji','slider','color'] |
Types available for random mode |
difficulty |
string | 'easy' |
Math difficulty: easy, medium, hard |
theme |
string | 'colorful' |
Visual theme: minimal, colorful, dark, glass, fun |
dark_mode |
bool | false |
Enable dark color scheme |
min_submit_time |
int | 3 |
Minimum seconds before valid submission |
challenge_expiration |
int | 300 |
Seconds until challenge expires |
max_attempts |
int | 5 |
Failed attempts before lockout |
lockout_time |
int | 900 |
Lockout duration in seconds (15 min) |
honeypot_enabled |
bool | true |
Enable invisible honeypot field |
rate_limit_enabled |
bool | true |
Enable rate limiting |
language |
string | 'pt' |
Language: pt, en, es, fr |
secret_key |
string | — | Secret for HMAC signing (CHANGE THIS!) |
Clean, subtle borders, no shadows. Fits anywhere.
Gradient background, vibrant indigo accent. Default theme.
Deep dark background, purple accent. Native dark theme.
Glassmorphism effect with backdrop blur. Modern and elegant.
Dashed borders, warm colors, playful feel.
'theme' => 'glass', // Options: minimal, colorful, dark, glass, fun
Enable dark mode alongside any theme:
'dark_mode' => true,
Dark mode adapts colors while preserving the chosen theme's personality.
Every challenge generates a signed token using hash_hmac('sha256', ...). The token binds the challenge ID, form ID, and timestamp. Tampered tokens are rejected.
Each challenge can only be verified once. After verification (success or failure), the challenge is removed from the session. Replay attacks are impossible.
Forms submitted faster than min_submit_time seconds are rejected. Bots that instantly submit forms are caught.
Challenges expire after challenge_expiration seconds (default: 5 minutes). Old challenges are automatically cleaned from the session.
After max_attempts failed tries, the user is locked out for lockout_time seconds. Prevents brute force attacks.
An invisible field catches bots that auto-fill all inputs. The field is positioned off-screen with aria-hidden and tabindex="-1".
Correct answers are never exposed in HTML or JavaScript. All validation happens on the server. Disabling JS doesn't bypass the CAPTCHA.
CaptchaFun::render(string $formId): stringRenders the CAPTCHA widget HTML for a given form.
| Parameter | Type | Description |
|---|---|---|
$formId | string | Unique identifier for this form (default: 'default') |
Returns: HTML string to echo inside your form.
CaptchaFun::verify(array $postData, string $formId): boolValidates the submitted CAPTCHA response.
| Parameter | Type | Description |
|---|---|---|
$postData | array | The $_POST superglobal (or equivalent) |
$formId | string | Must match the ID used in render() |
Returns: true if valid, false if invalid.
CaptchaFun::getLastError(): stringReturns the last error message after a failed verify() call. The message is already translated to the configured language.
CaptchaFun::resetChallenge(string $formId): voidRemoves all stored challenges for a specific form. Useful when programmatically resetting a form.
CaptchaFunJS.refresh(formId)Refreshes the CAPTCHA widget via AJAX. Fetches a new challenge from the server and replaces the current one.
// Refresh after failed AJAX submission
CaptchaFunJS.refresh('ajax_form');
CaptchaFun dispatches custom events on the document element:
| Event | When | Detail |
|---|---|---|
captchafun:loaded |
Widget initialized on page | {} |
captchafun:reset |
After AJAX refresh completes | { formId: '...' } |
document.addEventListener('captchafun:loaded', function() {
console.log('CaptchaFun is ready!');
});
document.addEventListener('captchafun:reset', function(e) {
console.log('CAPTCHA refreshed for form:', e.detail.formId);
});
Use different form IDs for each CAPTCHA instance:
<!-- Login form -->
<form method="post" action="login.php">
<input name="username">
<input name="password" type="password">
<?= CaptchaFun::render('login_form') ?>
<button>Login</button>
</form>
<!-- Registration form -->
<form method="post" action="register.php">
<input name="email">
<input name="password" type="password">
<?= CaptchaFun::render('register_form') ?>
<button>Register</button>
</form>
Verify each one independently:
CaptchaFun::verify($_POST, 'login_form');
CaptchaFun::verify($_POST, 'register_form');
CaptchaFun includes 4 languages out of the box:
| Code | Language |
|---|---|
pt | Portuguese |
en | English |
es | Spanish |
fr | French |
'language' => 'en',
Add a new key to the $translations array inside the CaptchaFun class, following the existing pattern (copy the 'en' block and translate).