Errors

Every error response uses the same JSON envelope:

{
  "error": {
    "code": "validation_error",
    "message": "url is required",
    "correlation_id": "8c8a1b80-6c97-4f5e-8d0a-3a25d1e8e4c1"
  }
}

Status to code mapping

HTTP statuserror.codeMeaning
400validation_errorThe request body, query, or headers failed validation
401unauthorizedThe token is missing, malformed, expired, or revoked
403forbiddenThe token is valid but lacks permission for this resource
404not_foundThe resource does not exist (or the caller cannot see it)
409conflictThe request conflicts with current state (e.g. duplicate name)
429rate_limitedA rate limit was exceeded; see Rate limits
503plan_limit_exceededA plan quota or limit was reached. The body includes an upgrade.url link
5xxinternal_errorSomething failed on our side. Retry with backoff and report correlation_id

Plan-limit responses

When error.code = "plan_limit_exceeded", the body has an additional field:

{
  "error": {
    "code": "plan_limit_exceeded",
    "message": "Workspace bookmark limit reached on the Free plan.",
    "correlation_id": "...",
    "upgrade": {"url": "https://not24get.me/billing/upgrade"}
  }
}

Surface the upgrade.url to the user; that is the canonical place to upgrade.

Always log correlation_id

If you open a support ticket about a failed request, include the correlation_id from the response (or from the X-Correlation-ID response header). It lets us find the exact server-side trace for that call.

Client pseudocode

async function callApi<T>(req: Request): Promise<T> {
  const res = await fetch(req);
  if (res.ok) return res.json() as Promise<T>;
  const body = await res.json().catch(() => ({}));
  const err = body?.error ?? {};
  throw Object.assign(new Error(err.message ?? res.statusText), {
    status: res.status,
    code: err.code,
    correlationId: err.correlation_id,
  });
}