Documentation

LightCaptcha v1.0.0 — A free product by vCaptcha

Installation

LightCaptcha is distributed as a single ZIP file. Download it, unzip it into your webroot, and you're ready to go.

# Download and unzip into your project root
unzip lightcaptcha-v1.0.0.zip

# Your project will look like this:
your-website/
├── lightcaptcha/
│   ├── js/
│   │   └── lightcaptcha.js
│   ├── images/
│   │   └── puzzle_01_sunset.jpg  (+ 9 more)
│   └── server/
│       ├── lightcaptcha.php
│       ├── lightcaptcha-verify.php
│       ├── lightcaptcha.py
│       └── lightcaptcha.js

Quick start

Three steps to integrate LightCaptcha into any form.

Step 1 — Set your secret key

Open lightcaptcha/server/lightcaptcha-verify.php and set your secret key at the top of the file:

define('LIGHTCAPTCHA_SECRET', 'your-long-random-key-here');
Tip: Generate a secure key with openssl rand -hex 32 on any Linux or macOS terminal. Use the same key in both the verify endpoint and your form handler.

Step 2 — Add the widget to your form

<form method="POST" action="/submit.php">

  <!-- your form fields -->

  <div data-lightcaptcha
       data-lc-verify="/lightcaptcha/server/lightcaptcha-verify.php"
       data-lc-images="/lightcaptcha/images">
  </div>

  <button type="submit">Submit</button>
</form>

<script src="/lightcaptcha/js/lightcaptcha.js"></script>

Step 3 — Verify on form submit

In your form handler, verify the token before processing anything:

define('LIGHTCAPTCHA_SECRET', 'your-long-random-key-here');
require_once 'lightcaptcha/server/lightcaptcha.php';

if (!LightCaptcha::check()) {
    http_response_code(400);
    die('Captcha verification failed.');
}

// Safe to process the form

Configuration

LightCaptcha uses PHP constants for server-side configuration. Define them before including the library:

// Required
define('LIGHTCAPTCHA_SECRET', 'your-key');

// Optional — defaults shown
define('LIGHTCAPTCHA_TTL',    600);       // token lifetime in seconds
define('LIGHTCAPTCHA_FIELD',  'lc_token'); // POST field name

Generating a secret key

Use any of these to generate a cryptographically secure key:

# Linux / macOS terminal
openssl rand -hex 32

# PHP
bin2hex(random_bytes(32));

# Python
import secrets; secrets.token_hex(32)

# Node.js
require('crypto').randomBytes(32).toString('hex')

JavaScript widget

The widget is a single vanilla JS file with no dependencies. Include it once on any page that has a LightCaptcha element, and it auto-initializes.

Data attributes

The simplest integration — configure everything via HTML attributes:

<div data-lightcaptcha
     data-lc-verify="/lightcaptcha-verify.php"
     data-lc-images="/lightcaptcha/images"
     data-lc-theme="auto"
     data-lc-threshold="strict"
     data-lc-token="lc_token">
</div>
AttributeValuesDefaultDescription
data-lc-verifyURL path./lightcaptcha-verify.phpEndpoint the widget POSTs behavioral data to
data-lc-imagesURL path./lightcaptcha/imagesPath to puzzle image folder
data-lc-themeauto / light / darkautoWidget color theme
data-lc-thresholdstrict / relaxedstrictBehavioral scoring sensitivity
data-lc-tokenstringlc_tokenName of the hidden token field

Manual initialization

For more control, initialize the widget in JavaScript:

const captcha = new LightCaptcha('#my-captcha', {
  verifyEndpoint:  '/lightcaptcha-verify.php',
  imagesPath:      '/lightcaptcha/images',
  theme:           'auto',    // 'auto' | 'light' | 'dark'
  threshold:       'strict', // 'strict' | 'relaxed'
  tokenField:      'lc_token',
  labels: {
    instruction: 'Drag the puzzle piece into position',
    verifying:   'Verifying\u2026',
    success:     'Verified \u2713',
    retry:       'Try again'
  }
});

Widget API

MethodReturnsDescription
getToken()string | nullReturns the current signed token, or null if not yet verified
reset()voidResets the widget to its initial state (e.g. after a failed submission)

Custom images

Point data-lc-images (or imagesPath) at any folder of JPEG or PNG images. The widget randomly picks from the 10 bundled filenames by default. To use your own images, serve them with the same filenames, or extend the _getImages() method in lightcaptcha.js.

Images should be at least 300×160px for best results. High-contrast photos work better than flat illustrations — the jigsaw piece needs to be visually distinct from its surroundings.

Theming the widget

The widget supports three themes set via data-lc-theme or the theme config option:

To change the widget's colors, edit the CSS variables at the top of lightcaptcha.js in the _injectStyles method.

PHP library

Include lightcaptcha.php after defining your constants. The library exposes a single static class.

LightCaptcha::check()

Reads $_POST['lc_token'] automatically and returns true if valid.

if (!LightCaptcha::check()) {
    http_response_code(400);
    die('Captcha check failed.');
}

LightCaptcha::verify($token)

Verify a token string explicitly.

$token = $_POST['lc_token'] ?? '';
$ok    = LightCaptcha::verify($token); // bool

LightCaptcha::issue()

Issue a signed token. Used inside lightcaptcha-verify.php — you don't normally call this directly.

$token = LightCaptcha::issue();
echo json_encode(['token' => $token]);

Python library

The Python library works with any framework — Flask, Django, FastAPI, or plain WSGI.

Setup

from lightcaptcha import LightCaptcha

lc = LightCaptcha(
    secret='your-secret-key',
    ttl=600,       # optional, default 600
    field='lc_token' # optional, default 'lc_token'
)

Flask example

@app.route('/lightcaptcha-verify', methods=['POST'])
def lc_verify():
    data    = request.get_json()
    elapsed = int(data.get('elapsed', 0))
    if elapsed < 500:
        return jsonify({'token': None})
    return jsonify({'token': lc.issue()})

@app.route('/submit', methods=['POST'])
def submit():
    if not lc.verify(request.form.get('lc_token')):
        return 'Captcha failed', 400

Node.js library

Uses Node's built-in crypto module — no npm dependencies required.

Setup

const LightCaptcha = require('./lightcaptcha/server/lightcaptcha');

const lc = new LightCaptcha({
    secret: process.env.LC_SECRET, // required
    ttl:    600,                     // optional
    field:  'lc_token'              // optional
});

Express example

app.post('/lightcaptcha-verify', (req, res) => {
    const { elapsed = 0 } = req.body;
    if (elapsed < 500) return res.json({ token: null });
    res.json({ token: lc.issue() });
});

app.post('/submit', (req, res) => {
    if (!lc.verify(req.body.lc_token))
        return res.status(400).json({ error: 'Captcha failed' });
    // process form...
});

Behavioral threshold

SettingWhat it requires to pass silently
strictMouse or touch movement with variance, at least 2 seconds on page, no linearly robotic movement patterns
relaxedAny interaction (mouse, touch, or keyboard), at least 500ms on page

Start with strict. Switch to relaxed only if you see false positives from real users — for example, on touch-only devices where users land and immediately tap a field.

Token lifetime

Tokens expire after 10 minutes by default. Adjust with:

// PHP
define('LIGHTCAPTCHA_TTL', 300); // 5 minutes

# Python
lc = LightCaptcha(secret='...', ttl=300)

// Node.js
const lc = new LightCaptcha({ secret: '...', ttl: 300 });

Security notes

Changelog

v1.0.0 — Initial release