Search documentation

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

Authenticating requests

Authenticate external API requests with the X-API-Key header.

Every request to /api/v1/external/* is authenticated with a single header: a workspace-bound API key in X-API-Key. This page covers how to issue, send, rotate, and revoke that key — and exactly what happens at the backend on each request.

The header

GET /api/v1/external/posts HTTP/1.1
Host: api.clog.dev
X-API-Key: clog_live_xxx

A missing or empty X-API-Key returns 401 UNAUTHORIZED. An unknown, revoked, or expired key returns the same generic 401 — Clog deliberately doesn't distinguish, so callers can't probe key state.

fetch (JS / TS)
await fetch('https://api.clog.dev/api/v1/external/posts/my-first-post', {
  headers: {
    'X-API-Key': process.env.CLOG_API_KEY!,
  },
});

Issuing a key

From the dashboard: API keys → New key. Provide a name (e.g. marketing-site-prod) and an optional expiresAt. The create response contains the plaintext key exactly once:

clog_live_AbCdEfGhIjKlMnOpQrStUvWxYz...

Copy it into your secret store immediately. The dashboard's Reveal action can re-show it later (it AES-GCM-decrypts the stored ciphertext), but the value never appears in any list response or API read.

The stored row carries three values, none of which are the plaintext you saw at create time:

FieldWhat it is
keyPrefixFirst ~18 chars (e.g. clog_live_AbCdEfGh). Safe to display in the dashboard.
keyHashsha256(plaintext) — the constant-time lookup column used by the middleware.
keyEncryptedAES-256-GCM ciphertext under a server-side master key. Only ever read by the explicit reveal endpoint.

How a key maps to permissions

API keys carry no permission column of their own. At request time, the middleware:

  1. Hashes the incoming key (sha256) and looks up the matching ApiKey row.
  2. Rejects if the key is revoked (revokedAt set) or expired (expiresAt ≤ now) — both surface as a generic 401 UNAUTHORIZED.
  3. Resolves the creator's current Membership on the bound workspace.
  4. Uses that membership's permissions array (or implicit-full if the creator is the workspace owner) as the request's permission set.
  5. Enforces the route's required scope (e.g. posts:read on GET /external/posts).

The consequences are important:

Revoking a member's Membership instantly disables every API key they created on that workspace — without touching the ApiKey rows. The keys still exist; they just stop authorising. For machine integrations, create keys from a dedicated service user whose membership is stable.

Permission changes apply live. If you remove posts:write from a member, their existing keys lose write access on the next request. There is no token re-issuance step.

Status codes

StatusCodeWhen it fires
401UNAUTHORIZEDMissing, malformed, unknown, revoked, or expired key. Generic on purpose.
403FORBIDDENKey is valid, but the creator's membership doesn't grant the required permission for the route.
404NOT_FOUNDThe resource doesn't exist in the bound workspace. (Slugs are workspace-scoped, so a slug present elsewhere is still a 404 here.)

A 403 always means "the key is fine, the permission isn't there" — re-issue the membership grant, don't re-issue the key.

Rotation and revocation

There is no in-place key rotation — the plaintext is irrecoverable after creation. The pattern is create-then-revoke:

  1. Create a new key in the dashboard.
  2. Deploy it to your application.
  3. Revoke the old key (API keys → Revoke in the dashboard).

Revocation sets revokedAt = now(). It is idempotent — re-revoking a revoked key is a no-op. The key starts returning 401 on the next request after the revocation commits.

You can also set expiresAt at create time for time-bounded keys — useful for short-lived integration tokens.

What the middleware does not do (v1)

  • No last_used_at tracking. v1 doesn't record per-key usage. If you need to audit a key's traffic, run it through a proxy you control.
  • No per-key rate limit. Clog rate-limits globally, not per-key.
  • No IP allowlist. A leaked key works from anywhere until revoked.

These are tracked as v1 out-of-scope; treat your keys as production secrets accordingly.

Best practices

  • Server-side only. Never embed an API key in a browser bundle, a mobile app, or any client the user controls. The key carries the creator's full workspace permissions on the bound workspace.
  • One key per integration. Don't share marketing-site-prod with mobile-app-ios. Per-integration keys make rotation surgical.
  • Bind to a service user. For production integrations, create a workspace member solely for machine access (integrations@your-domain.com), grant it only the permissions the integration needs, and issue keys as that user. A teammate leaving never breaks production.
  • Set expiresAt for short-lived needs. Audits, time-bounded experiments, contractors. A key that expires by itself is one fewer thing to remember.
  • Store keys in a secret manager, not in a .env file checked into a repo.

On this page