Errors

Every error response is JSON with a stable error code field. Always check the HTTP status first; the error field disambiguates same-status errors.

Shape

{
  "error": "<machine-readable code>",
  "detail": <optional, varies by error>,
  "retryAfterSec": <optional, present on 429>
}

Error catalog

StatusCodeWhen
400invalid_requestThe request body failed schema validation (bad URL, missing field, wrong type).
403turnstile_failedThe Turnstile token did not validate against the server-side secret.
404not_foundNo scan or skill with that ID/name exists.
429rate_limitedYou hit the 10-scans-per-24h anonymous cap. Includes retryAfterSec.
500turnstile_misconfiguredServer-side Turnstile secret is missing. Operator issue, not your problem — file a bug.

invalid_request detail shape

When validation fails, detail is the zod-format error structure showing which field broke:

{
  "error": "invalid_request",
  "detail": {
    "_errors": [],
    "url": {
      "_errors": ["Invalid url"]
    }
  }
}

Scan-level failures

If POST /scan accepted the job but the worker failed (timeout, 404 on the upstream URL, invalid markdown), the scan row will land at status="failed" with errorMessage populated. Check via GET /scan/:id — the HTTP request itself returns 200 with the failed scan record.