CrafyCAPTCHA Documentation
Welcome to the official documentation for CrafyCAPTCHA, an advanced and modular platform for protection against bots, massive traffic DDoS attacks, and spam.
Unlike traditional captchas, CrafyCAPTCHA uses an adaptive friction model. Legitimate users see nothing or just a checkbox, while suspicious traffic faces escalating biometric and cryptographic challenges.
Value Proposition
- Invisible when safe, rigorous when doubtful: Invisible Proof-of-Work based tests for safe traffic.
- Extreme Customization (White-label): The widget adapts to your brand design (colors, borders, typography).
- Easy Integration (Drop-in): Clean Frontend and Backend SDKs ready for production.
Getting Started
Starting to protect your site with CrafyCAPTCHA is quick and simple. Follow these steps for your first integration:
- Create an account: You will need to access with a Crafy Account. You can use your existing one or create a new one if you don't have it yet. Log in to our Control Panel.
-
Add Domains: In the panel, go to the My Sites section and add the domains
you want to protect.
Note: Thelocalhostdomain is authorized by default so you can test your integration locally without issues. -
Create a Credential: In the Credentials section, click on New
Credential. Fill in the requested information:
- Name (Label): An identifier for your key (e.g. Production or Development).
- Plan: Choose the plan that best fits your traffic volume.
-
Save your Secret Key: Upon creating the credential, you will be provided with the
necessary keys for your integration. Important: the
Secret Keywill only be shown to you once. Save it in a safe place (such as a.envfile). - Start Integration: Once you have your credentials, you are ready to begin. Head over to the Integration Flow section to connect your Backend and Frontend.
System Architecture
The system is divided into layers that analyze and mitigate malicious access attempts:
1. The Challenge Engine (AI and Risk Analysis)
Evaluates a risk matrix on the user to determine a Risk Score (between
0.0 and 1.0). Analyzes IP reputation, geolocation, Tor or
DataCenter usage, and Header/User-Agent behavior.
2. Multi-Layer Validations
- Proof of Work (PoW): Uses Altcha and Cloudflare Turnstile in the background. If the risk increases, the computational difficulty rises.
- Telemetry and Browser Fingerprinting: Deep analysis of the client environment, Private Access Tokens, and Machine Learning to detect anomalies.
- Dynamic Challenges: Can be completely invisible, checkbox, slider, or connect (in that order according to the detected Risk Score).
Credentials
To integrate CrafyCAPTCHA you need 4 credentials. The first 3 (Public Key,
Secret Key, and Signing Public Key) are obtained from your
Control Panel. The
Public Token is obtained dynamically from the Backend SDK using the
getPublicToken() method.
| Credential | Usage | Exposure | Changes? |
|---|---|---|---|
Public Key |
Identifies your account. Used in both Backend (SDKs) and Frontend (Client-Side SDK). | Client-Side + Server-Side | Never |
Secret Key |
Secret key for server cryptographic operations (creating flows, verifying tokens). Only shown once when creating the credential. | Only Server-Side | Never |
Signing Public Key |
Cryptographic public key used by the Client-Side SDK to verify iframe signatures. | Client-Side + Server-Side | Never |
Public Token |
Public token that identifies the active plan of your credential. Sent to the iframe as a parameter. | Client-Side + Server-Side | Yes (when changing plan) |
The Secret Key is only shown once at the time of creating the
credential. If you lose it, you must generate a new credential. For security, never
expose it in client-side code (HTML, public JavaScript, repositories, etc.).
When your credential changes plan (upgrade or downgrade), the Public Token is
regenerated
automatically. Backend SDKs handle this change dynamically: use the getPublicToken()
method to always get the updated value and pass it to the Client-Side SDK. The other credentials
remain the same.
Best Practices: Secure Storage
It is recommended to centralize all credentials in an environment variables file, outside the source code and version control.
# .env (PHP with phpdotenv, Node.js with dotenv, Python with python-dotenv)
CRAFY_PUBLIC_KEY=pk_xxxxxxxxxxxxxxxx
CRAFY_SECRET_KEY=sk_xxxxxxxxxxxxxxxx
CRAFY_SIGNING_PUBLIC_KEY=base64_xxxxxxxx
Note: the Public Token is not stored in the .env because it is obtained
dynamically from the Backend SDK via the getPublicToken() method.
Then, in your code, load the variables from the .env file:
// PHP (with phpdotenv)
$crafy = new CrafyCAPTCHA(
$_ENV['CRAFY_PUBLIC_KEY'],
$_ENV['CRAFY_SECRET_KEY']
);
// Node.js (with dotenv)
require('dotenv').config();
const captcha = new CrafyCAPTCHA(
process.env.CRAFY_PUBLIC_KEY,
process.env.CRAFY_SECRET_KEY
);
# Python (with python-dotenv)
import os
from dotenv import load_dotenv
from crafy_captcha import CrafyCAPTCHA
load_dotenv()
captcha = CrafyCAPTCHA(
os.environ['CRAFY_PUBLIC_KEY'],
os.environ['CRAFY_SECRET_KEY']
)
Add .env to your .gitignore file to avoid exposing your credentials in
version control systems. Include an .env.example file with empty keys as a reference
for
other developers.
Integration Flow
To protect a form (e.g., a Login), the flow requires the coordinated participation of your server and the user's browser.
- Create Flow (Backend): Using the Backend SDK on your server, you generate the
encryptedIframeOptions(containing the Nonce and challenge configuration cryptographically signed) via thecreateFlowmethod. - Rendering (Frontend): You pass the
encryptedIframeOptionsgenerated by the server to the Client-Side SDK (e.g., JavaScript or React). This SDK securely injects the CrafyCAPTCHA iframe, where the user resolves the challenge (either a visual puzzle or an invisible validation). - Approval and Submission (Frontend): Once the challenge is successfully completed,
the iframe emits an Encrypted Token via
postMessageto the Client-Side SDK. This token is automatically placed in a hidden input and sent to the server when the user submits the form. - Final Verification (Backend): Upon receiving the web form data, your server takes
the Token and validates it locally, atomically, and securely using the
verifyFlowmethod of the Backend SDK. If the result is valid (true), access is granted.
Frontend SDKs
Integrate CrafyCAPTCHA on the frontend using our official libraries. These are responsible for injecting the iframe, applying styles, and communicating securely and asynchronously.
Available Client-Side SDKs
1. JavaScript Vanilla
You can use the jsDelivr CDN to include the Vanilla JS version:
GitHub: crafy-captcha-js
Stats and versions: jsDelivr
Package
Package: npmjs
Package
A. Library Inclusion
<script src="https://cdn.jsdelivr.net/gh/crafycaptcha/[email protected]/dist/CrafyCAPTCHA.min.js" integrity="sha256-6U66+z7itP4Nexm7OJauXWVAKPMPY3sbdLU9/JMfbmY=" crossorigin="anonymous"></script>
You can force the latest version using
https://cdn.jsdelivr.net/gh/crafycaptcha/crafy-captcha-js/dist/CrafyCAPTCHA.min.js (not
recommended for production).
B. Initialize the Widget
Place a <div> container in your HTML (e.g.,
<div id="crafy-container"></div>) and call CrafyCAPTCHA.init():
document.addEventListener('DOMContentLoaded', () => {
CrafyCAPTCHA.init(
'crafy-container', // Container div ID
'YOUR_PUBLIC_KEY', // Public Key (Credential)
'YOUR_PUBLIC_TOKEN', // Public Token (Credential)
'YOUR_SIGNING_PUBLIC_KEY', // Signing Public Key (Base64)
{
// Security configuration options (you MUST use ONE of these two)
optionsUrl: '/crafy-options.php', // (RECOMMENDED: fetches options via Fetch to avoid caching issues)
// encryptedOptions: 'YOUR_ENCRYPTED_IFRAME_OPTIONS', // (Alternative: options injected directly via PHP into the HTML)
// Appearance and event options (these are not encrypted)
iframeUrl: 'https://captcha.crafy.net/challenge', // Challenge base URL
inputName: 'CrafyCAPTCHA_token', // Final hidden input name
theme: 'dark', // 'light' or 'dark'
onSuccess: (token) => {
console.log("Human verified. Token:", token);
}
}
);
});
C. Deferred Loading (defer / async)
To optimize your page performance, you can load the library in a deferred manner using the
defer or async attribute on the <script> tag. This prevents
the script from blocking page rendering.
<script src="https://cdn.jsdelivr.net/gh/crafycaptcha/[email protected]/dist/CrafyCAPTCHA.min.js" integrity="sha256-6U66+z7itP4Nexm7OJauXWVAKPMPY3sbdLU9/JMfbmY=" crossorigin="anonymous" defer></script>
When using defer or async, the CrafyCAPTCHA class might not be
available yet when DOMContentLoaded fires. For these cases, the library automatically
dispatches a custom CrafyCAPTCHALoaded event on window once it has finished
loading. Use this event to safely initialize the widget:
<!-- 1. Register the listener BEFORE loading the script -->
<script>
window.addEventListener('CrafyCAPTCHALoaded', () => {
CrafyCAPTCHA.init(
'crafy-container',
'YOUR_PUBLIC_KEY',
'YOUR_PUBLIC_TOKEN',
'YOUR_SIGNING_PUBLIC_KEY',
{
optionsUrl: '/crafy-options.php',
inputName: 'CrafyCAPTCHA_token',
onSuccess: (token) => {
console.log("Human verified. Token:", token);
}
}
);
});
</script>
<!-- 2. Load the library with defer -->
<script src="https://cdn.jsdelivr.net/gh/crafycaptcha/[email protected]/dist/CrafyCAPTCHA.min.js" integrity="sha256-6U66+z7itP4Nexm7OJauXWVAKPMPY3sbdLU9/JMfbmY=" crossorigin="anonymous" defer></script>
defer: The script is downloaded in parallel and executes after the DOM is ready, respecting script order. This is the recommended option.async: The script is downloaded in parallel and executes as soon as it downloads, without waiting for the DOM or other scripts. Useful when there are no ordering dependencies, but requires using theCrafyCAPTCHALoadedevent.
In both cases, registering the CrafyCAPTCHALoaded listener before the
<script> tag that loads the library ensures you never miss the event.
D. Initialization Options
The fifth parameter of the init() method is a configuration object to customize
the security loading, behavior and appearance of the widget on the client side:
| Property | Type | Description |
|---|---|---|
optionsUrl |
String | (Recommended) Path to the endpoint on your server that dynamically returns
the options. The JavaScript SDK will make an internal POST request to fetch the data. Your
backend must process the request by calling the createFlow() method of the SDK
and returning a JSON with the eo property:
echo json_encode(['eo' => $crafy->createFlow()]);. Ideal if your website uses
caching systems (like Cloudflare or WP Rocket) as it prevents nonce conflicts.
|
encryptedOptions |
String | (Legacy Alternative) The encrypted string generated by the server using
createFlow(), injected directly via PHP (or NodeJS) into the HTML code of the
page. Note: If the page is cached, the captcha will fail due to Replay Attack. It
is mandatory to use either optionsUrl or encryptedOptions.
|
theme |
String | Predefined widget theme. Can be 'light' or 'dark'. If not
specified, 'light' will be applied. |
inputName |
String | Name of the <input type="hidden"> field that the widget will
automatically create and inject into the form when the challenge is resolved. Default is
'CrafyCAPTCHA_token'.
|
onSuccess |
Function | Callback function executed after successfully validating the captcha. Receives as an
argument the final token (in base64 format) to be sent to the backend. |
iframeUrl |
String | (Optional) Allows overriding the base path where the challenges endpoint is mounted. |
style |
Object |
Object for total customization of widget colors. Supported properties are:
|
E. Automatic Loading Control (Optional)
By default, the widget will attempt to preload the challenge automatically in the background after initialization to ensure that verification is as fast as possible for the client.
You can disable this behavior using the setAutoLoad(false) method before
calling init(). If automatic loading is disabled, the challenge iframe will not load until
the user explicitly clicks the verification checkbox.
document.addEventListener('DOMContentLoaded', () => {
// 1. Disable automatic loading
CrafyCAPTCHA.setAutoLoad(false);
// 2. Initialize
CrafyCAPTCHA.init('crafy-container', 'YOUR_PUBLIC_KEY', 'YOUR_PUBLIC_TOKEN', 'YOUR_SIGNING_PUBLIC_KEY', {
optionsUrl: '/crafy-options.php'
});
});
To manually or programmatically force the loading from outside the script (for example, when the user focuses on a text input in the form), you can use:
CrafyCAPTCHA.loadIframe();
setAutoLoad(true)(Default): Significantly improves the customer experience by achieving a slightly faster verification. However, each load consumes credential quota (deducts one call tochallenge).setAutoLoad(false): Saves calls and credential quota usage. We highly recommend setting it to false when inserting the widget on a public page where many users enter but few submit the form, such as a comments form at the end of a blog article.
F. Multiple Widgets on the same page
You can render multiple instances of CrafyCAPTCHA within the same web page simply by calling
CrafyCAPTCHA.init() multiple times. This is extremely useful if you have, for example, a
registration form and a login form visible at the same time (e.g., in a modal).
To avoid collisions between widgets, you must ensure you strictly follow two rules:
- Each call to
init()must point to a unique container ID. - Each widget must configure a unique
inputNamein its options object. If two widgets share the same input name, the tokens will be overwritten and the SDK will throw a critical error in the console.
// Initialize Widget 1 (Login)
CrafyCAPTCHA.init('crafy-container-login', 'PK', 'PT', 'SPK', {
optionsUrl: '/crafy-options.php',
inputName: 'CrafyCAPTCHA_token_login'
});
// Initialize Widget 2 (Registration)
CrafyCAPTCHA.init('crafy-container-register', 'PK', 'PT', 'SPK', {
optionsUrl: '/crafy-options.php',
inputName: 'CrafyCAPTCHA_token_register'
});
If your system architecture requires placing two or more distinct widgets within the same HTML form, the SDK intelligently handles global validation via "Cascading Submit".
When the user presses the form's "Submit" button, the SDK will automatically detect all unresolved widgets, and will display the corresponding challenge one by one, passing the turn dynamically. The form will only be effectively submitted to the backend when the user has successfully resolved all widgets anchored to that form.
2. React SDK
If you use React, we have an official component that simplifies the data flow.
NPM: @crafyholding/crafy-captcha-react
A. Installation
npm install @crafyholding/crafy-captcha-react
B. Component Usage
import React, { useState } from 'react';
import CrafyCaptcha from '@crafyholding/crafy-captcha-react';
function LoginForm() {
const [captchaToken, setCaptchaToken] = useState(null);
const handleSubmit = (e) => {
e.preventDefault();
if (!captchaToken) {
alert('Please resolve the CAPTCHA first.');
return;
}
console.log("Sending to backend...", captchaToken);
};
return (
<form onSubmit={handleSubmit}>
<input type="email" placeholder="Your email" />
<CrafyCaptcha
publicKey="YOUR_PUBLIC_KEY"
publicToken="YOUR_PUBLIC_TOKEN"
signingPublicKey="YOUR_SIGNING_PUBLIC_KEY"
encryptedIframeOptions="YOUR_ENCRYPTED_IFRAME_OPTIONS"
options={{ theme: 'dark' }}
onSuccess={(token) => {
console.log("CAPTCHA Resolved!");
setCaptchaToken(token);
}}
/>
<button type="submit">Log In</button>
</form>
);
}
export default LoginForm;
Ensure that the domain where you use these scripts is in your authorized origins (Referers) list, otherwise the API will reject the request.
Backend (SDKs & API)
The backend allows you to create protection flows and validate incoming payloads from the client. You can interact with CrafyCAPTCHA directly through the REST API, or by using one of our official SDKs to simplify integration (local cryptographic validation).
Available Server-Side SDKs
Main SDK Methods
All server-side SDKs implement three fundamental methods:
getPublicToken()
Dynamically obtains the updated Public Token for your credential. This value changes
when your plan is updated (upgrade or downgrade), so it should not be hardcoded.
The obtained Public Token must be passed to the Client-Side SDK along with the other credentials when
initializing the widget.
createFlow(options)
Generates the encryptedIframeOptions: a cryptographically signed and encrypted string that
contains the flow Nonce and challenge configuration. This is the string you must pass to the Client-Side SDK to inject the iframe with the corresponding challenge.
Each call to createFlow creates a unique, one-time flow.
verifyFlow(captchaToken)
Validates the captchaToken: the base64 format string that the Client-Side SDK automatically
generates when the user completes the iframe challenge. This token travels to the server as part of the
form submission (default in the CrafyCAPTCHA_token field). The verifyFlow
method
verifies the HMAC signature, checks expiration, and consumes the Nonce atomically (Anti-Replay). It
returns a boolean: true if the token is valid, false
otherwise.
The PHP and Node.js SDKs use camelCase (createFlow,
verifyFlow). The Python SDK follows language conventions and uses
snake_case (create_flow, verify_flow). Functionality is
identical in all cases.
Options for `createFlow`
The createFlow method accepts an object/array of options to customize the behavior
of the validation widget. The main properties are:
| Option | Type | Default | Description |
|---|---|---|---|
mode |
String | 'auto' |
Defines the puzzle display behavior:
|
puzzles |
Array | [] |
Sets the order of visual puzzles. If empty, it defaults to:
['checkbox', 'slider', 'connect']. The higher the detected Risk Score, the
more the user will progress through the puzzles in this list. It also serves to force a
single type of visual puzzle if only one name is set in the array (e.g.,
['slider']).
|
1. PHP SDK
The PHP SDK facilitates flow creation and atomic validation. Recommended to prevent Replay Attacks.
GitHub: crafy-captcha-php
A. Installation
composer require crafycaptcha/crafy-captcha
B. Initialization and Create Flow
require __DIR__ . '/vendor/autoload.php';
use Crafy\Captcha\CrafyCAPTCHA;
// Initialize the SDK
$crafy = new CrafyCAPTCHA(
'YOUR_PUBLIC_KEY',
'YOUR_SECRET_KEY',
"https://captcha.crafy.net/api" // Optional, API URL
);
// Generate secure options for the iframe
$encryptedIframeOptions = $crafy->createFlow([
'mode' => 'puzzle', // auto | hidden | puzzle
'puzzles' => [], // [] | ['checkbox', 'slider', 'connect'] (custom order)
]);
// Obtain dynamic Public Token to pass to the Client-Side SDK
$publicToken = $crafy->getPublicToken();
// $encryptedIframeOptions and $publicToken are passed to the JS initializer in the frontend
C. Validate the captchaToken (When processing the form)
$captchaToken = $_POST['CrafyCAPTCHA_token']; // The captchaToken sent by the frontend
try {
// verifyFlow validates HMAC signature, expiration and consumes Nonce (Anti-Replay)
$isValid = $crafy->verifyFlow($captchaToken);
if($isValid) {
// Valid CAPTCHA. Process login or registration...
echo "Safe user.";
} else {
die("Captcha validation error.");
}
} catch (Exception $e) {
die("Invalid or expired token: " . $e->getMessage());
}
D. Custom Storage (Cache & Nonces)
By default, the SDK uses atomic storage in temporary files on the operating system, ensuring 100%
backward compatibility. However, for decentralized environments (e.g., multiple clustered servers) where
the /tmp folder is not shared, you can inject a different storage engine.
Option 1: Database (PDO)
Ideal if you already have a database connection and do not want to save files. First, you must run a one-time configuration script to create the necessary table:
The installSchema() method should be used a single time to configure
the initial table. You must not call it every time you instantiate CrafyCAPTCHA to create or verify
a flow.
<?php
// File: setup_database.php
require __DIR__ . '/vendor/autoload.php';
use Crafy\Captcha\PDOStorage;
// Configuration of your PDO connection (example with MySQL/MariaDB)
$dsn = 'mysql:host=127.0.0.1;dbname=my_app_db;charset=utf8mb4';
$user = 'root';
$password = 'your_password';
try {
$pdo = new PDO($dsn, $user, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
// Instantiate the storage
$storage = new PDOStorage($pdo);
// Execute schema creation one time only
$storage->installSchema();
echo "✅ Table 'crafy_storage' and its indexes created successfully.\n";
} catch (PDOException $e) {
echo "❌ Database error: " . $e->getMessage() . "\n";
} catch (Exception $e) {
echo "❌ General error: " . $e->getMessage() . "\n";
}
Once the table is created, you can inject the PDO engine into your code regularly:
use Crafy\Captcha\PDOStorage;
$pdo = new PDO('mysql:host=localhost;dbname=my_app', 'root', 'password');
// Inject the previously configured PDO engine
$crafy->setStorage(new PDOStorage($pdo));
// Usage remains exactly the same
$encryptedIframeOptions = $crafy->createFlow();
Option 2: Custom System (E.g. Redis / Memcached)
You can create your own class by implementing the StorageInterface:
use Crafy\Captcha\StorageInterface;
class MyRedisStorage implements StorageInterface {
private $redis;
public function __construct($redisInstance) {
$this->redis = $redisInstance;
}
public function getCache(string $key): ?string {
return $this->redis->get($key) ?: null;
}
public function consumeNonce(string $nonce): bool {
// In Redis, "DEL" returns 1 if it existed, which is atomic and perfect
return $this->redis->del('nonce_' . $nonce) > 0;
}
// ... Implement other required methods from StorageInterface ...
}
$crafy->setStorage(new MyRedisStorage($myRedisConnection));
2. Node.js SDK
The official Node.js SDK allows easily integrating CrafyCAPTCHA into Express, NestJS apps, etc.
NPM: crafy-captcha
GitHub: crafy-captcha-node
A. Installation
npm install crafy-captcha
B. Initialization and Create Flow
const {CrafyCAPTCHA} = require('crafy-captcha');
async function test() {
// Initialize the SDK
const captcha = new CrafyCAPTCHA('pk_your_public', 'sk_your_secret');
// Create Flow and generate encrypted options
const encryptedIframeOptions = await captcha.createFlow({
mode: 'puzzle', // auto | hidden | puzzle
puzzles: [] // [] | ['checkbox', 'slider', 'connect'] (custom order)
});
// Obtain dynamic Public Token to pass to the Client-Side SDK
const publicToken = await captcha.getPublicToken();
console.log("Iframe options:", encryptedIframeOptions);
console.log("Public Token:", publicToken);
}
test();
C. Validate the captchaToken
const {CrafyCAPTCHA} = require('crafy-captcha');
const captcha = new CrafyCAPTCHA('pk_your_public', 'sk_your_secret');
// Example in an Express route
app.post('/login', async (req, res) => {
const captchaToken = req.body.CrafyCAPTCHA_token;
try {
const isValid = await captcha.verifyFlow(captchaToken);
if(isValid) {
res.send("Safe user.");
} else {
res.status(403).send("Captcha validation error.");
}
} catch (error) {
res.status(500).send("Invalid or expired token.");
}
});
D. Custom Storage Adapters
Since JavaScript is inherently asynchronous, the pattern is very clean. For distributed environments, you
can store the cache and nonces in Redis by injecting an adapter that inherits from
StorageAdapter.
const { CrafyCAPTCHA, StorageAdapter } = require('crafy-captcha');
const { createClient } = require('redis');
// 1. Create your own adapter
class RedisStorage extends StorageAdapter {
constructor(redisClient) {
super();
this.redis = redisClient;
}
async getCache(key) {
return await this.redis.get(key);
}
async setCache(key, data, expiresAt) {
// Calculate the remaining seconds for Redis to auto-delete
const ttlSeconds = Math.max(0, expiresAt - Math.floor(Date.now() / 1000));
await this.redis.set(key, data, { EX: ttlSeconds });
}
async deleteCache(key) {
await this.redis.del(key);
}
async storeNonce(nonce, expiresAt) {
// Save with TTL for automatic Garbage Collection
const ttlSeconds = Math.max(0, Math.floor((expiresAt - Date.now()) / 1000));
await this.redis.set(`nonce_${nonce}`, '1', { EX: ttlSeconds });
}
async consumeNonce(nonce) {
// DEL in Redis returns 1 if it deleted something and 0 if it didn't exist. It's atomic (Anti-Replay Attack)
const deletedCount = await this.redis.del(`nonce_${nonce}`);
return deletedCount > 0;
}
async clearAllNonces() {
const keys = await this.redis.keys('nonce_*');
if (keys.length > 0) await this.redis.del(keys);
return keys.length;
}
async gcNonces() {
// No need for Garbage Collection in Redis because we set { EX: ttl }
return;
}
}
// 2. Application usage
async function startApp() {
const redisClient = createClient({ url: 'redis://localhost:6379' });
await redisClient.connect();
const captcha = new CrafyCAPTCHA('pk_...', 'sk_...');
// 3. Inject your new storage engine
captcha.setStorage(new RedisStorage(redisClient));
// 4. Everything works magically over Redis
const flowOptions = await captcha.createFlow({ mode: 'invisible' });
}
startApp();
3. Python SDK
The official Python SDK allows integrating CrafyCAPTCHA into Django, Flask, FastAPI apps, and any other Python framework.
The Python SDK uses snake_case following language conventions:
create_flow() instead of createFlow() and verify_flow()
instead
of verifyFlow().
PyPI: crafy-captcha
GitHub: crafy-captcha-python
A. Installation
pip install crafy-captcha
B. Initialization and Create Flow
from crafy_captcha import CrafyCAPTCHA
# Initialize the SDK
captcha = CrafyCAPTCHA('YOUR_PUBLIC_KEY', 'YOUR_SECRET_KEY')
# Generate secure options for the iframe
encrypted_iframe_options = captcha.create_flow({
'mode': 'puzzle', # auto | hidden | puzzle
'puzzles': [] # [] | ['checkbox', 'slider', 'connect']
})
# Obtain dynamic Public Token to pass to the Client-Side SDK
public_token = captcha.get_public_token()
# encrypted_iframe_options and public_token are passed to the frontend
print(encrypted_iframe_options)
print(public_token)
C. Validate the captchaToken
from crafy_captcha import CrafyCAPTCHA
captcha = CrafyCAPTCHA('YOUR_PUBLIC_KEY', 'YOUR_SECRET_KEY')
# The captchaToken sent by the frontend (e.g., from a Flask/Django form)
captcha_token = request.form.get('CrafyCAPTCHA_token')
try:
is_valid = captcha.verify_flow(captcha_token)
if is_valid:
print("Safe user.")
else:
# Get the exact failure reason
reason = captcha.get_last_flow_verify_error()
print(f"Validation error: {reason}")
except Exception as e:
print(f"Invalid or expired token: {e}")
D. Custom Storage Adapters
If your application runs across multiple containers (e.g., Kubernetes or Docker), you can use Redis to
temporarily store data by inheriting from StorageAdapter.
import redis
import time
from crafy_captcha import CrafyCAPTCHA, StorageAdapter
# 1. Create your own adapter
class RedisStorage(StorageAdapter):
def __init__(self, redis_client):
self.redis = redis_client
def get_cache(self, key: str) -> str:
data = self.redis.get(key)
return data.decode('utf-8') if data else None
def set_cache(self, key: str, data: str, expires_at: int) -> None:
ttl = max(0, expires_at - int(time.time()))
self.redis.setex(key, ttl, data)
def delete_cache(self, key: str) -> None:
self.redis.delete(key)
def store_nonce(self, nonce: str, expires_at: int) -> None:
ttl = max(0, expires_at - int(time.time()))
self.redis.setex(f"nonce_{nonce}", ttl, "1")
def consume_nonce(self, nonce: str) -> bool:
# Atomic: delete returns > 0 if it existed and 0 if already consumed
deleted_count = self.redis.delete(f"nonce_{nonce}")
return deleted_count > 0
def clear_all_nonces(self) -> int:
keys = self.redis.keys("nonce_*")
if keys:
self.redis.delete(*keys)
return len(keys)
def gc_nonces(self) -> None:
pass # Automatically deleted with TTL in Redis
# === APPLICATION USAGE ===
# 1. Connect to Redis
r = redis.Redis(host='localhost', port=6379, db=0)
# 2. Initialize the SDK
captcha = CrafyCAPTCHA('pk_...', 'sk_...')
# 3. Inject the storage engine
captcha.set_storage(RedisStorage(r))
# 4. The SDK now operates transparently over Redis
public_token = captcha.get_public_token()
WordPress Plugin
If you use WordPress, we provide an official plugin that greatly simplifies the integration of CrafyCAPTCHA into native forms without writing any code.
Step-by-step Installation
- Download the Plugin: Download the latest version.
- Upload to WordPress: In your WordPress administration panel, navigate to
Plugins > Add New Plugin. Click on the Upload Plugin button at the
top and select the
.zipfile you downloaded. - Install and Activate: Click Install Now and, once the installation is finished, click Activate Plugin.
- Configure: In the left sidebar menu of your WordPress, go to Settings > CrafyCAPTCHA. There you can enter your credentials (Public Key, Secret Key, and Signing Public Key) which you can get from your Control Panel.
Once activated and configured, the plugin will automatically inject and validate the CAPTCHA in
standard WordPress forms, such as the login page (wp-login.php), the registration form,
and comments form.
Integration Examples
To facilitate the implementation of CrafyCAPTCHA, we offer ready-to-use examples that you can take as a reference or starting point for your projects.
Complete Example: Frontend (JS) + Backend (PHP)
This example demonstrates step by step how to integrate the widget in an HTML form using our JavaScript SDK, and how to atomically receive and validate the resulting token on your server using our PHP SDK.
- Source Code (Public): GitHub Repository
- Live Demo: View Working Demo
Puzzle Types
Depending on the detected Risk Score, the system may present different levels of
friction. You can test each of the puzzles by forcing the challenge type in our demos:
- Invisible: No user interaction. Proof-of-Work (PoW) cryptographic tests are resolved in the background completely invisibly.
- Checkbox: A simple click. Evaluates mouse movement biometrics and reaction time. See Checkbox Demo.
- Slider: The user must slide a piece to match the image. See Slider Demo.
- Connect: The user must connect numbers with letters in an image. See Connect Demo.
Cloudflare Turnstile Integration
By default, CrafyCAPTCHA features its own security engine and cryptographic algorithms (Proof-of-Work). However, you can enhance this security by natively integrating Cloudflare Turnstile. This additional layer is optional and configured directly in your Control Panel.
By integrating Turnstile, you add Cloudflare's global telemetry and threat analysis to CrafyCAPTCHA's risk engine. The system will automatically create and manage Turnstile challenges, providing an invisible, privacy-preserving experience for genuine users while stopping sophisticated automated traffic.
Synchronization Steps
You don't need to modify your code; everything is managed from the panel:
- Create a Cloudflare account or log in.
- Go to your API Tokens panel.
- Click the Create Token button and select Get started under "Create Custom Token".
- Give it a name, for example: CrafyCAPTCHA Token.
- Under Permissions, add exactly these two permissions:
- Account ➔ Turnstile ➔ Edit
- Account ➔ Account Settings ➔ Read
- Click Continue to summary and then Create token.
- Copy the generated API Token.
- In your CrafyCAPTCHA Control Panel, go to the user menu, select Synchronize Cloudflare, and paste your token.
Once synchronized, CrafyCAPTCHA will automatically create the necessary Turnstile widgets in your Cloudflare account for each of your domains, orchestrating transparent validations without any intervention on your part.
Authentication (REST API)
If you wish to consume the administration API or protected endpoints, you must first obtain a JWT (JSON Web Token) by authenticating with your credentials.
Obtain Token
Endpoint: POST /api/?action=authenticate
fetch('/api/?action=authenticate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
public_key: 'YOUR_PUBLIC_KEY',
secret_key: 'YOUR_SECRET_KEY'
})
})
.then(res => res.json())
.then(data => {
if(data.status === 'success') {
console.log("Bearer Token:", data.data.token);
console.log("Updated Public Token:", data.data.public_token);
console.log("Expires in (seconds):", data.data.expires_in);
}
});
The successful response includes:
| Field | Type | Description |
|---|---|---|
token |
String | The JWT Bearer Token to authenticate subsequent API calls. |
public_token |
String | The updated Public Token for your credential. This is the same value returned by
getPublicToken() in the SDKs.
|
expires_in |
Integer | Lifetime of the Bearer Token in seconds. |
Token Usage
For all other endpoints, you must pass the token in the Authorization Header:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsIn...
API Endpoints
The cryptographic logic to generate encryptedIframeOptions (the createFlow
method) resides natively within the Backend SDKs and is not available as an endpoint in the
REST API for decentralized architecture (Zero-RTT) and security reasons. Therefore, it
is strongly recommended to use one of the official SDKs, or failing that,
port the encryption logic to your programming language.
Check Flow (Alternative Validation)
If you cannot use an SDK, you can validate the token by making an HTTP call to this atomic endpoint.
Endpoint: POST /api/?action=check_flow
Authentication: Requires JWT Bearer
// Payload to send
{
"flow_token": "token-uuid-generated-by-frontend"
}
// Successful Response
{
"status": "success",
"data": {
"valid": true,
"risk_score": 0.1,
"message": "Flow consumed atomically"
}
}
Note: This endpoint consumes the token ensuring the state changes to consumed.
Attempting to validate it twice will result in an error (prevents Replay Attacks).
Rate Limits & Billing
To protect our infrastructure and ensure availability for everyone, CrafyCAPTCHA uses a strict traffic control model at the edge.
1. Authentication Rate Limit
The Login endpoint (/api/?action=authenticate) is protected to prevent brute force on
your credentials. Limit:
10 attempts per minute per
IP, and 2 per
second. Exceeding this will return HTTP
429.
2. Limits per User and Plan
General API usage is limited according to the quota of your active billing plan. The plan comes embedded in the Bearer Token (JWT) once you authenticate.
| Plan | Requests per Minute | Requests per Second (Burst) |
|---|---|---|
| Starter | 8 req / min | 2 req / sec |
| Growth | 150 req / min | 20 req / sec |
| Scale | 500 req / min | 50 req / sec |
When changing plan (upgrade or downgrade), keep the following in mind:
- Public Key, Secret Key, and Signing Public Key never change.
- The Public Token is automatically regenerated. Backend SDKs handle this
dynamically through the
getPublicToken()method. See the Credentials section for more details. - For new Rate Limits to take effect in the REST API, you must re-authenticate
(
/api/?action=authenticate) to obtain a new Bearer Token with your updated limits.
If you exceed your plan limits, you will receive a 429 Too Many Requests response. We
suggest implementing Exponential Backoff delays in your scripts for these events.