Skip to content

Developer documentation

Add a privacy-first captcha in minutes

Drop in a widget, verify one token on your server, done. No tracking, no image puzzles, EU-hosted proof-of-work. Works with any language or framework.

Quickstart

Two steps: embed the widget in your form, then verify the token on your server when the form is submitted. You need a sitekey (public) and a secret (server-only), both from your InfoPeak dashboard.

1. Add the widget to your form

<form action="/signup" method="POST">
  <input type="email" name="email" required>

  <!-- InfoPeak Captcha widget -->
  <div class="infopeak-captcha" data-sitekey="YOUR_SITEKEY"></div>

  <button type="submit">Sign up</button>
</form>

<!-- Load once, near the end of <body> -->
<script src="https://captcha.infopeak.io/infopeak-captcha.js" defer></script>

The widget solves automatically in the background the moment the visitor starts interacting with your form - there is no checkbox to click. When it finishes it adds a hidden field infopeak-captcha-response to the surrounding <form>, submitted with your form like any other field.

2. Verify the token on your server

<?php
$token = $_POST['infopeak-captcha-response'] ?? '';

$res = file_get_contents('https://captcha.infopeak.io/api/v1/pow/siteverify', false, stream_context_create([
    'http' => [
        'method'  => 'POST',
        'header'  => 'Content-Type: application/json',
        'content' => json_encode([
            'sitekey' => 'YOUR_SITEKEY',
            'secret'  => 'YOUR_SECRET',
            'token'   => $token,
        ]),
    ],
]));

$ok = json_decode($res, true)['valid'] ?? false;
if (!$ok) {
    http_response_code(400);
    exit('Captcha verification failed');
}
// ... proceed with signup

That is the whole integration. Everything below is reference detail.

How it works

InfoPeak Captcha uses proof-of-work instead of tracking or image puzzles. The visitor's browser solves a small computational challenge in the background - invisible, no clicking traffic lights. Bots pay a CPU cost that makes mass abuse expensive; humans wait a fraction of a second.

  1. 1
    Challenge. The widget requests a challenge for your sitekey (/pow/config).
  2. 2
    Solve. The browser solves the proof-of-work in a Web Worker - no user interaction.
  3. 3
    Token. The solved work is exchanged for a single-use token (/pow/verify).
  4. 4
    Verify. Your server confirms the token with your secret (/pow/siteverify).
Steps 1-3 happen automatically inside the widget. You only implement step 4.

Widget reference

Any element with class infopeak-captcha is turned into a widget on page load. It solves automatically when the visitor first interacts with the surrounding form - no click required. If there is no surrounding <form>, it solves on load.

Attribute Required Description
data-sitekey Yes Your public sitekey. The widget shows an error if missing.
data-lang No Two-letter language code (e.g. da, de). Defaults to the page <html lang>, then the browser language. 16 languages supported.
data-api No Override the API origin. Defaults to where the script was loaded from (https://captcha.infopeak.io). You rarely need this.

Reading the token

On success the widget injects a hidden input into the closest <form> (or into the widget element if there is no form):

<input type="hidden" name="infopeak-captcha-response" value="THE_TOKEN">

A solved widget also carries the CSS class is-done, which you can use to enable a submit button. Tokens are single-use and short-lived - verify them server-side promptly.

Read the token when the form is submitted, not when the page loads. The widget quietly re-issues it in the background before it can expire, so the field always holds a valid token no matter how long the visitor spends on the page.

Languages

The widget picks its language in this order: the data-lang attribute, then the page's <html lang>, then the browser's language. Anything unrecognised falls back to English, and nb is treated as no.

<div class="infopeak-captcha" data-sitekey="YOUR_SITEKEY" data-lang="da"></div>

Supported codes

cs Czech da Danish de German en English
es Spanish et Estonian fi Finnish fr French
it Italian nl Dutch no Norwegian pl Polish
pt Portuguese ro Romanian sv Swedish uk Ukrainian

Accessibility

The usual CAPTCHA accessibility problems come from the challenge itself: distorted text, image grids, audio fallbacks and time limits. InfoPeak Captcha has none of them, so the barrier never gets built in the first place.

  • Nothing to solve. Verification runs by itself in the background. Most visitors never interact with the widget at all.
  • Real button semantics. The widget renders an actual <button> with an aria-label, so it stays reachable and operable by keyboard and screen reader.
  • State is announced. On success the button is marked aria-checked="true" and the visible label changes, so the result is conveyed as text rather than colour alone.
  • Translated status text. The idle, verifying, verified and error labels ship in all 16 supported languages.
  • No time pressure. There is no countdown and no penalty for taking your time.
Keep the widget inside the <form> it protects. That way the verification starts as soon as the visitor engages with the form, and assistive technology reads it in the right place in the flow.

Server-side verification

Always verify the token on your server before trusting a submission. Send your sitekey, secret and the submitted token to /api/v1/pow/siteverify. A valid: true response means the token was genuine and had not been used before. The same code that protects infopeak.io's own signup is shown here for PHP.

function infopeak_captcha_verify(string $token): bool {
    $ch = curl_init('https://captcha.infopeak.io/api/v1/pow/siteverify');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
        CURLOPT_POSTFIELDS     => json_encode([
            'sitekey' => 'YOUR_SITEKEY',
            'secret'  => 'YOUR_SECRET',
            'token'   => $token,
        ]),
        CURLOPT_TIMEOUT        => 8,
    ]);
    $resp = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    return $resp !== false && $code === 200
        && (json_decode($resp, true)['valid'] ?? false) === true;
}
async function infopeakCaptchaVerify(token) {
  const res = await fetch('https://captcha.infopeak.io/api/v1/pow/siteverify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      sitekey: 'YOUR_SITEKEY',
      secret: 'YOUR_SECRET',
      token,
    }),
  });
  if (!res.ok) return false;
  const data = await res.json();
  return data.valid === true;
}
import requests

def infopeak_captcha_verify(token: str) -> bool:
    r = requests.post(
        "https://captcha.infopeak.io/api/v1/pow/siteverify",
        json={
            "sitekey": "YOUR_SITEKEY",
            "secret": "YOUR_SECRET",
            "token": token,
        },
        timeout=8,
    )
    return r.status_code == 200 and r.json().get("valid") is True
func InfopeakCaptchaVerify(token string) bool {
    body, _ := json.Marshal(map[string]string{
        "sitekey": "YOUR_SITEKEY",
        "secret":  "YOUR_SECRET",
        "token":   token,
    })
    resp, err := http.Post(
        "https://captcha.infopeak.io/api/v1/pow/siteverify",
        "application/json", bytes.NewReader(body),
    )
    if err != nil {
        return false
    }
    defer resp.Body.Close()
    if resp.StatusCode != 200 {
        return false
    }
    var out struct{ Valid bool `json:"valid"` }
    json.NewDecoder(resp.Body).Decode(&out)
    return out.Valid
}
require 'net/http'
require 'json'

def infopeak_captcha_verify(token)
  uri = URI('https://captcha.infopeak.io/api/v1/pow/siteverify')
  res = Net::HTTP.post(uri, {
    sitekey: 'YOUR_SITEKEY',
    secret:  'YOUR_SECRET',
    token:   token
  }.to_json, 'Content-Type' => 'application/json')

  res.code == '200' && JSON.parse(res.body)['valid'] == true
end

API reference

Base URL https://captcha.infopeak.io. All endpoints accept and return JSON.

/pow/config and /pow/verify are called by the widget in the browser. In normal use you only call /pow/siteverify from your server. The first two are documented for completeness and custom integrations.
POST/api/v1/pow/siteverify

Confirm a token server-side. Your secret authenticates the call. This is the only endpoint you normally implement.

Request

  • sitekey - string - your public sitekey
  • secret - string - your server-side secret
  • token - string - the value from the infopeak-captcha-response field

Response

  • valid - boolean - true if the token was genuine and unused
  • over_limit - boolean - true if the sitekey is over its monthly quota (see Quotas)
curl -X POST https://captcha.infopeak.io/api/v1/pow/siteverify \
  -H "Content-Type: application/json" \
  -d '{"sitekey":"YOUR_SITEKEY","secret":"YOUR_SECRET","token":"THE_TOKEN"}'

# => {"valid":true,"over_limit":false}
POST/api/v1/pow/config

Called by the widget. Issues a proof-of-work challenge for a sitekey.

Request

  • sitekey - string - your public sitekey

Response

  • challenge - string - the challenge to solve
  • salt - string - salt for the work function
  • difficulty - number - target work factor
POST/api/v1/pow/verify

Called by the widget. Exchanges solved work for a single-use token.

Request

  • sitekey - string - your public sitekey
  • challenge - string - the challenge from /pow/config
  • nonce - number - the solution found by the browser

Response

  • token - string - single-use verification token

Quotas & pricing

Every sitekey has a monthly verification quota based on its plan. Verification never hard-fails when you go over - instead over_limit becomes true and the proof-of-work difficulty ramps up, so legitimate traffic keeps working while abuse gets more expensive. Treat over_limit: true as a signal to upgrade.

Plan Verifications / month
Free 10,000
Pro 500,000
Business 5,000,000
Enterprise Unlimited
See plan prices

Testing

Use these public test credentials to build and test your integration before creating your own key. They work on any domain, never count against a quota, and run the real proof-of-work flow. Never use them in production.

Test sitekey ipk_test_sitekey
Test secret ipk_test_secret
The test sitekey is unbound and always available - handy for local development and CI. Swap in your own sitekey and secret from the dashboard before you ship.

Domain binding

Your sitekey is public - it ships in your HTML, and that is fine. Your secret is not and must only ever live on your server. Never expose the secret in client-side code.

To stop others from embedding your sitekey on their own pages, bind it to one or more domains in your dashboard. A bound key only issues challenges when the request comes from an allowed domain; unbound keys work everywhere. Bind your keys before going to production.

Content Security Policy

If your site sends a Content-Security-Policy header, allow these three directives. This is the most common reason a correct integration still shows nothing.

script-src   https://captcha.infopeak.io
connect-src  https://captcha.infopeak.io
worker-src   blob:
  • script-src Loads the widget script itself.
  • connect-src The widget fetches the challenge API, the worker source and the WebAssembly engine from this origin.
  • worker-src blob: The proof-of-work runs in a Web Worker created from a same-origin blob, because browsers block workers loaded straight from another domain.

Strict style-src

The loader injects its own inline <style> so the two-line embed works without a separate stylesheet. If your policy forbids inline styles, load the stylesheet yourself and give it the id ipk-captcha-css - the loader sees it and skips the injection.

<link rel="stylesheet" id="ipk-captcha-css"
      href="https://captcha.infopeak.io/infopeak-captcha.css">

Then add https://captcha.infopeak.io to your style-src as well.

Migrating

The shape is familiar in both cases, so migration is mostly find-and-replace.

From reCAPTCHA

reCAPTCHA InfoPeak Captcha
<div class="g-recaptcha" data-sitekey> <div class="infopeak-captcha" data-sitekey>
https://www.google.com/recaptcha/api.js https://captcha.infopeak.io/infopeak-captcha.js
POST field g-recaptcha-response POST field infopeak-captcha-response
/recaptcha/api/siteverify /api/v1/pow/siteverify
secret + response params sitekey + secret + token (JSON)
success in response valid in response

From hCaptcha

hCaptcha InfoPeak Captcha
<div class="h-captcha" data-sitekey> <div class="infopeak-captcha" data-sitekey>
https://js.hcaptcha.com/1/api.js https://captcha.infopeak.io/infopeak-captcha.js
POST field h-captcha-response POST field infopeak-captcha-response
https://api.hcaptcha.com/siteverify /api/v1/pow/siteverify
Form-encoded secret + response JSON sitekey + secret + token
success in response valid in response

Errors

Errors return a non-2xx status and a JSON body {"error": "..."}.

Status Meaning Fix
400 Challenge not found or invalid work The token is stale, malformed, or already used. Have the visitor solve the captcha again.
401 Wrong secret The secret does not match the sitekey. Check your server configuration.
403 Domain not allowed The request origin is not in the sitekey's allowed domains. Add it in your dashboard.
404 Unknown sitekey The sitekey does not exist. Check for typos.

Ready to ship it?

Create a free sitekey and protect your first form in under five minutes.

Get your free key