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

ScopeEndpoint
events:writePOST /v1/events
events:readGET query endpoints (future — some already reserve the scope)
live:readGET /v1/live/sessions + GET /live/ws (dashboard live viewer)
identity:resolvePOST born.clickstream.com/v1/resolve + signals queries
signals:readGET /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:

  1. X-API-Key header (preferred for server-to-server):
    X-API-Key: cs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    
  2. ?key= query param (for sendBeacon, WebSocket URLs, and curl convenience):
    https://t.example.com/sdk.js?key=cs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    
  3. Sec-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

  1. einstein.clickstream.comSettings → API Keys → Create key.
  2. Set the name, plan tier, and allowed domains.
  3. 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

  1. Create a new key in the dashboard (see above).
  2. Deploy the new key to every surface that uses it (SDK config, server env vars, CI secrets).
  3. Wait 7 days for cached page loads to pick up the new key.
  4. 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:

HTTPerrorMeaning
401missing_api_keyNo X-API-Key, ?key=, or subprotocol provided.
401invalid_api_keyKey not found in KV.
403domain_not_allowedRequest Origin/Referer doesn't match any configured domain.
403origin_requiredDomain-gated key + no Origin on the event-ingest POST.
403plan_upgrade_requiredKey's plan tier lacks the requested feature (Free → Signals, Builder → Signals Feed, …).
429rate_limit_exceededSliding window limit blown. Retry-After header carries the cooldown.
503service_unavailableKV 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