Skip to Content

every error from the crawlbrulee api follows a consistent format. parse it once, handle everywhere.

error format

all error responses share this shape:

interface ApiError { name: string message: string details?: UsageAllocationDetails | TooManyRequestsDetails }

the most common error names are listed below. the message is a human-readable explanation. the details object is present only for usage_allocation_error and too_many_requests errors.

http status codes

statusmeaningwhen
200successrequest completed
202acceptedasync job queued
400bad requestvalidation failed (invalid url, bad params)
401unauthorizedmissing or invalid token
403forbiddenthe target site’s anti-bot protection blocked the request (scrape or map)
404not foundasync job doesn’t exist, or a resource you don’t have access to
408request timeoutthe request took too long to complete
415unsupported media typetarget content type isn’t supported for extraction
422unprocessable entitya screenshot-only request targeted a content type that can’t be screenshotted
429too many requestsrate limit or usage limit exceeded
499client closed requestclient disconnected before the request completed
500internal server errorsomething went wrong on our end
503service unavailablea transient failure on our end — retry the same request

error names

namehttp statusdescription
invalid_url400url failed validation
url_too_long400url exceeds 8,192 bytes
unsupported_url_schema400only http and https are supported
url_credentials_not_supported400urls with embedded credentials are rejected
unsupported_content415target content type not supported for extraction
unsupported_screenshot_output422the request asked only for a screenshot and the content type can’t be screenshotted (json, plain text, markdown, xml) — never billed. see screenshots
validation_error400request body failed validation
blocked_url400url is not allowed to be scraped
invalid_credentials401token missing, expired, or revoked
access_denied404no permission for this resource — returned as 404 to avoid revealing whether it exists
not_found404resource (e.g., async job) not found
too_many_requests429rate limit exceeded
usage_allocation_error429credit or concurrency limit hit
request_timeout408the request took too long to complete
client_closed_request499client disconnected before completion
scrape_error4xx / 5xxthe scrape itself failed. the response mirrors the target’s http status — a page that returns 404 surfaces as scrape_error with status 404, a 500 as 500
antibot_blocked403the target site’s anti-bot protection blocked the request (scrape or map)
too_many_redirects422the target site redirected the request in a loop, or through more hops than we follow (scrape or map). not a bad request; retrying rarely helps
page_too_large422the page’s html was too large to process (scrape). not a bad request, and terminal — the same url fails the same way, so don’t retry it; scrape a smaller page instead
job_failed400async job failed
internal_server_error500server-side failure
service_unavailable503a dependency we need was briefly unavailable. your request was fine — retry it unchanged

a 503 is not an auth problem. authentication failures that are genuinely about your token — unknown, expired, or revoked — always come back as 401 invalid_credentials. 503 means we couldn’t complete the check right now, so retry the same request with the same key rather than rotating it.

usage allocation errors

these errors mean you’ve hit a billing or concurrency limit. the response includes a details object with the specific reason and current usage numbers.

{ "name": "usage_allocation_error", "message": "Credit limit exceeded", "details": { "error_name": "usage_allocation_error", "reason": "credit_limit", "details": { "current_usage": 750, "current_reserved": 0, "max_credits": 750 } } }

reasons

reasonmeaning
credit_limityour organization has used all available credits for the current billing period. upgrade your plan or wait for the next cycle.
concurrency_limittoo many scrape jobs are running at the same time. wait for in-flight jobs to finish before submitting new ones.
duplicate_reservationthe same job already has an active credit reservation. this is an internal-consistency guard — retrying the request is usually safe.
internal_errorthe allocation check failed unexpectedly. returned as http 500 rather than 429 — treat it like any other server-side failure.

credits are reserved when a request starts and finalized on completion. failed requests release the reservation. successful requests are charged at the delivered engine and proxy; a fully cached result has a 0-credit base, and a newly produced screenshot-slice variant costs +1.

rate limit errors

when you exceed a rate limit, the response tells you exactly how long to wait and which limit you hit.

{ "name": "too_many_requests", "message": "Rate limit exceeded", "details": { "error_name": "too_many_requests", "retry_after_ms": 12000, "limited_by": "org" } }
  • retry_after_ms — milliseconds until the limit resets. use this instead of guessing backoff intervals.
  • limited_by — which limit you hit: org (your organization’s rate limit), ip (ip-based limit on unauthenticated requests), or target_site (the target site itself rate-limited the scrape).

the response also carries a standard Retry-After header (in seconds) alongside retry_after_ms — stock http clients honor it automatically.

see rate limits for the full list of active limits and retry strategies.

handling errors in code

const API_KEY = process.env.CRAWLBRULEE_API_KEY async function scrape(url) { const res = await fetch('https://api.crawlbrulee.com/api/scrape', { method: 'POST', headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ url, extract: { markdown: true } }), }) if (!res.ok) { const error = await res.json() switch (error.name) { case 'too_many_requests': { const retryMs = error.details?.retry_after_ms ?? 10_000 console.log(`Rate limited. Retrying in ${retryMs}ms...`) await new Promise((r) => setTimeout(r, retryMs)) return scrape(url) // retry once } case 'service_unavailable': { // Transient on our side — the key is fine, so retry it unchanged console.log('Service temporarily unavailable. Retrying in 2s...') await new Promise((r) => setTimeout(r, 2_000)) return scrape(url) // retry once } case 'usage_allocation_error': console.error(`Usage limit hit: ${error.details?.reason}`) throw new Error('Usage limit reached — upgrade your plan') case 'invalid_credentials': throw new Error('Bad API key — check your token') default: throw new Error(`API error [${error.name}]: ${error.message}`) } } return res.json() }