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
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | The request body failed schema validation (bad URL, missing field, wrong type). |
| 403 | turnstile_failed | The Turnstile token did not validate against the server-side secret. |
| 404 | not_found | No scan or skill with that ID/name exists. |
| 429 | rate_limited | You hit the 10-scans-per-24h anonymous cap. Includes retryAfterSec. |
| 500 | turnstile_misconfigured | Server-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.