Skip to main content
Every endpoint returns one of three envelope shapes: completed, queued, or error. Check the top-level status first, then read data (success or queued) or error (failure).

Status codes

CodeMeaningerror.codeAction
200 OKSynchronous successn/aRead data
202 AcceptedAsync dispatch (SmartBrowse)n/aRead data.run_id and poll
400 Bad RequestMalformed requestinvalid_requestFix the request
401 UnauthorizedMissing or revoked API keyunauthorizedCheck X-API-Key
402 Payment RequiredOut of creditsinsufficient_creditsTop up at Billing
402 Payment RequiredEmail not verifiedemail_verification_requiredVerify your email
403 ForbiddenAuthenticated, but not allowed to perform this actionforbiddenCheck the key’s scope or the resource owner
404 Not FoundResource doesn’t existnot_foundVerify the ID
409 ConflictResource in a state that disallows the actionconflict / account_deletion_pendingE.g. spending while account deletion is pending
422 Unprocessable EntitySchema validation failedvalidation_failedAdjust the JSON schema or prompt
429 Too Many RequestsRate limit hitrate_limitedBack off. See Rate limits
500 / 502Internal errorinternal_error / service_unavailableRetry with backoff; failed requests cost 0 credits

Success envelope

{
  "status": "completed",
  "data": { "...": "..." },
  "credits_used": 5,
  "credits_remaining": 495,
  "request_id": "req_aB3xY9Kp"
}
data holds the payload — extracted JSON for /v1/smartscraper, fetched content for /v1/scrape. credits_used is what this call cost after deduction.

Queued envelope

Returned by the async-dispatch endpoint (POST /v1/smartbrowse/recipes/:id/run). No credit fields here because the work hasn’t finished and nothing’s been billed yet.
{
  "status": "queued",
  "data": {
    "run_id": "k7Xb9dRmQ2p",
    "recipe_id": "m3Yc2tFvN8q",
    "run_status": "running",
    "poll_url": "/v1/smartbrowse/runs/k7Xb9dRmQ2p",
    "created_at": "2026-05-21T17:42:01Z"
  },
  "request_id": "req_aB3xY9Kp"
}
Poll data.poll_url (i.e. GET /v1/smartbrowse/runs/:id) until the inner field hits a terminal state. We named it run_status so it doesn’t clash with the envelope’s outer status:
  • data.run_status is one of queued | running | completed | failed | cancelled
Or set up a webhook in Settings > Webhooks and skip polling entirely.

Error envelope

{
  "status": "error",
  "error": {
    "code": "validation_failed",
    "message": "Output did not match the requested schema after one repair attempt.",
    "details": { "...": "optional structured context" }
  },
  "request_id": "req_aB3xY9Kp"
}
error.code is stable — branch on it. error.message is human-readable and can change between releases, so log it for debugging but don’t pattern-match on the text. error.details is optional structured context (e.g. {balance, required} for insufficient_credits). The request_id also comes back as the X-Request-ID response header on every response, success or failure. Include it when you ping support.

Retrying

Transient failures (timeouts, 429, 5xx) are safe to retry. Since failed requests cost 0 credits, a capped backoff loop with jitter costs you nothing beyond the eventual successful call. Don’t retry on 400, 401, 402, 403, 404, or 422 — those won’t change until you fix the request.