Docs · Reference · Errors

Errors.

Every non-2xx response uses one envelope. The code is a stable machine-readable string — branch on it, not on the message text, which can change.

The envelope#

Error response body
{
  "error": {
    "code": "invalid_request",
    "message": "topic: String must contain at least 4 character(s)"
  }
}

Validation failures concatenate every issue into the message as field: problem pairs joined with "; ", so one response tells you everything wrong with the payload.

Error codes#

FieldTypeDescription
invalid_request400The body failed validation, or a conditionally-required field is missing (e.g. url on a website source). The message lists each violation.
invalid_api_key401Missing Authorization header, malformed key, unknown key, or revoked key — deliberately indistinguishable.
insufficient_credits402The org balance can't cover the operation. The message states exactly how many credits were needed and available. Nothing was charged.
not_found404The resource doesn't exist or belongs to another organization — the API doesn't distinguish. Applies to articles, jobs, brand profiles, and integrations.
rate_limit_exceeded429Per-key request budget exhausted for the current 60-second window. Comes with a retry-after header in seconds.
internal_error500Something broke on our side. Safe to retry with backoff; the failure is already logged and alerting.
publish_failed502The downstream CMS rejected a publish. The message carries the upstream status and reason (auth, permissions, content model). Only from POST /v1/articles/{id}/publish.

Status mapping#

  • 2xx — success. 200 reads and idempotent replays, 201 created resources (brand profiles, webhook endpoints), 202 accepted async work (article jobs, source ingestion).
  • 4xx — your request needs to change before retrying (except 429, which just needs time).
  • 5xx— our problem or the CMS's problem; retry with backoff.

402 insufficient_credits#

402 example
HTTP/1.1 402 Payment Required
Content-Type: application/json

{
  "error": {
    "code": "insufficient_credits",
    "message": "Insufficient credits: need 25, have 8. Top up or upgrade your plan."
  }
}

Note the failure happens before job creation — there is no half-created job to clean up. Failed jobs that were successfully created refund automatically, so a 402 always reflects your true balance, not stuck holds.

429 rate_limit_exceeded#

429 example
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
x-ratelimit-limit: 60
x-ratelimit-remaining: 0
x-ratelimit-reset: 1751795160
retry-after: 21

{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Too many requests. Back off and retry."
  }
}

Wait retry-after seconds (or until x-ratelimit-reset, a unix timestamp) and retry. Details on the window mechanics are in Rate limits.

Handling pattern#

With the SDK
try {
  const job = await scribe.articles.create({ topic });
} catch (err) {
  if (err instanceof ScribeError) {
    switch (err.code) {
      case "insufficient_credits": // 402 — top up, then retry
      case "rate_limit_exceeded":  // 429 — the SDK already retried twice
      case "invalid_request":      // 400 — fix the payload, don't retry as-is
      case "not_found":            // 404 — check the ID and org
      default:                     // internal_error etc.
    }
    console.error(err.status, err.code, err.message);
  } else {
    throw err; // network failure, abort, bug
  }
}