Errors
Error bodies, every HTTP status the API returns, and which ones to retry.
Error format
Errors use a non-2xx status and a JSON body with a detail field. For most errors it is a string:
401 response
{
"detail": "missing or invalid API key"
}Request validation errors (422) return detail as a list, one entry per problem, with the location of the bad field:
422 response
{
"detail": [
{
"type": "missing",
"loc": [
"body",
"token"
],
"msg": "Field required",
"input": {
"days": 90
}
},
{
"type": "extra_forbidden",
"loc": [
"body",
"nights"
],
"msg": "Extra inputs are not permitted",
"input": 3
}
]
}A few endpoints also return 422 with a string detail for semantic checks (for example an unsupported booking engine). Handle both shapes.
Status codes
| Status | Meaning | Retry? |
|---|---|---|
200 | Success. Charged unless the result is empty. | — |
202 | Accepted: a calendar job was queued (POST /v1/jobs). | — |
400 | Well-formed but unusable parameters (e.g. end before start, bad cursor). | No |
401 | Missing or invalid API key. | No |
402 | Not enough credits. Only once credit enforcement is on — never during the beta. | After credits renew or are added |
404 | Unknown property, stay window, job or request, or a key not linked to a billing account. | No |
409 | Idempotency-Key reused with a different body (POST /v1/jobs). | No — use a new key |
422 | Validation failed: missing or unknown fields, out-of-range values. | No |
429 | Too many requests: per-minute rate limit, or plan concurrency once enforcement is on. | Yes, with backoff |
502 | The upstream source failed, blocked the request or returned something unusable. | Yes, with backoff |
503 | A dependency is temporarily unavailable. | Yes, with backoff |
402 and 429
Both only block when they have to:
| Status | detail | When |
|---|---|---|
402 | insufficient credits: this request costs 5, 0 available. Credits renew 2026-11-01. | Credit enforcement is on and the request costs more than your available credits. Not returned during the beta (shadow mode). |
429 | rate limit 60/min exceeded | Your key sent more requests in the last 60 seconds than its limit (see Rate limits). |
429 | concurrency limit 2 reached for the Free plan | Credit enforcement is on and you already have as many requests in flight as your plan allows. |
Retrying safely
- Retry
429,502and503with exponential backoff and jitter; there are noRetry-Afterheaders. - Do not retry other 4xx errors: fix the request instead.
- For
POST /v1/jobs, retry with the sameIdempotency-Keyso a retry cannot create a second job.
import os
import random
import time
import requests
RETRYABLE = {429, 502, 503}
def call(method, path, **kwargs):
url = f"https://api.scrapercompany.com{path}"
headers = {"x-api-key": os.environ["SCRAPERCOMPANY_API_KEY"]}
for attempt in range(5):
response = requests.request(method, url, headers=headers, timeout=120, **kwargs)
if response.status_code not in RETRYABLE:
break
time.sleep(min(2 ** attempt, 30) + random.random())
if not response.ok:
detail = response.json().get("detail")
raise RuntimeError(f"{response.status_code} {detail} (request {response.headers.get('x-request-id')})")
return response.json()
calendar = call("POST", "/v1/calendar", json={"token": "ChUIoben2Mv6-CYaCi9tLzA3czVwbjQQAQ", "days": 90})Request IDs
Every response carries an x-request-id header (req_…). It matches the id in your request history and the request_id on ledger entries. Include it when you contact support.