Skip to content

CAPTCHA handling

TEBHOS requires a CAPTCHA solution on a small set of abuse-sensitive operations. The production instance uses hCaptcha. A client reads the provider and its site key from instance discovery, renders the provider's widget, and sends the solution in a request header.

Discovering the provider

GET /.well-known/tebhos includes a captcha object:

Field Type Description
provider string hcaptcha, turnstile or none
hcaptcha_site_key ?string The hCaptcha site key
turnstile_site_key ?string The Turnstile site key, when one is configured
{
  "captcha": {
    "provider": "hcaptcha",
    "hcaptcha_site_key": "00000000-0000-0000-0000-000000000000",
    "turnstile_site_key": null
  }
}

hcaptcha is the provider TEBHOS uses. turnstile is supported by the API for instances configured with it. none means the widget is not configured; clients should still handle CAPTCHA_REQUIRED as described below instead of assuming that no challenge will ever be sent.

Gated operations

Method Route Operation
POST /auth/login Log in with a password
POST /auth/send-verification Send the registration verification code
POST /oauth2/applications Create an application

Registration itself is not gated. POST /auth/register requires the verification code that only a CAPTCHA-checked POST /auth/send-verification can deliver, so the CAPTCHA on the verification step protects registration as well.

On login and on sending the verification code, the CAPTCHA check runs before the operation's rate limits and before any account lookup. Application creation checks its rate limit first and the CAPTCHA second.

Request headers

Header Description
X-Captcha-Token The solution returned by the provider widget
X-Captcha-Type? The provider that produced the solution: hcaptcha or turnstile. Omitted or any other value means hcaptcha

An absent or empty X-Captcha-Token on a gated operation returns 400 CAPTCHA_REQUIRED.

The retry handshake

  1. Send the request without CAPTCHA headers.
  2. On 400 CAPTCHA_REQUIRED, show the provider's widget with the site key from discovery and obtain a solution.
  3. Retry the same request with X-Captcha-Token set to the solution and, optionally, X-Captcha-Type.

An accepted solution lets the operation proceed. A rejected solution returns 400 INVALID_CAPTCHA.

curl -X POST https://tebhos.com/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -H "X-Captcha-Token: $HCAPTCHA_RESPONSE" \
  -H "X-Captcha-Type: hcaptcha" \
  -d '{"login": "[email protected]", "password": "correct horse battery staple"}'

A solution is single-use

The provider treats an already redeemed solution as invalid. After INVALID_CAPTCHA, obtain a new solution before retrying; never replay the previous X-Captcha-Token.

Provider verification

TEBHOS verifies the solution with the provider, passing the client IP address. A rejected solution, a provider error and a verification timeout (10 seconds) all return 400 INVALID_CAPTCHA; the response does not distinguish them.

Naming a provider in X-Captcha-Type that the instance has no secret for is a server configuration error and returns 500 INTERNAL_SERVER_ERROR. Omit the header, or send hcaptcha, unless discovery reports turnstile.

Error codes

Code Status Description
CAPTCHA_REQUIRED 400 The operation is gated and the request has no solution
INVALID_CAPTCHA 400 The provider rejected the solution, or verification could not be completed

Both use the ordinary error response.