Skip to Content
AuthenticationError envelope

Error envelope

Scrapewise returns errors in a consistent JSON shape across both REST and MCP surfaces, modeled on RFC-7807 (Problem Details for HTTP APIs ).

The shape

{ "type": "https://scrapewise.ai/errors/scope-rejected", "title": "Scope rejected", "status": 403, "detail": "Your API key has scope USER but this endpoint requires LLM_READ or higher.", "instance": "/api/scraper-api/api/key/whoami", "correlationId": "req_abc123def456", "code": "scope_rejected" }
FieldMeaning
typeURL identifying the error class. Stable across versions; safe to switch on.
titleShort human-readable summary
statusHTTP status code (same as the response status)
detailHuman-readable explanation specific to this occurrence
instanceThe request path that produced the error
correlationIdUnique ID for this request. Include this in support tickets. Matches a server log line.
codeShort machine-readable error code (preferred over type for switching)

Common errors

401 Unauthorized

Missing, malformed, or revoked authentication.

{ "status": 401, "code": "unauthorized", "title": "Unauthorized", "detail": "Missing or invalid Authorization header.", "correlationId": "req_..." }

Causes:

  • No Authorization header
  • Wrong format — must be Bearer <token> with a space
  • Token expired (Firebase JWT) or revoked (API key)
  • Token from a different environment (e.g. dev key against prod URL)

Fix: check the header format. For API keys, ensure the full sw_live_<prefix>.<secret> string. For JWTs, sign in to the portal again to refresh.

403 Forbiddenscope_rejected

Token is valid but its scope can’t call this endpoint.

{ "status": 403, "code": "scope_rejected", "title": "Scope rejected", "detail": "Your scope is USER but this requires LLM_READ.", "data": { "scope": "USER", "requiredScope": "LLM_READ" } }

Fix: mint a new key with the higher scope. See Scopes.

404 Not Found

The resource doesn’t exist for your customer. Note: Scrapewise returns 404 for resources that exist for a different customer too — we don’t leak the existence of other-tenant resources via different status codes.

{ "status": 404, "code": "not_found", "title": "Not found", "detail": "Scraper with id '...' does not exist in your customer." }

409 Conflict

The request can’t be applied because the resource is in a state that doesn’t allow it.

{ "status": 409, "code": "scraper_already_running", "detail": "Cannot trigger a new run while job 'job_xyz' is still running." }

422 Unprocessable Entity

Request was syntactically valid (parsed OK) but semantically rejected (failed schema validation, business rule, etc.).

{ "status": 422, "code": "validation_failed", "detail": "Field 'startUrls' must be a non-empty array.", "violations": [ { "field": "startUrls", "rule": "non_empty" } ] }

429 Too Many Requests

Rate-limited. Response includes a Retry-After header (seconds).

HTTP/1.1 429 Too Many Requests Retry-After: 30 Content-Type: application/problem+json { "status": 429, "code": "rate_limited", "title": "Too many requests", "detail": "Per-key rate limit exceeded. Retry in 30 seconds.", "data": { "retryAfterSeconds": 30, "limitedBy": "per-key" } }

Fix: wait the Retry-After seconds, then retry with exponential backoff. The limitedBy field tells you which limiter fired (per-key, per-customer, per-ip).

500 Internal Server Error

Something broke on our side. Always paste the correlationId into a support message — we use it to trace your request through our logs.

{ "status": 500, "code": "internal_error", "detail": "An unexpected error occurred. Contact support with the correlationId below.", "correlationId": "req_..." }

503 Service Unavailable

Briefly overloaded or in maintenance. Often paired with Retry-After.

{ "status": 503, "code": "unavailable", "detail": "Service temporarily unavailable. Retry in 60 seconds." }

Error-handling pattern

Robust client code:

async function callScrapewise(path: string, init?: RequestInit): Promise<unknown> { const res = await fetch(`https://portal.scrapewise.ai/api/scraper-api${path}`, { ...init, headers: { ...init?.headers, Authorization: `Bearer ${process.env.SCRAPEWISE_KEY}` }, }); if (res.ok) return res.json(); // Parse the error envelope const err = await res.json().catch(() => ({})); const corrId = err.correlationId ?? 'unknown'; if (res.status === 429) { const retryAfter = Number(res.headers.get('Retry-After') ?? '30'); throw new Error(`Rate limited; retry in ${retryAfter}s (corr=${corrId})`); } if (res.status === 401) { throw new Error(`Auth failed — check your API key (corr=${corrId})`); } if (res.status === 403 && err.code === 'scope_rejected') { throw new Error(`Scope ${err.data?.scope} can't call this; need ${err.data?.requiredScope} (corr=${corrId})`); } throw new Error(`Scrapewise ${res.status} ${err.code ?? 'unknown'}: ${err.detail ?? 'no detail'} (corr=${corrId})`); }

Reporting bugs / support

Whenever you contact us about an unexpected error, include:

  1. The HTTP status
  2. The correlationId from the response body
  3. The approximate UTC timestamp of the request
  4. What you were trying to do

With the correlationId, we can pull the exact server-side trace in seconds.

What’s next