Calenzy API
APIs/For your website

Captcha API

New

An 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 endpoints

When 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.

It proves the sender did the work a browser does, not that a person is there. Keep your own spam check (a hidden honeypot field, a word filter) behind it.

Add it to a form

  1. 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 altcha holding the answer.
  2. Check the answer on your server, before anything else in the form handler, with Check an answer. Refuse the form when verified is false.
  3. If our API cannot be reached, the form still arrives, with altcha_unavailable=1 instead of altcha. 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.
HTML
<!-- 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.

GET/v1/captcha/challenge

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).

Response
parametersobjectWhat the browser needs to solve the puzzle: algorithm, cost, keyLength, keyPrefix, keySignature, nonce, salt
parameters.expiresAtintegerWhen the puzzle stops being valid, as a Unix timestamp
signaturestringOur signature of the puzzle, so its answer can be checked once
Request
GETapi.calenzy.app/v1/captcha/challenge
No API key
Response · 200 OK
{
  "parameters": {
    "algorithm": "PBKDF2/SHA-256",
    "cost": 2000,
    "expiresAt": 1791515232,
    "keyLength": 32,
    "keyPrefix": "09fd8c63928172bf68f0e871316da65e",
    "keySignature": "e18f77ab31161588170b6bdc4dddac742dd4d2599548653b4535b5f255d38a72",
    "nonce": "cacd9a4352a7f071af6714f398212253",
    "salt": "edd6752b0ece78e379e7c93163757e60"
  },
  "signature": "66822ca44cf55fe7ed8869f6f6c26bfddd4d71d45d102827bfe6ec04bd7e5c37"
}
POST/v1/captcha/verify

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.

Headers
AuthorizationstringBearer YOUR_API_KEY. Optional for now; when you send it, it must be right
Body
payloadRequiredstringThe form's altcha field, as it arrived
hoststringYour website's address, such as www.example.com, kept with the answer
Response
statusstringsuccess, whether the answer is right or not
verifiedbooleantrue when the answer is right and was not used before
reasonstringOnly when verified is false: missing (no answer sent), malformed, expired, invalid (a wrong answer) or used (already checked once)
POSTapi.calenzy.app/v1/captcha/verify
{
  "payload": "eyJjaGFsbGVuZ2UiOnsicGFyYW1ldGVycyI6ey...",
  "host": "www.example.com"
}
Response · 200 OK
{
  "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
<?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;
}
?>