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": {
"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#
| Field | Type | Description |
|---|---|---|
| invalid_request | 400 | The body failed validation, or a conditionally-required field is missing (e.g. url on a website source). The message lists each violation. |
| invalid_api_key | 401 | Missing Authorization header, malformed key, unknown key, or revoked key — deliberately indistinguishable. |
| insufficient_credits | 402 | The org balance can't cover the operation. The message states exactly how many credits were needed and available. Nothing was charged. |
| not_found | 404 | The resource doesn't exist or belongs to another organization — the API doesn't distinguish. Applies to articles, jobs, brand profiles, and integrations. |
| rate_limit_exceeded | 429 | Per-key request budget exhausted for the current 60-second window. Comes with a retry-after header in seconds. |
| internal_error | 500 | Something broke on our side. Safe to retry with backoff; the failure is already logged and alerting. |
| publish_failed | 502 | The 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.
200reads and idempotent replays,201created resources (brand profiles, webhook endpoints),202accepted 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#
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#
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#
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
}
}