Search documentation

Find pages, sections, and snippets across the Clog docs.

Error codes

Every error code Clog emits, its HTTP status, when it fires, and how to handle it.

Every error response from the Clog API returns the same JSON envelope:

{
  "code": "BAD_REQUEST",
  "message": "Invalid post body: 'title' is required.",
  "details": { /* optional, code-specific context */ }
}
FieldMeaning
codeMachine-readable code from the table below. Branch on this, not on message.
messageHuman-readable summary. Safe to log; not safe to expose to end users without translation.
detailsOptional extra context. Shape depends on the code — Zod issue list, Prisma constraint info, per-row bulk failures, etc.

The codes

CodeHTTPWhen it firesTypical handling
BAD_REQUEST400Zod validation failure, invalid state transition, cross-workspace FK reference, malformed body. details usually carries the Zod issues.Surface the validation issues to the user; don't retry without changes.
UNAUTHORIZED401Missing, malformed, unknown, revoked, or expired X-API-Key. Generic on purpose — Clog deliberately doesn't distinguish so callers can't probe key state.Re-issue the key (create a new one + revoke the old).
PAYMENT_REQUIRED402Reserved. Not currently emitted by v1 endpoints.n/a
FORBIDDEN403Key is valid but the creator's membership doesn't grant the route's required permission.Grant the missing scope to the member who created the key (don't re-issue the key).
NOT_FOUND404The resource doesn't exist in the bound workspace. Slugs are workspace-scoped, so a slug that exists in another workspace is still a 404 here.Treat as missing; don't retry.
METHOD_NOT_ALLOWED405The HTTP method isn't supported on this path.Programmer error — check the method against the endpoint reference.
CONFLICT409Duplicate slug, blocked delete due to references (e.g. deleting an author still attached to posts, deleting a media row still set as a featured image), duplicate redirect fromPath.Surface the conflict; fix it before retrying.
RATE_LIMITED429Global rate limit tripped. v1 limits are coarse (per IP / per workspace), not per-key.Back off and retry with exponential delay.
INTERNAL500Unhandled server error.Treat as transient; retry with backoff. Persistent INTERNAL errors warrant a bug report.

Branching on code, not message

The code field is a stable, machine-readable enum. The message field is human-readable and not part of the API contract — it can change between releases for clarity. Don't string-match on it.

Right
if (err.code === 'NOT_FOUND') return null;
if (err.code === 'CONFLICT') showDuplicateSlugBanner();
Wrong
if (err.message.includes('not found')) return null;

A generic error wrapper

fetch.ts
type ClogError = { code: string; message: string; details?: unknown };

export async function clog<T>(path: string, init?: RequestInit): Promise<T> {
  const res = await fetch(`https://api.clog.dev/api/v1/external${path}`, {
    ...init,
    headers: {
      'X-API-Key': process.env.CLOG_API_KEY!,
      'Content-Type': 'application/json',
      ...(init?.headers ?? {}),
    },
  });

  if (res.ok) return res.json() as Promise<T>;

  const err: ClogError = await res
    .json()
    .catch(() => ({ code: 'INTERNAL', message: res.statusText }));
  throw Object.assign(new Error(`Clog ${err.code}: ${err.message}`), {
    code: err.code,
    status: res.status,
    details: err.details,
  });
}

Differentiating 401 from 403 in practice

A 401 UNAUTHORIZED always means the key itself doesn't authenticate — missing, revoked, expired, or unknown. The fix is on Clog's side (revoke + create a fresh key), not on the route's permission set.

A 403 FORBIDDEN always means the key authenticates, but the creator's membership doesn't carry the scope this route requires. The fix is to grant the missing permission to the member who created the key — not to re-issue the key.

If your integration starts returning 403 on routes it previously worked on, check the Members page — someone may have removed a permission from the member who issued the key.

details shapes worth knowing

The contents of details vary by error code. The most common shapes:

Zod validation issues (on BAD_REQUEST)

{
  "code": "BAD_REQUEST",
  "message": "Validation failed",
  "details": {
    "issues": [
      { "path": ["title"], "code": "too_small", "message": "Required" },
      { "path": ["slug"], "code": "invalid_string", "message": "Invalid slug format" }
    ]
  }
}

Bulk action failures (on bulk endpoints)

{
  "code": "BAD_REQUEST",
  "message": "Some rows failed",
  "details": {
    "errors": [
      { "id": "0a4b...", "reason": "post already archived" },
      { "id": "1b5c...", "reason": "not in this workspace" }
    ]
  }
}

Some bulk endpoints (e.g. bulk-import CSV redirects) commit successful rows and report failures in details. The HTTP status in that case is 200, not 400 — the response is "partially successful." Read the response body for per-row outcomes regardless of status.

Retrying

Only retry on RATE_LIMITED (429) and INTERNAL (500). Everything else is a permanent failure that won't resolve with a retry:

  • BAD_REQUEST — fix the input.
  • UNAUTHORIZED — fix the key.
  • FORBIDDEN — fix the permission.
  • NOT_FOUND — the resource isn't there.
  • CONFLICT — fix the conflict (rename the slug, detach the references, etc.).

For retries, use exponential backoff with jitter — 1s, 2s, 4s, 8s caps with random ±25%. Give up after 3–4 attempts.

On this page