API keys + auth
ClickStream uses API keys scoped to a single tenant (clientId) with a permission array. The same key works across every surface (collector, identity graph, signals, feed); the permission array determines which endpoints it can reach.
Key format
cs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ← production
cs_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ← staging / test
The cs_live_ / cs_test_ prefix is advisory — tools that scrub secrets from logs (pre-commit hooks, CI masking, sentry filters) pattern-match on it so production keys don't accidentally end up in error tickets. The collector treats both prefixes identically at runtime.
Permissions you'll see on a key
Every key carries a list of scopes + a plan tier. You don't configure the storage layout yourself — the dashboard handles it when you click Create key. For reference, the key metadata returned by the API looks like this:
{
"clientId": "your-client-id",
"name": "Your Company",
"plan": "builder",
"permissions": ["events:write", "events:read", "live:read", "identity:resolve"],
"rateLimit": 1000,
"createdAt": "2026-04-01T00:00:00Z",
"sites": [
{
"siteId": "main",
"name": "Main Site",
"domains": ["example.com", "*.example.com"]
}
]
}
Collector responses carry a X-RateLimit-Remaining header so you can track usage without querying the dashboard.
Permission scopes
| Scope | Endpoint |
|---|---|
events:write | POST /v1/events |
events:read | GET query endpoints (future — some already reserve the scope) |
live:read | GET /v1/live/sessions + GET /live/ws (dashboard live viewer) |
identity:resolve | POST born.clickstream.com/v1/resolve + signals queries |
signals:read | GET /v1/signals/:visitorId + GET /signals/stream WebSocket |
Tier-based feature flags (see the Pricing page) gate some scopes regardless of what's in the permissions array — e.g. Free-tier keys can never use signals:read for /v1/signals/:visitorId, and only Scale+ keys can open the Signals Feed WebSocket.
Passing the key
ClickStream accepts the key three ways, in order of precedence:
X-API-Keyheader (preferred for server-to-server):X-API-Key: cs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx?key=query param (forsendBeacon, WebSocket URLs, and curl convenience):https://t.example.com/sdk.js?key=cs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSec-WebSocket-Protocol(for browser WebSocket clients — keeps the key off the URL):Sec-WebSocket-Protocol: clickstream-v1, cs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Query-string keys are fine for short-lived debugging, but prefer the header for production — query strings can end up in CDN access logs, browser history, and referrer headers. The X-API-Key header never does.
Domain gating
If the API key has configured domains (ClientConfig.sites[].domains), every authenticated request must carry an Origin or Referer header matching one of them. The 403 domain_not_allowed response is what you see when the origin doesn't match.
Wildcard subdomains are supported: *.example.com matches www.example.com, app.example.com, but not example.com itself. Add both if you need to match the apex domain.
Server-to-server callers with no browser-style Origin can request an account-scoped admin key from support — it carries a signed internal header that bypasses the Origin check while still honoring rate limits + per-tenant isolation.
Creating a key
- einstein.clickstream.com → Settings → API Keys → Create key.
- Set the name, plan tier, and allowed domains.
- Copy the key — it's shown exactly once. Store it in your deployment's secret manager (Vercel environment variable, GitHub Actions secret, 1Password, Doppler, etc.).
Rotating a key
- Create a new key in the dashboard (see above).
- Deploy the new key to every surface that uses it (SDK config, server env vars, CI secrets).
- Wait 7 days for cached page loads to pick up the new key.
- Delete the old key in the dashboard.
Caching notes: the collector caches ClientConfig for 2 minutes per isolate. The negative cache (invalid-key lookup) is 30 seconds. Rotation takes effect globally within ~2 minutes after the KV write.
Error taxonomy
All auth-layer errors return a JSON body with an error code + human-readable message:
| HTTP | error | Meaning |
|---|---|---|
| 401 | missing_api_key | No X-API-Key, ?key=, or subprotocol provided. |
| 401 | invalid_api_key | Key not found in KV. |
| 403 | domain_not_allowed | Request Origin/Referer doesn't match any configured domain. |
| 403 | origin_required | Domain-gated key + no Origin on the event-ingest POST. |
| 403 | plan_upgrade_required | Key's plan tier lacks the requested feature (Free → Signals, Builder → Signals Feed, …). |
| 429 | rate_limit_exceeded | Sliding window limit blown. Retry-After header carries the cooldown. |
| 503 | service_unavailable | KV read failed. Usually transient; Retry-After: 5. |
X-Request-ID is set on every response so you can correlate a client error to a specific collector log line in Logpush.
See also
- Signals API — uses
signals:readscope - Signals Feed (WebSocket) — Scale+ plan gate details
- Rate limits — per-tier caps
- Event schema — what the collector records under each authenticated event