logo
captchaAPI

5-minute integration

Five steps: create a project, drop in the script, mark your form, verify on the server, and make every attestation single-use. If you already have a project, skip to step 2.

On WordPress? Do step 1 to get your keys, then install the captchaapi.eu plugin and skip the rest. It protects the login, registration, lost-password, and comment forms plus Contact Form 7 - activate it, paste your site key and secret key, and choose which forms to protect. No script tag, no markup, no backend code. Download the latest release.

1. Create a project and grab your keys

  1. Sign in and go to Projects.
  2. Click New Project, give it a name, and optionally add the domain(s) you'll embed the widget on. Leaving Allowed Domains empty means any origin can use the site key.
  3. On the project detail page you'll now see two keys:
    • Site Key — public. Goes into your HTML.
    • Secret Key — private. Shown once here after creation; goes into your backend config (not your HTML). You can re-reveal it later with password confirmation.

Copy the secret key somewhere durable (password manager, environment variable) before you navigate away. To reveal it again you'll need to enter your account password.

2. Include the script

Add one tag to your page, ideally just before </body>:

<script>window.CAPTCHA_SITE_KEY = 'YOUR_SITE_KEY';</script>
<script src="https://captchaapi.eu/captcha.js" defer></script>

The script auto-initialises every form that carries the data-captcha attribute. Nothing runs until the user interacts with that form.

3. Mark your form

Add data-captcha to the form, and optionally a [data-captcha-status] element to show the protection state to the user.

<form method="POST" action="/contact" data-captcha>
    <input name="email" type="email" required>
    <textarea name="message" required></textarea>

    <span data-captcha-status></span>

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

When the form submits, the widget inserts a hidden input named captcha_attestation. It contains a signed proof that the PoW was solved. You don't need to add it yourself.

4. Verify the attestation on your server

On Laravel? Skip steps 4 and 5 and install captchaapi/laravelcomposer require captchaapi/laravel gives you the validation rule (replay protection included), Blade component, and Livewire trait pre-wired. See the Laravel section in backend examples.

Put your secret key(s) in an environment variable — never in code or client-side HTML. CAPTCHA_SECRET_KEYS is a comma-separated list so you can deploy a new key alongside the current one during rotation:

CAPTCHA_SECRET_KEYS=your_secret_here
# During rotation, temporarily:
# CAPTCHA_SECRET_KEYS=your_current_secret,your_pending_secret

On the endpoint that receives the form, verify the attestation's HMAC locally. No HTTP call to captchaapi.eu is needed.

<?php

/**
 * @param list<string> $secrets Accept any key in this list.
 */
function verifyCaptcha(string $attestation, array $secrets, string $expectedSiteKey): bool
{
    if (! str_contains($attestation, '.')) return false;

    [$payloadB64, $sigB64] = explode('.', $attestation, 2);

    $actual = base64_decode(strtr($sigB64, '-_', '+/'), strict: true);
    if ($actual === false) return false;

    $matched = false;
    foreach ($secrets as $secret) {
        $expected = hash_hmac('sha256', $payloadB64, $secret, binary: true);
        if (hash_equals($expected, $actual)) {
            $matched = true;
            break;
        }
    }
    if (! $matched) return false;

    $rawPayload = base64_decode(strtr($payloadB64, '-_', '+/'), strict: true);
    if ($rawPayload === false) return false;

    $payload = json_decode($rawPayload, true);
    if (! is_array($payload))                         return false;
    if (($payload['exp'] ?? 0) < time())               return false;
    if (($payload['sk']  ?? '') !== $expectedSiteKey)  return false;

    return true;
}

$secrets = array_filter(array_map('trim', explode(',', getenv('CAPTCHA_SECRET_KEYS') ?: '')));

$ok = verifyCaptcha(
    $_POST['captcha_attestation'] ?? '',
    $secrets,
    'YOUR_SITE_KEY',
);

5. Add replay protection (required for production)

Make every attestation single-use to close the replay window. The HMAC + exp check above proves the attestation is genuine and fresh — it doesn't prove it hasn't been used before. Without this last step, a bot that solves one PoW can re-submit the same attestation many times within its TTL. One atomic cache call closes that window:

// After verifyCaptcha() returns true, claim the jti as single-use.
// Assumes $redis is a connected client (e.g. new Redis(), or pulled from your DI container).
[$payloadB64] = explode('.', $_POST['captcha_attestation'] ?? '', 2);
$payload = json_decode(base64_decode(strtr($payloadB64, '-_', '+/')) ?: '', true) ?? [];
$jti = $payload['jti'] ?? '';
$ttl = max(1, ($payload['exp'] ?? 0) - time());

if ($jti === '' || ! $redis->set("captcha:jti:{$jti}", 1, ['NX', 'EX' => $ttl])) {
    // Already used or malformed — reject this submission.
    return false;
}

The TTL on the cache key matches the attestation's remaining lifetime — once the attestation expires, the jti record vanishes with it. Redis does the cleanup; you don't carry growing state.

Same atomic pattern in every language — Cache::add in Laravel (Redis driver), SET NX in phpredis, nx=True in redis-py. See backend examples for the full set in seven languages. Local HMAC verification means there's no central state for us to dedup against; the jti cache lives on your side, by design.

That's it. Submissions with a missing or invalid attestation, or with a replayed jti, fail the check — reject them. See Backend examples for ready-made snippets in Laravel, plain PHP, Node.js, Python, Go, Ruby, and Java.

Tip: the payload's ol field is true when the project is past its monthly request quota. Verification still succeeds (visitors continue to be served on the same baseline difficulty curve as in-quota traffic) so your users aren't punished — it's a signal to upgrade.

Next: full API reference · widget configuration · backend examples.