🕵️ Privacy
Does not track, does not use cookies, and does not connect to third-party services.
This page details how to integrate and configure BladeCAPTCHA on your website.
Does not track, does not use cookies, and does not connect to third-party services.
Compatible with screen readers, keyboard navigation, and low visual contrast.
No dependency on external APIs. Everything runs on your server.
Web Workers and JavaScript ES6 Modules (guaranteed compatibility with modern browsers such as Chrome, Firefox, Edge, and Safari).openssl, mbstring, and json.mv config/config.sample.php config/config.php
nano config/config.php
Import the captcha.js module and call initCaptcha() with the desired configuration:
<script type="module">
import { initCaptcha } from './captcha.js';
(async () => {
try {
await initCaptcha({
mode: 'autoFormIntegration',
// other parameters...
});
} catch (err) {
console.error(err || err.message);
}
})();
</script>
The initCaptcha(options) function accepts the following parameters:
| Key | Type | Req / Opt | Description |
|---|---|---|---|
mode |
'manualHandling' or 'autoFormIntegration' |
Required | Defines whether verification is started manually (without direct form integration) or if it intercepts form submission to automatically add a hidden field with the token value. |
formSelector |
string |
Required if mode = 'autoFormIntegration' |
CSS selector for the form to intercept. |
inputName |
string |
Required if mode = 'autoFormIntegration' |
Name of the hidden field where the token will be injected. |
statusSelector |
string |
Optional | Selector of the element where the CAPTCHA status will be displayed as text. CSS classes applied during execution:
|
verifyButtonSelector |
string |
Required if mode = 'manualHandling' |
Selector of the button that starts manual verification. |
submitButtonSelector |
string |
Required if mode = 'autoFormIntegration' |
Selector of the submit button. |
apiBaseUrl |
string |
Optional (default: '../php') |
Base URL for backend API calls (PHP endpoints). Useful when using servers on other domains or custom paths; ensure backend CORS allows the frontend domain (see configuration). |
onStart |
function |
Optional | Callback executed when the process starts. Ideal to disable buttons or show a spinner. |
onEnd |
function |
Optional | Callback to revert visual state after challenge ends or is canceled. |
onProgress |
function(p: number) |
Optional | Callback to report verification progress (0–100). |
manualHandlingAutoStartOnLoad |
boolean |
Optional | 🚀 Only for manualHandling. If true, verification starts immediately when the page loads. |
onSuccess |
function(token: string) |
Required for manualHandling |
Callback executed upon successful verification. Receives the token for server validation. 🔑 Essential for manual integration, since the token is not automatically injected. |
onError |
function(err: Error) |
Optional | Callback executed when an error occurs during verification. Receives an Error object with message for diagnosis. |
<script type="module">
import { initCaptcha } from './captcha.js';
(async () => {
try {
await initCaptcha({
mode: 'autoFormIntegration',
apiBaseUrl : '../php',
formSelector: '#contactForm',
inputName: 'captcha_token',
submitButtonSelector: '#submitBtn',
onStart: () => {
document.querySelector('#submitBtn').textContent = 'Verifying...';
},
onEnd: () => {
document.querySelector('#submitBtn').textContent = 'Submit';
}
});
} catch (err) {
console.error(err || err.message);
}
})();
</script>
<script type="module">
import { initCaptcha } from './captcha.js';
(async () => {
try {
await initCaptcha({
mode: 'manualHandling',
apiBaseUrl : '../php',
verifyButtonSelector: '#checkHuman',
statusSelector: '#captchaStatus',
manualHandlingAutoStartOnLoad: false,
onStart: () => {
console.log('Starting verification...');
},
onEnd: () => {
console.log('Verification canceled or completed.');
},
onProgress: (p) => {
console.log(`Progress: ${p}%`);
}
});
} catch (err) {
console.error(err || err.message);
}
})();
</script>
When using initCaptcha as an imported ES6 module, each function call runs in an isolated environment and internal variables are not accessible externally.
Therefore, if you need to preserve information between callbacks—like the original text of a button before changing it—you should define an auxiliary variable in your code and explicitly use it in the functions passed in the configuration.
import { initCaptcha } from './captcha.js'; // or correct path
// Define memo in the module or script context
let memo = null; // Auxiliary variable for onStart and onEnd
(async () => {
try {
await initCaptcha({
// other parameters...
onStart: () => {
const btn = document.querySelector('#submitBtn');
memo = btn.textContent; // save original text in memo
btn.disabled = true;
btn.style.cursor = 'wait';
btn.textContent = 'Please wait...';
},
onEnd: () => {
const btn = document.querySelector('#submitBtn');
btn.textContent = memo; // restore original text from memo
btn.disabled = false;
btn.style.cursor = 'pointer';
}
});
} catch (err) {
console.error(err || err.message);
}
})();
<script type="module">
import { initCaptcha } from './path/to/captcha.js';
// Array of form selectors that will use BladeCAPTCHA
const forms = ['#contactForm', '#loginForm', '#newsletterForm'];
forms.forEach(async (formSelector) => {
// Auxiliary variable to store the original submit button text
let memo;
try {
await initCaptcha({
mode: 'autoFormIntegration',
apiBaseUrl : '../php',
formSelector: formSelector,
inputName: 'captcha_token',
submitButtonSelector: `${formSelector} button[type="submit"]`,
onStart: () => {
const btn = document.querySelector(`${formSelector} button[type="submit"]`);
if (!btn) return;
memo = btn.textContent;
btn.textContent = 'Verifying...';
btn.disabled = true;
btn.style.cursor = 'wait';
},
onEnd: () => {
const btn = document.querySelector(`${formSelector} button[type="submit"]`);
if (!btn) return;
btn.textContent = memo || 'Submit';
btn.disabled = false;
btn.style.cursor = 'pointer';
}
});
} catch (err) {
console.error(err || err.message);
}
});
</script>
You can easily adjust its appearance, visibility, and behavior.
The challenge execution is locally adjusted according to the device’s capabilities.
Only 9 KB of minified JavaScript, including workers.
Algorithms designed to resist automated attacks.
100% cost-free, no premium plans.
You can use it in personal or commercial projects without restrictions.
BladeCAPTCHA dynamically optimizes the execution parameters of its proof-of-work challenge—such as the number of parallel workers and the size of computation batches—based on the device's hardware capabilities.
This optimization ensures that devices with lower processing power, such as mobile phones, can complete verification swiftly, while maintaining a high level of security against bots.
The entire calculation and adjustment process occurs locally within the browser. No hardware information or performance metrics are ever sent to external servers, ensuring complete user privacy.
BladeCAPTCHA allows defining some general behavior parameters from a simple PHP file.
This file defines constants and configuration variables used by the library during token validation. For security reasons, it is recommended to place it outside the public directory (e.g., in BladeCAPTCHA/config/config.php).
<?php
// BladeCAPTCHA/config/config.php
define('CAPTCHA_SECRET_KEY', '3b7e151628aed2a6abf7158809cf4f3c');
define('CAPTCHA_DIFFICULTY', 4);
define('CAPTCHA_EXPIRY', 300); // 5 minutes
// CORS configuration for AJAX requests from frontend
$config['cors_enabled'] = true; // Enable/disable CORS handling
$config['cors_allowed_origins'] = [ // Allowed origins for CORS (do not use '*')
'http://localhost',
'http://127.0.0.1',
'http://[::1]'
];
$config['cors_allow_credentials'] = true; // Allow cookies and CORS credentials
'*' for security and credential compatibility.
If CAPTCHA_SECRET_KEY is not defined, no token can be validated.
After the browser completes the challenge, the generated token is sent along with the form. Your backend must verify this token to ensure it is valid and recent.
require_once __DIR__ . '/captcha-lib.php';
use function Captcha\validateToken;
This example checks the CAPTCHA token and, if valid, continues processing the form.
<?php
// BladeCAPTCHA/public/php/process-form.php
require_once __DIR__ . '/captcha-lib.php';
use function Captcha\validateToken;
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('Method not allowed');
}
// Sanitize and validate the captcha token (always as string)
$token = trim((string)($_POST['captcha_token'] ?? ''));
function render($msg) {
exit("<!DOCTYPE html><meta charset='UTF-8'><body>$msg</body>");
}
// Validate format and authenticity
if (!preg_match('/^[a-f0-9]{32}$/i', $token) || !validateToken($token)) {
render('❌ Invalid CAPTCHA.');
}
if (!isset($_POST['name'])) {
render('❌ Missing "name" field.');
}
// ✅ Valid CAPTCHA
echo "<!DOCTYPE html><meta charset='UTF-8'><body><h1>CAPTCHA correct</h1><pre>";
foreach ($_POST as $k => $v) {
printf("<strong>%s:</strong> %s\n",
htmlspecialchars($k, ENT_QUOTES, 'UTF-8'),
htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8')
);
}
echo "</pre></body>";
manualHandling with manualHandlingAutoStartOnLoad: true, carefully consider whether it makes sense to also include verifyButtonSelector. While automatic start and the verify button can coexist, it is usually redundant in terms of usability.
BladeCAPTCHA requires Web Workers to execute the proof-of-work challenge without blocking the interface. If the browser does not support them, validation cannot complete and the form will be rejected. Currently, there is no alternative mode built-in.
The system relies on JavaScript to generate and solve the challenge. Without JavaScript, the token cannot be generated and the submission will be blocked.
Yes, since it is self-hosted and does not depend on external services, BladeCAPTCHA can work even in offline environments as long as the local server is available.
Each token is valid for approximately 60 seconds and can only be used once.
Yes, you can protect multiple forms on the same page. However, there must be a minimum interval of 10 seconds between challenges.
If the page is reloaded or closed before completing the challenge, the generated token is lost and a new one must be generated when submitting the form again.
Yes, BladeCAPTCHA is fully customizable via CSS and configuration options, allowing you to adapt its appearance and behavior to your site's needs.
To set the difficulty, change the CAPTCHA_DIFFICULTY value in config/config.php.
No. BladeCAPTCHA does not store, track, or send personal information to third parties.
To ensure accessibility and privacy, BladeCAPTCHA avoids images, sounds, or videos. Instead, it relies on a computational proof-of-work challenge that is transparent to the user and compatible with assistive technologies.
BladeCAPTCHA avoids loading scripts from CDNs or external services to prevent dependency on third parties that may collect data, use cookies, or compromise user privacy and security.
No. BladeCAPTCHA requires your backend to validate the received token to ensure the challenge was solved correctly and within the allowed time.
Yes, the BladeCAPTCHA JavaScript module can be used even if frontend and backend are on different servers or domains. For AJAX (fetch) requests to work correctly in cross-origin environments, BladeCAPTCHA includes built-in configuration for handling CORS (Cross-Origin Resource Sharing).
This means the PHP backend can automatically send the necessary HTTP headers (Access-Control-Allow-Origin, Access-Control-Allow-Credentials, etc.)
according to the settings in config.php ($config['cors_enabled'], $config['cors_allowed_origins'], etc.).
However, it is the developer's responsibility to correctly define allowed origins in cors_allowed_origins to maintain security, and adjust other CORS options as needed.
If using BladeCAPTCHA on the same domain or subdomain as the backend, usually no additional configuration is required.
It depends on the configured integration mode:
manualHandling: the token is received as an argument in the onSuccess(token) callback when the challenge is completed.
autoFormIntegration: the token is automatically inserted into a hidden field named according to inputName inside the protected form.
In both cases, this token must be sent to the backend to be validated using validateToken().
Currently, BladeCAPTCHA works with PHP, but support for Python and other backends will be available soon.
The name pays homage to Blade Runner, where the Voight-Kampff test (inspiring our inverted turtle logo) acts as a biological CAPTCHA to distinguish humans from replicants. This philosophical reference—drawing from Carl Jung archetypes that influenced Philip K. Dick—reflects our approach: a challenge evaluating not only responses but how they are generated.
| Feature | BladeCAPTCHA | reCAPTCHA | hCaptcha |
|---|---|---|---|
| GDPR/CCPA compliance | Yes | No | Partial |
| Privacy | Does not collect personal data | Sends data to Google | Sends data to third parties |
| Dependency on external services | No, runs entirely on your server without CDN or external APIs | Yes, loads scripts and depends on Google APIs | Yes, depends on CDN and third-party APIs |
| Accessibility | High, no visual challenges | Low | Medium |
| Customization | Full via CSS/HTML | None | Limited |
| Dual integration mode | Flexible: automatic or manual | Automatic only | Automatic only |
| Performance | Uses Web Workers to avoid blocking | Undocumented / unconfirmed | No Web Workers |
| Self-hosted | Yes, runs entirely on your server | No | No |
| Open source | 100% Open Source and no external APIs | Proprietary, depends on Google | Proprietary, depends on third parties |
| License | MIT | Proprietary | Proprietary |
| Cost | Free | Free with limits | Free / Paid |
| Mandatory credits | No | Yes | Yes (free plan) |
BladeCAPTCHA is a 100% free and self-funded project. If you found it useful and want to help cover hosting costs, improvements, and support, you can buy me a coffee ☕. Any contribution is welcome!
☕ Buy a coffee