Captcha API
NewAn invisible check that a form was sent from a browser, not by a spam script. No cookie, no Google.
/v1/captchaNot counted in your quota2 endpointsWhen a visitor sends your form, their browser solves a small puzzle in about half a second, without seeing anything. Your server then asks us whether the answer is right. Nothing is stored in the browser and nothing goes to a third party, while a spam script has to do that work again for every message it sends.
The puzzle is fetched by the visitor's browser, so it needs no key. Check the answer from your server, with your API key: the key is optional for now, and lets us show you the checks your website makes. Captcha calls are never counted in your quota.
Add it to a form
- Load our script once in the page, and put the widget in each form (the HTML below). When the form is sent, it adds a field named
altchaholding the answer. - Check the answer on your server, before anything else in the form handler, with Check an answer. Refuse the form when
verifiedisfalse. - If our API cannot be reached, the form still arrives, with
altcha_unavailable=1instead ofaltcha. Ask for a New puzzle from your server: if it answers, the browser's claim was false, so refuse the form; if it does not, let the message on to your own spam check. Your forms must keep working when we cannot answer.
<!-- Once in the page --> <script type="module" src="https://api.calenzy.app/v1/captcha/captcha.js"></script> <!-- Inside each form --> <form method="post" action="/contact"> ... <altcha-widget challenge="https://api.calenzy.app/v1/captcha/challenge" auto="onsubmit" display="invisible"></altcha-widget> <button type="submit">Send</button> </form>
It pairs with the IP API: when a lookup refuses a visitor, show them a page with this captcha, and when they solve it, vouch for them with Let a visitor through.
New puzzle
A puzzle for the widget, valid 15 minutes. The visitor's browser fetches it: no key.
The widget reads it for you: you only call it yourself to find out whether our API answers (step 3 above).
| parametersobject | What the browser needs to solve the puzzle: algorithm, cost, keyLength, keyPrefix, keySignature, nonce, salt |
| parameters.expiresAtinteger | When the puzzle stops being valid, as a Unix timestamp |
| signaturestring | Our signature of the puzzle, so its answer can be checked once |
curl "https://api.calenzy.app/v1/captcha/challenge"
{ "parameters": { "algorithm": "PBKDF2/SHA-256", "cost": 2000, "expiresAt": 1791515232, "keyLength": 32, "keyPrefix": "09fd8c63928172bf68f0e871316da65e", "keySignature": "e18f77ab31161588170b6bdc4dddac742dd4d2599548653b4535b5f255d38a72", "nonce": "cacd9a4352a7f071af6714f398212253", "salt": "edd6752b0ece78e379e7c93163757e60" }, "signature": "66822ca44cf55fe7ed8869f6f6c26bfddd4d71d45d102827bfe6ec04bd7e5c37" }
Check an answer
Whether the answer your form received is right, and still unused.
An answer is good once: checking the same answer again says used, so a captured form cannot be sent twice. Answers expire 15 minutes after the puzzle was made.
| Authorizationstring | Bearer YOUR_API_KEY. Optional for now; when you send it, it must be right |
| payloadRequiredstring | The form's altcha field, as it arrived |
| hoststring | Your website's address, such as www.example.com, kept with the answer |
| statusstring | success, whether the answer is right or not |
| verifiedboolean | true when the answer is right and was not used before |
| reasonstring | Only when verified is false: missing (no answer sent), malformed, expired, invalid (a wrong answer) or used (already checked once) |
{ "payload": "eyJjaGFsbGVuZ2UiOnsicGFyYW1ldGVycyI6ey...", "host": "www.example.com" }
curl -X POST "https://api.calenzy.app/v1/captcha/verify" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"payload": "eyJjaGFsbGVuZ2UiOnsicGFyYW1ldGVycyI6ey...",
"host": "www.example.com"
}'{ "payload": "eyJjaGFsbGVuZ2UiOnsicGFyYW1ldGVycyI6ey...", "host": "www.example.com" }
curl -X POST "https://api.calenzy.app/v1/captcha/verify" \
-H "Content-Type: application/json" \
-d '{
"payload": "eyJjaGFsbGVuZ2UiOnsicGFyYW1ldGVycyI6ey...",
"host": "www.example.com"
}'{ "status": "success", "verified": true }
Check it in PHP
Copy this function into your website and call it first in your form handler. It checks the answer with your key, gives up after 6 seconds, and follows the rule above when our API cannot be reached.
<?php /* Calenzy captcha, your website's side. In your form handler, before anything else: if (!calenzy_captcha_ok('YOUR_API_KEY')) { ...refuse the form... } When our API cannot be reached, the message goes on to your own spam check. A form that says the puzzle was unavailable while the API answers is refused. */ function calenzy_captcha_request(string $method, string $path, string $api_key, ?array $body = null): array { $headers = ['Content-Type: application/json']; if ($api_key !== '') { $headers[] = 'Authorization: Bearer ' . $api_key; } $ch = curl_init('https://api.calenzy.app/v1/captcha/' . $path); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 3, CURLOPT_TIMEOUT => 6, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers, ]); if ($body !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); } $out = curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); return [$code, $out === false ? null : json_decode((string) $out, true)]; } /* true = send the message on; false = refuse the form */ function calenzy_captcha_ok(string $api_key = ''): bool { $payload = (string) ($_POST['altcha'] ?? ''); if ($payload !== '') { [$code, $answer] = calenzy_captcha_request('POST', 'verify', $api_key, ['payload' => $payload, 'host' => strtolower((string) ($_SERVER['HTTP_HOST'] ?? ''))]); if ($code === 200 && is_array($answer)) { return !empty($answer['verified']); } return true; /* our API did not answer: your own spam check decides */ } if (!empty($_POST['altcha_unavailable'])) { [$code] = calenzy_captcha_request('GET', 'challenge', ''); return $code !== 200; /* the API answers, so "unavailable" was not true */ } return false; } ?>