Each website has an ordered chain of captcha providers. The script tries the first one; if it cannot load in the browser — ad blocker, corporate policy, network — it moves on to the next ALTCHA link of the chain, the only one it may choose on its own. The token obtained is sent with the submission:
X-Captcha-Provider: recaptcha_v3:12
X-Captcha-Token: 03AGdBq…
X-Captcha-Provider is the chain link key, in the form type:identifier. The server
verifies the token against the designated link, and that link only.
| Type | Token sent |
|---|---|
altcha |
Base64 payload produced by the ALTCHA widget |
recaptcha_v3 |
Token from grecaptcha.execute(siteKey, {action: 'submit'}) |
recaptcha_enterprise |
Token from grecaptcha.enterprise.execute(siteKey, {action: 'submit'}) |
turnstile |
Token rendered by the Turnstile widget |
hcaptcha |
Token rendered by the invisible hCaptcha widget |
The default captcha
A website with an empty chain is protected by ALTCHA, self-hosted by the platform. Its
chain link key is altcha, with no identifier:
X-Captcha-Provider: altcha
X-Captcha-Token: eyJhbGdvcml0aG0iOi…
No website therefore accepts a submission without a captcha, unless the platform has explicitly disabled this default.
Which link the server accepts
The chain is ordered on purpose: a caller does not get to pick the least demanding link, nor to aim straight at the last one to trigger the pass-through described below. Only three links are accepted:
- the first link of the chain;
- the link designated by a fallback token, which the server signs once it has itself observed the failure of the previous link;
- any ALTCHA link, verified locally by the platform — so that a visitor whose browser blocks a third party's script can still get through.
Any other link — a Turnstile, hCaptcha or reCAPTCHA link that is in the chain but was
neither reached in order nor designated — is refused with
403 {"error": "captcha_provider_not_allowed"}, as is a key that is not in the chain at
all. A non-designated ALTCHA link is verified, but it can neither open a fallback nor
benefit from the pass-through: on it, an unreachable provider is a captcha_failed.
A token longer than 4 096 characters is refused with captcha_failed, without any
call to the provider.
The fallback
The fallback is triggered by a technical failure observed by the server, never by a rejected token, and never by a failure the browser merely declares:
| Situation | Server response |
|---|---|
| No token although the chain expects one | 403 {"error": "captcha_required"} |
| Token rejected by the provider | 403 {"error": "captcha_failed"} |
| Link not accepted for this submission | 403 {"error": "captcha_provider_not_allowed"} |
| Provider unreachable, a next link exists | 403 {"error": "captcha_fallback", "next": "altcha:7", "fallback_token": "1789459320.4f3b…"} |
| Provider unreachable, last link | the submission goes through, the incident is logged |
A 4xx from the provider is a refusal, including a 429 — an exhausted quota is not
an outage. Only a 5xx, or no answer at all within three seconds, counts as a failure and
opens a fallback.
The fallback token
fallback_token is the server's signed authorization to use the next link. It is tied
to the website and to that one link, and it is valid for two minutes.
Send it back in the body of the replayed submission, in the captcha_fallback field —
not in a header, which the browser's CORS preflight would reject:
{
"name": "Smith",
"email": "[email protected]",
"captcha_fallback": "1789459320.4f3b…"
}
captcha_fallback is a technical field: it is never recorded with the request.
Without it, the next link is only accepted if it is an ALTCHA link — and even then it
opens no further fallback. So: on captcha_fallback, resume solving at the next link,
carry the fallback_token with the replay, and do so once per link, no more.
When the last link is unreachable
When the server observes the failure of the last link of the chain, and the caller was entitled to that link — it was the first, or it was designated by a fallback token — the submission is accepted without verification and the incident is logged. A provider's outage must not cost every client their leads. A link reached any other way never gets this treatment.
ALTCHA
ALTCHA is a proof of work solved in the browser: nothing is sent to a third party. It has two dedicated endpoints.
GET /api/captcha/altcha.js
The ALTCHA widget, served by the platform.
| Check | Value |
|---|---|
| Authentication | none |
| Cache | public, max-age=31536000, immutable — the URL changes with every release |
The script loads it with import() when it needs it. Responds 404 if the platform has
not built its assets.
GET /api/websites/{website}/captcha/altcha/challenge
The challenge to solve, tied to the website requesting it.
| Check | Value |
|---|---|
| Authentication | none; the Origin is authoritative |
| Origin | checked |
| Rate limiting | api-captcha-challenge, 30 per minute per IP |
| Cache | Cache-Control: no-store |
Response 200
{
"algorithm": "SHA-256",
"challenge": "9f2c…",
"salt": "b71d…",
"signature": "40ae…",
"maxnumber": 50000
}
Errors
| Code | Body | Cause |
|---|---|---|
404 |
{"error": "altcha_not_enabled"} |
The website does not have ALTCHA in its chain |
403 |
{"error": "origin_not_allowed"} |
The Origin does not match |
ALTCHA relies on the Web Crypto API, which only exists in a secure context. A page served over plain HTTP cannot solve the challenge: the chain then moves on to the next link. Over HTTPS, the issue does not arise.
A challenge cannot be replayed
An ALTCHA response that has already been accepted is rejected the second time it is presented. Request a new challenge for each submission.
API