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"
}| Field | Meaning |
|---|---|
type | URL identifying the error class. Stable across versions; safe to switch on. |
title | Short human-readable summary |
status | HTTP status code (same as the response status) |
detail | Human-readable explanation specific to this occurrence |
instance | The request path that produced the error |
correlationId | Unique ID for this request. Include this in support tickets. Matches a server log line. |
code | Short 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
Authorizationheader - 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.
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:
- The HTTP status
- The
correlationIdfrom the response body - The approximate UTC timestamp of the request
- What you were trying to do
With the correlationId, we can pull the exact server-side trace in seconds.
What’s next
- Auth methods → Authentication overview
- Scopes (to avoid 403) → Scopes
- REST API surface → REST API overview