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_xxxA 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.
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:
| Field | What it is |
|---|---|
keyPrefix | First ~18 chars (e.g. clog_live_AbCdEfGh). Safe to display in the dashboard. |
keyHash | sha256(plaintext) — the constant-time lookup column used by the middleware. |
keyEncrypted | AES-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:
- Hashes the incoming key (
sha256) and looks up the matchingApiKeyrow. - Rejects if the key is revoked (
revokedAtset) or expired (expiresAt ≤ now) — both surface as a generic401 UNAUTHORIZED. - Resolves the creator's current
Membershipon the bound workspace. - Uses that membership's
permissionsarray (or implicit-full if the creator is the workspace owner) as the request's permission set. - Enforces the route's required scope (e.g.
posts:readonGET /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
| Status | Code | When it fires |
|---|---|---|
401 | UNAUTHORIZED | Missing, malformed, unknown, revoked, or expired key. Generic on purpose. |
403 | FORBIDDEN | Key is valid, but the creator's membership doesn't grant the required permission for the route. |
404 | NOT_FOUND | The 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:
- Create a new key in the dashboard.
- Deploy it to your application.
- 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_attracking. 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-prodwithmobile-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
expiresAtfor 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
.envfile checked into a repo.
Related
- Conventions — the response envelope and error format you'll see on every request.
- Reference → Permissions — the full permission vocabulary and per-endpoint gate.