Errors

Every error has the same shape and a stable code. Branch on the code; show the message.

Every error has the same shape and a stable code. Branch on the code; show the message.

Error shape

{
  "error": {
    "code": "INVALID_PARAM",
    "message": "Unsupported country_code. Use a lowercase ISO 3166-1 alpha-2 code.",
    "param": "country_code",
    "docs": "/docs/errors#invalid_param"
  }
}
  • code is stable and machine-readable: branch on it.

  • message is for humans and may be reworded.

  • param names the offending parameter when there is one.

  • docs links to the code's entry below.

  • Every response, error or not, carries X-Request-Id.

Codes

Code

HTTP

Meaning

MISSING_PARAM

400

Missing parameter

INVALID_PARAM

400

Invalid parameter

UNAUTHORIZED

401

Unauthorized

DEMO_READ_ONLY

403

Demo workspace

INSUFFICIENT_CREDITS

402

Insufficient credits

NOT_FOUND

404

Not found

CONFLICT

409

Conflict

CONCURRENCY_LIMIT

429

Concurrency limit reached

RATE_LIMITED

429

Rate limited

TARGET_FAILED

502

Target failed

UNAVAILABLE

503

Temporarily unavailable

NOT_IMPLEMENTED

501

Not implemented

INTERNAL

500

Internal error

MISSING_PARAM {#missing_param}

HTTP 400 · Missing parameter. A required parameter was not sent. param names it.

What to do: Add the parameter. For GET requests, remember to URL-encode the target url.

INVALID_PARAM {#invalid_param}

HTTP 400 · Invalid parameter. A parameter was sent but its value is not accepted: wrong type, out of range, an unsupported country, a private target URL, an entry in asins that is not an ASIN (param is then asins[i]), or two options that cannot be combined. param names it.

What to do: Read message: it says exactly what is expected.

UNAUTHORIZED

HTTP 401 · Unauthorized. No API key was sent, or the key is unknown or revoked.

What to do: Send your key as Authorization: Bearer <key>, X-API-Key: <key> or ?api_key=<key>. Keys are in the dashboard under API keys.

DEMO_READ_ONLY {#demo_read_only}

HTTP 403 · Demo workspace. The key belongs to the public, read-only demo workspace. It can be seen in the dashboard but cannot make requests or spend credits.

What to do: Create an account (5,000 free credits) and use your own API key.

INSUFFICIENT_CREDITS {#insufficient_credits}

HTTP 402 · Insufficient credits. Your credit balance cannot cover this request. Nothing was charged. Batches are all-or-nothing: the whole batch must be affordable.

What to do: Buy a credit pack (credits never expire), or enable auto top-up in Billing.

NOT_FOUND {#not_found}

HTTP 404 · Not found. The job, batch or structured endpoint does not exist, or belongs to another account.

What to do: Check the id. Job results expire after 72 hours; the job itself remains.

CONFLICT

HTTP 409 · Conflict. The request conflicts with the resource's current state: a request with the same Idempotency-Key is still in progress, or a job that is already running was asked to cancel.

What to do: For idempotency conflicts, retry after a few seconds. Running jobs finish on their own (and are free if they fail).

CONCURRENCY_LIMIT {#concurrency_limit}

HTTP 429 · Concurrency limit reached. All of your concurrent request slots are busy. Sync requests are refused immediately; async jobs simply wait in the queue.

What to do: Retry with backoff, or use async jobs, which queue instead of failing. Concurrency grows with lifetime credits purchased.

RATE_LIMITED {#rate_limited}

HTTP 429 · Rate limited. Too many API calls with this key in a short window. Separate from concurrency: this counts calls, including cheap ones such as job polling.

What to do: Wait Retry-After seconds. Prefer webhooks to polling job status in a tight loop.

TARGET_FAILED {#target_failed}

HTTP 502 · Target failed. We could not get a usable answer from the target site after our retries: it blocked us, rate-limited us, errored, or never responded. You were not charged.

What to do: Retry; consider premium=true or ultra_premium=true for heavily protected sites, or render=true for JavaScript-built pages.

UNAVAILABLE

HTTP 503 · Temporarily unavailable. A dependency on our side is down or overloaded. Nothing was charged.

What to do: Retry with exponential backoff. Status updates at /status.

NOT_IMPLEMENTED {#not_implemented}

HTTP 501 · Not implemented. The endpoint or option exists in the API surface but is not available yet. Structured data endpoints answer this with "message": "Coming soon" and "coming_soon": true; nothing is charged.

What to do: For Amazon product data use the Amazon Product API, POST /v1/products. Contact support if you need another endpoint sooner.

INTERNAL

HTTP 500 · Internal error. Something failed on our side. Nothing was charged, and the failure was logged with your X-Request-Id.

What to do: Retry. If it persists, contact support with the X-Request-Id header value.

Retrying

Safe to retry with exponential backoff: CONCURRENCY_LIMIT, RATE_LIMITED (honour Retry-After), TARGET_FAILED, UNAVAILABLE, INTERNAL. Do not retry 400/401/402/404 without changing something. For creates, send an Idempotency-Key so a retry can never double-charge.

import time, requests

RETRYABLE = {"CONCURRENCY_LIMIT", "RATE_LIMITED", "TARGET_FAILED", "UNAVAILABLE", "INTERNAL"}

def scrape(url, attempts=4):
    for i in range(attempts):
        r = requests.get("https://api.scrapely.dev/v1/scrape", params={"url": url},
                         headers={"Authorization": "Bearer YOUR_API_KEY"}, timeout=70)
        if r.ok:
            return r.json()
        code = r.json()["error"]["code"]
        if code not in RETRYABLE or i == attempts - 1:
            raise RuntimeError(code)
        time.sleep(float(r.headers.get("Retry-After", 2 ** i)))
Updated