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 signupThat 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
Challenge. The widget requests a challenge for your sitekey (
/pow/config). -
2
Solve. The browser solves the proof-of-work in a Web Worker - no user interaction.
-
3
Token. The solved work is exchanged for a single-use token (
/pow/verify). -
4
Verify. Your server confirms the token with your secret (
/pow/siteverify).
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.
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 anaria-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.
<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 Truefunc 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
endAPI 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.
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}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
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 |
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 |
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-srcLoads the widget script itself. -
connect-srcThe 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