Errors

Every non-2xx response from /api/v1 endpoints carries the same JSON body:

{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded"
  }
}

code is a stable machine-readable identifier — branch on it, not on the message. message is human-readable detail; for validation failures it names the offending fields.

Error codes

StatusCodeMeaning
400invalid_requestMalformed body or parameters — the message lists what's wrong.
401unauthorizedMissing, malformed, revoked, or expired API key.
403forbiddenValid key, but it lacks the scope — or its owner's team role lacks the permission.
404not_foundThe file doesn't exist (or belongs to another team).
413payload_too_largeDirect upload over 100 MB — use the resumable tus endpoint.
415unsupported_media_typeContent type not accepted (images, videos, PDF, plain text only).
429rate_limitedThe key's request budget is exhausted — wait Retry-After seconds.
507quota_exceededThe team's storage quota is full.
500internal_errorUnexpected server error — safe to retry with backoff.

Handling rate limits

429 responses include a Retry-After header with the number of seconds until the window resets. Respect it rather than hammering:

async function apiFetch(url, options) {
  for (;;) {
    const res = await fetch(url, options)
    if (res.status !== 429) return res
    const wait = Number(res.headers.get("retry-after") ?? "5")
    await new Promise((resolve) => setTimeout(resolve, wait * 1000))
  }
}

Validation errors

400 invalid_request messages name each invalid field and what was expected, so they're safe to log during development:

{
  "error": {
    "code": "invalid_request",
    "message": "limit: Too big: expected number to be <=100"
  }
}