Skip to main content
Every failure returns the same envelope, with the right HTTP status:
{
  "success": false,
  "error": {
    "code": "not_found",
    "message": "Resource not found",
    "details": null
  }
}
code is a stable machine string - branch on it, not on the human message. details is present on a few errors with extra structure (see below).

Codes

StatuscodeWhen
400invalid_requestMalformed parameters or JSON body (e.g. a bad uuid, an empty triage body).
401unauthorizedMissing, invalid, revoked, or expired API key on a protected endpoint.
402payment_requiredNot enough credits for a credit-spending action.
403forbiddenA limit was reached - e.g. the plan’s watchlist product cap.
404not_foundUnknown resource id (or a product/opportunity not on your team).
429rate_limitedRate limit exceeded.
500internal_errorSomething went wrong on our side.

details payloads

Some errors carry structured details: 403 product limit reached (follow a product past your plan cap):
{
  "code": "forbidden",
  "message": "Product limit reached for your plan",
  "details": { "reason": "product_limit_reached", "limit": 5, "current": 5, "upgrade_required": true }
}
429 rate limited:
{ "code": "rate_limited", "details": { "retry_after_seconds": 12, "limit": 60, "remaining": 0 } }

Handling

Check success first, then switch on error.code:
const res = await fetch(url, { headers });
const body = await res.json();
if (!body.success) {
  switch (body.error.code) {
    case "unauthorized": /* refresh/replace the key */ break;
    case "rate_limited": /* honor Retry-After, then retry */ break;
    case "not_found": /* handle missing resource */ break;
    default: /* surface error.message */
  }
}