A free captcha service, priorizing user privacy
GET /captcha -> returns X-Captcha-Id, X-PoW-Prefix, X-PoW-Difficulty, and X-Captcha-TTL.
The user types the characters. The browser finds a nonce such that SHA256(prefix + nonce) has the required leading zero bits, then posts { id, answer, nonce } to POST /captcha/solve. With ?visualDifficulty=0 the image is blank and only the proof of work is checked, so answer can be omitted. The server replies with { authCode, captchaId, ts, algorithm, powDifficulty, visualDifficulty }.
Your backend posts { captchaId, authCode, ts, algorithm, powDifficulty, visualDifficulty } to POST /captcha/verify to confirm the token. Omitting algorithm means sha256.
Add pow=argon2id to request a memory-hard challenge, for example GET /captcha?pow=argon2id&visualDifficulty=0&difficulty=7&memoryCost=32768&timeCost=2. The server clamps difficulty to 1-12 bits, memoryCost to 8192-65536 KiB and timeCost to 1-4. Parallelism is always 1 and the hash length always 32 bytes. The response adds X-PoW-Algorithm: argon2id and X-PoW-Params: m=32768,t=2,p=1,len=32. At visual difficulty 0 it is a 204 with headers only.
The client searches for a decimal nonce such that argon2id(password = nonce, salt = prefix as hex text, m, t, p, len = 32) has at least difficulty leading zero bits. Each captcha id gets exactly one solve attempt. The solve response and POST /captcha/verify also carry memoryCost, timeCost, parallelism and hashLength, which your backend must forward to verify. Verify returns algorithm and every parameter, so check algorithm === "argon2id" and that each parameter meets your minimum.
Five new captchas per IP per 5 seconds on GET /captcha and five solves per IP per 10 seconds on POST /captcha/solve. Blocked responses include a Retry-After header. When the server is busy verifying Argon2id solves, POST /captcha/solve returns 503 with Retry-After and the captcha stays valid.