Errors
Every error response is JSON with an error string and a matching HTTP status code:
HTTP 400
{ "error": "`url` is required" }The API never throws raw stack traces to the client and never returns a 500 for an expected condition (a bad URL, a page that wouldn't load, a missing key). Those map to specific 4xx codes below.
Status codes
| Status | Meaning | Typical cause |
|---|---|---|
200 | OK | Request succeeded |
201 | Created | A key was provisioned (POST /v1/keys/provision) or created |
202 | Accepted | An async crawl job was queued (POST /v1/crawl) |
400 | Bad request | Missing/invalid field, malformed JSON body, invalid URL, or an SSRF-blocked URL |
401 | Unauthorized | Missing, invalid, or revoked API key |
402 | Payment required | A pay-per-call (x402) route was hit without payment, or credits are exhausted |
404 | Not found | Unknown job/resource id, or a cache-only scrape (minAge) with no cached copy |
422 | Unprocessable | The URL was valid but the page could not be fetched or parsed |
429 | Too many requests | Rate limit exceeded for your tier (rate limits) |
503 | Service unavailable | A required backend isn't configured (e.g. crawl without a job queue, or auth backend down) |
Common error bodies
| Status | Body | When |
|---|---|---|
| 400 | { "error": "Invalid JSON body" } | Request body isn't valid JSON |
| 400 | { "error": "`url` is required" } | A required field is missing |
| 400 | { "error": "Invalid URL" } | The url field isn't a valid http(s) URL |
| 400 | { "ok": false, "error": "Blocked URL: …" } | URL rejected by the SSRF guard (private/internal hosts) |
| 401 | { "error": "API key required. Pass via x-api-key header." } | No key header present |
| 401 | { "error": "Invalid or revoked API key." } | Key not found, inactive, or revoked |
| 422 | { "ok": false, "error": "…" } | Page couldn't be fetched/parsed (e.g. /v1/scrape failure) |
| 429 | { "error": "Rate limit exceeded", "limit": 200, "tier": "pro", "retryAfterSeconds": 60 } | Too many requests in the sliding window |
| 503 | { "error": "Async crawl unavailable: job queue not configured (REDIS_URL). Crawl was not started." } | Crawl with no worker queue configured |
| 503 | { "error": "Auth backend not configured." } | The auth/database backend isn't reachable |
Rate-limit errors
A 429 additionally sets response headers so you can back off precisely:
| Header | Description |
|---|---|
X-RateLimit-Limit | Your tier's per-minute ceiling |
X-RateLimit-Remaining | Requests left in the current window |
Retry-After | Seconds to wait before retrying (60) |
Note that X-RateLimit-Limit and X-RateLimit-Remaining are returned on every request, not only on a 429, so you can throttle proactively. See rate limits.
Handling errors
- Always check the HTTP status first, then read
error(andokwhere present) for the human-readable reason. - On
429, honorRetry-After(or theX-RateLimit-Remainingheader) before retrying. - On
503, the failure is a configuration/backend issue, not your request, retry with backoff. - A
422from/v1/scrapemeans the URL was reachable as input but the page itself didn't yield content; it is not a quota or auth problem.
| 502 | map_fetch_failed | Map could not fetch the site | | 503 | search_unavailable | No search backend completed | | 504 | map_timeout | Map exceeded time budget with no links |