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)

Which shape you actually get. The endpoints under /api/... on portal.scrapewise.ai (and the same endpoints proxied through the MCP gateway) answer with a compact envelope — {"errors": [{ "code", "message", "correlationId" }]} — rather than the flat fields above. Switch on errors[0].code. The examples in this section that show a flat code / detail object describe the older shape and are being brought into line; the 402 section below shows the current one verbatim.

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 Forbidden — scope_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.

402 Payment Required — WALLET_INSUFFICIENT_BALANCE

The prepaid wallet has neither balance nor free requests left. Returned by every billable call — a scraper run, a group run, a sample, a URL or curl preview, and the ad-hoc fetch endpoints.

{ "errors": [ { "code": "WALLET_INSUFFICIENT_BALANCE", "message": "Insufficient wallet balance — top up to continue" } ] }

Fix: top up the wallet, then repeat the call — nothing is lost. This is a refusal, not a transient failure: retrying or backing off returns the same 402. A run stopped mid-flight for the same reason carries stopReason: "WALLET_INSUFFICIENT_BALANCE" on its job-status row.

Through MCP the same response reaches the client as a tool result with isError: true whose text is HTTP 402 followed by the body above — see MCP overview.

Accounts still on the legacy plan catalogue can also see 402 when a plan lacks a feature; that response carries a different code, which is why you switch on the code and not the status. See Wallet & billing.

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 === 402 && err.errors?.[0]?.code === 'WALLET_INSUFFICIENT_BALANCE') { throw new Error(`Scrapewise wallet is empty — top up and retry (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