APIErrors

Errors

Status codes, error shapes, and what to do about each one.

Successful responses are wrapped in { "success": true, "data": … }. Errors are not — they come back in one of three shapes.

Shapes

Gate errors

Raised by the subscription and rate-limit gates before the endpoint runs. These carry a stable code worth branching on.

{
  "error": "Rate limit exceeded",
  "message": "You have exceeded the minute rate limit for this endpoint.",
  "code": "RATE_LIMIT_EXCEEDED",
  "retry_after": 42
}
CodeStatusMeaning
UNAUTHENTICATED401No user could be resolved for the request
NO_ACTIVE_SUBSCRIPTION402The account has no plan
RATE_LIMIT_EXCEEDED429A minute, hour, or day window is full

Validation errors

Raised when the request body fails validation. Status is always 422, and errors maps each rejected field to its messages.

{
  "message": "The given data was invalid.",
  "errors": {
    "title": ["Article title must be at least 3 characters."],
    "target_word_count": ["Target word count must not exceed 10,000 words."]
  }
}

The messages are written for humans and can be surfaced directly in a UI.

Plain errors

Authentication and lookup failures return only a message.

{
  "message": "Unauthenticated."
}

Status reference

StatusWhenRetry?
401Missing, malformed, expired, or revoked key; creator no longer an account memberNo — fix the key
402Account has no active subscriptionNo — subscribe
403Not authorised for that project, or the user account is disabledNo
404Article, project, or workflow does not exist, or is not visible to this accountNo
422Validation failed, or the monthly word quota cannot cover the articleNo — change the request
429Rate limit exceededYes, after retry_after
500Unexpected server errorYes, with backoff

404 is also the answer when a record exists but belongs to another account. Existence is never confirmed across account boundaries.

Cases worth handling explicitly

Out of words. Article creation reserves 120% of target_word_count against the monthly allowance. When it does not fit, the failure arrives as a 422 on target_word_count with a message stating remaining and required words. It is not a validation mistake — the request would succeed next month, or with a smaller target. See Rate limits & quotas.

Unknown article type. article_type accepts a built-in type or a custom workflow type defined in that project. A custom type that exists in a different project fails validation here.

Research not supported. Setting research_enabled: true on an article type whose workflow has no research step is rejected rather than silently ignored.

A 404 on a workflow. Workflow routes are nested under a project; a step that exists but belongs to another project returns 404, not 403.

Failures after 201

POST /v1/articles returns 201 when the job is queued, not when the article is written. Anything that goes wrong afterwards shows up as status: "failed" on the article rather than as an HTTP error — it will never reach a webhook. Poll or watch for that status; see Article lifecycle.

Retry policy

async function withRetry(fn, attempts = 4) {
  for (let i = 0; i < attempts; i++) {
    const res = await fn()
    if (res.status !== 429 && res.status < 500) return res

    const body = res.status === 429 ? await res.clone().json() : null
    const wait = body?.retry_after ?? 2 ** i
    await new Promise((r) => setTimeout(r, wait * 1000))
  }
  throw new Error('exhausted retries')
}

Retry 429 and 5xx. Everything else is a request that needs changing, and repeating it only spends quota.