API keys + auth
ClickStream uses API keys scoped to a single tenant (clientId). Public site keys collect events and read per-visitor Signals snapshots from approved domains. Tenant-wide live streams use short-lived dashboard/server tokens so a key copied from website code cannot read live visitor activity.
Key format
cs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ← production website key
cs_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ← test / non-production website key
cs_mob_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx ← dedicated Mobile app key
cs_srv_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx ← dedicated Server key
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.
Key types and the provenance gate
Each key carries a keyType that decides whether the browser Origin/Referer gate applies. The key type is set by the property type you create in the dashboard:
| Property type | Key | keyType | Origin gate |
|---|---|---|---|
| Website | shared account key (cs_live_*) | browser (the absent default) | Enforced — browser writes and Signals reads need a matching Origin/Referer. |
| Mobile app | dedicated cs_mob_live_* | mobile | Exempt — native apps post directly with no Origin. |
| Server | dedicated cs_srv_live_* | server | Exempt — backends post directly with no Origin. |
Website keys are byte-identical to before: a key with no keyType is treated as browser. Mobile and server keys are minted on a dedicated property (no first-party domain, no DNS) and get a tighter per-key rate-limit bucket. Because they are domain-less, the collector skips domain validation entirely and keys the provenance exemption off the key type.
The exemption is gated on key posture, not on the user agent. A library UA like
OkHttporCFNetworkis expected under a mobile/server key, but the same UA under a website key still forces a bot classification — a copied website key cannot escape detection by spoofing a native UA. See Mobile apps.
Key metadata
Every key carries metadata, including reserved permission labels and a plan tier. You don't configure the storage layout yourself — the dashboard mints the account/site key during signup and site creation, then shows it on the Sites and Settings screens. For reference, the key metadata stored by the collector looks like this:
{
"clientId": "your-client-id",
"name": "Your Company",
"plan": "growth",
"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.
Reserved scope labels
The permissions array is preserved for compatibility with older keys and future account-scoped keys. Today, normal site keys are enforced by key validity, domain gating, plan gates, and rate limits.
| Label | Meaning |
|---|---|
events:write | POST /v1/events |
events:read | Reserved for future collector query endpoints |
live:read | Private account scope for dashboard live surfaces. Public site keys do not open live WebSockets. |
identity:resolve | Optional enrichment resolve endpoint |
signals:read | Private account scope for tenant-wide Signals Feed subscribers. Public site keys can still use page-side Signals snapshots. |
Tier-based feature flags (see the Pricing page) gate Signals regardless of any reserved scope label: Hobby keys can use /v1/signals/:visitorId for basic page-code snapshots, while only Scale+ accounts can mint Signals Feed stream tokens.
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 (for SDK loaders,sendBeacon, and curl convenience):https://t.example.com/sdk.js?key=cs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Tenant-wide WebSocket live surfaces do not accept public site keys. They use short-lived stream tokens sent as Sec-WebSocket-Protocol: clickstream-v1, <stream_token>. The per-visitor browser stream at /v1/signals/:visitorId/stream is different: it uses the public site key, the active sessionId, domain gating, plan gating, and Signals Coverage metering.
Query-string keys are fine for short-lived debugging on non-stream endpoints, but prefer the header for production server calls. Query strings can end up in CDN access logs, browser history, and referrer headers. The X-API-Key header never does.
Domain gating
If a website key has configured domains (ClientConfig.sites[].domains), browser-facing event writes and Signals snapshot reads 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 example.com, www.example.com, and app.example.com.
Mobile (cs_mob_live_*) and server (cs_srv_live_*) keys skip domain gating — they have no configured domains and are provenance-exempt, so native apps and backends send no Origin header at all.
Signals Feed and live-session WebSockets require short-lived stream tokens from an authenticated dashboard or approved server integration flow. If a browser-style Origin is present, it must be a trusted ClickStream dashboard origin unless the key behind the token is an explicitly private read key.
Creating a key
The dashboard currently creates one account/site API key during signup and site creation. You can copy it from Sites or Settings. Store it in your deployment's secret manager (Vercel environment variable, GitHub Actions secret, 1Password, Doppler, etc.).
Self-serve key creation, rotation, and per-key domain scopes are support-assisted today. Email support@clickstream.com when you need a new key.
Rotating a key
- Email support@clickstream.com with the site or account that needs rotation.
- 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.
- Ask support to revoke the old key once traffic has drained.
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 (Growth -> optional enrichment, Scale -> Signals Feed, etc.). |
| 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 - page-side visitor snapshots from approved domains
- Signals Feed (WebSocket) — Scale+ plan gate details
- Rate limits — per-tier caps
- Event schema — what the collector records under each authenticated event