Install ClickStream
Website properties install through their own first-party tracking domain. You add a CNAME on t.yourdomain.com → ClickStream, we verify it and provision SSL, then the browser SDK loads from your hostname. Mobile and server properties instead use dedicated provenance-exempt keys with the shared https://feynman.clickstream.com endpoint; they do not require DNS.
For a website, the dashboard DNS flow in First-party tracking is what makes the install first-party. You can fire your first events before finishing it (see below), but do not ship without it. Mobile and server installs skip DNS entirely.
What you need before you start
- A ClickStream account at einstein.clickstream.com with at least one property created. There is no self-serve key-minting API — creating the property in the dashboard is the one step you cannot script.
- An API key minted from the dashboard. Which key depends on the property type (next section).
- For a website, a first-party tracking subdomain (e.g.
t.example.com) — recommended, but not a prerequisite for seeing data. Ingestion is gated on the page'sOrigin/Referermatching your registered domains, not on which hostname served the SDK, so a bundler install withoutendpointwill report events before you touch DNS. The CNAME is what buys first-party cookie persistence (a browser-aligned ~400-day cookie lifetime instead of 7 days under Safari ITP) and immunity from third-party script blocking. That is a better reason than "required", and it is the honest one.
About the website key
The website key (cs_live_* / cs_test_*) is public by design: you paste it into data-key in your own page HTML, so anyone who views source can read it. The collector protects it with the domain gate rather than with secrecy, which is why shipping it in a browser bundle is safe.
The dedicated cs_mob_live_* and cs_srv_live_* keys are the opposite: they skip the domain gate, so they are real secrets. Keep them out of browser code and public env vars.
Pick your platform first
ClickStream has three property types. The type you create in Einstein decides which key you get and how you install. Pick the one that matches what you are tracking:
| Property type | What it tracks | Key you get | How you install |
|---|---|---|---|
| Website | A web property with a domain and DNS. | Your shared account key (cs_live_*), domain-gated. | Script tag or @clickstreamhq/sdk — runs in the browser. |
| Mobile app | A native iOS / Android / React Native app. No URL, no DNS. | A dedicated mobile key (cs_mob_live_*), provenance-exempt. | @clickstreamhq/react-native or direct REST from Swift / Kotlin. |
| Server | Backend / server-to-server event ingestion. No URL, no DNS. | A dedicated server key (cs_srv_live_*), provenance-exempt; event writes are HMAC-signed. | @clickstreamhq/node (signs for you), or signed REST. The same key is what @clickstreamhq/next uses to read Signals server-side. |
Website keys require a browser Origin/Referer that matches your configured domains. Mobile and server keys skip that gate — that is what lets a native app or a backend call the collector directly with no spoofed Origin and no fake web URL. Server event writes replace browser provenance with a timestamped HMAC signature. Create the matching property type in the dashboard; do not reuse a website key off the web.
Choose the install for the outcome you want
Most websites should start with the script tag, then add Edge capture when they want proof for traffic that does not run JavaScript.
| Goal | Install this | Result |
|---|---|---|
| Measure human behavior on a website | Browser SDK script tag or @clickstreamhq/sdk | Pageviews, clicks, forms, sessions, identity, and behavior scores. |
| Use Signals in page code | @clickstreamhq/signals or @clickstreamhq/react | Human-only UI and lane-aware behavior. |
| Measure AI/search and crawler coverage | Cloudflare Edge capture | Non-JavaScript requests appear in Human + AI Traffic and Signals coverage proof. |
| Stream events into your own systems | Signals Feed | Real-time labeled event feed for Scale tier and above. |
| Track a native mobile app | @clickstreamhq/react-native or direct REST with a cs_mob_live_* key | Screen views, taps, identity events, and Signals snapshots — no DOM, no Origin. |
| Send events from a backend | Signed REST with a cs_srv_live_* key (device.clientPlatform: 'server') | Server-to-server custom/identify events with no browser provenance. |
Script tag (recommended — all stacks)
Paste this into your site's <head>:
<script
src="https://t.example.com/sdk/v2.js"
data-key="cs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
async
></script>
Replace t.example.com with your registered tracking domain. Put this tag on every page; the SDK handles SPA route changes internally.
The loader is not the gate — /sdk/v2.js always serves (it is an immutable, SRI-stable shim that injects the real bundle from /sdk/bundle.js). Enforcement happens at ingestion: POST /v1/events requires an Origin/Referer whose hostname matches a domain registered for your key, or it answers 403 domain_not_allowed. A registered domain covers itself and every subdomain, and localhost / 127.0.0.1 are always allowed so local development works with a production key.
That's it. Pageview / click / session events start flowing to the dashboard within seconds.
Installing through Google Tag Manager
Do not paste the plain <script src=".../sdk/v2.js"> tag into a GTM Custom HTML tag — it will silently do nothing. The loader shim finds its own element with document.currentScript, which is null for a script a tag manager injects at runtime, so it returns before booting the SDK. A hand-pasted tag in page HTML is parser-inserted and works; that is why this only breaks under GTM.
Use the tag-manager form instead, which sets the config on window and loads the bundle directly:
<script>
window.ClickstreamConfig = {
apiKey: 'cs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
endpoint: 'https://t.example.com',
replay: { enabled: true }
};
</script>
<script async src="https://t.example.com/sdk/bundle.js"></script>
endpoint is required here. The window.ClickstreamConfig path does not auto-derive it from the script origin the way the script-tag path does, and an absent endpoint falls back to the shared collector — which quietly costs you the first-party install you set up DNS for.
In GTM: Custom HTML tag, trigger All Pages, fire Once per page, leave "Support document.write" unchecked.
Cloudflare edge capture for crawlers and answer engines
The browser SDK is the source of truth for human interaction. Search crawlers, answer engines, link previews, monitors, and other non-JavaScript clients often never run that SDK, so ClickStream also supports a first-party Cloudflare Worker install.
Use both layers when you want complete Signals coverage:
- Script tag or SDK: human pageviews, clicks, forms, identity, behavior scores.
- Cloudflare Worker: crawler and answer-engine labels, geo context, and machine-readable page hints before HTML is returned.
The onboarding Add Code → Cloudflare Edge Capture tab generates a Worker snippet with your API key and tracking hostname already filled in. Paste it into a Cloudflare Worker or Pages _worker.js in front of your site. See Edge capture for the full install, verification commands, and expected dashboard display.
After both layers are installed, use Signals API for page-code decisions and Signals coverage proof to confirm that human traffic, AI/search coverage, monitoring, automation, and review traffic stay in separate lanes.
NPM / pnpm / yarn (modern web apps)
For build-tool-managed browser apps, use @clickstreamhq/sdk to install the same first-party pixel you would otherwise paste as a script tag. The public package intentionally exposes the pixel installer helpers only; the full browser tracker runs from your first-party tracking domain after the helper injects the script.
pnpm add @clickstreamhq/sdk
import { installClickstreamPixel } from '@clickstreamhq/sdk';
installClickstreamPixel({
apiKey: 'cs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
endpoint: 'https://t.example.com', // your first-party tracking domain — required
replay: true,
});
The helper appends <script src="https://t.example.com/sdk/v2.js" data-key="…" data-endpoint="…"> for you, copies safe data-* options, and propagates a CSP nonce to the real bundle. With a first-party endpoint the key rides in data-key only — no ?key= in the URL. Point endpoint at the same t.example.com hostname you verified in the dashboard. Events sent from an unregistered hostname are rejected with 403 domain_not_allowed.
Omitting endpoint is allowed: the helper warns on the console and loads the shared loader at https://feynman.clickstream.com/sdk/v2.js. That is a fine way to see your first events, but it is third-party to your site, so ship the first-party endpoint before you rely on the data.
replay is opt-in — leave it out and session replay stays off.
Public exports:
installClickstreamPixel(options)→{ script, alreadyInstalled, src }uninstallClickstreamPixel(id?)→booleanisClickstreamPixelInstalled(id?)→booleanbuildClickstreamPixelUrl(options)→string
Use the browser global exposed by the loaded pixel for event calls:
import { installClickstreamPixel } from '@clickstreamhq/sdk';
installClickstreamPixel({
apiKey: 'cs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
endpoint: 'https://t.example.com',
});
// Later — in an event handler, not on the line after install().
button.addEventListener('click', async () => {
window.clickstream?.trackEvent({ name: 'docs_filter_changed', category: 'interaction' });
await window.clickstream?.identify('user@example.com');
});
installClickstreamPixel() only appends the loader tag; the loader then fetches the real bundle, which is what defines window.clickstream. It is not available on the next statement. Always use window.clickstream?.… — without the optional chaining an early call throws, and with it an early call silently does nothing.
A custom event delivers exactly name, category, action, label, and value. There is no free-form metadata field.
Full reference: Browser pixel helper.
React — @clickstreamhq/react
pnpm add @clickstreamhq/sdk @clickstreamhq/react @clickstreamhq/signals
// app/providers.tsx (or similar root component)
'use client';
import { useEffect } from 'react';
import { installClickstreamPixel } from '@clickstreamhq/sdk';
import { ClickStreamProvider } from '@clickstreamhq/react';
export function Providers({ children }: { children: React.ReactNode }) {
useEffect(() => {
installClickstreamPixel({
apiKey: process.env.NEXT_PUBLIC_CLICKSTREAM_KEY!,
endpoint: 'https://t.example.com',
replay: true,
});
}, []);
return (
<ClickStreamProvider
apiKey={process.env.NEXT_PUBLIC_CLICKSTREAM_KEY!}
endpoint="https://t.example.com"
>
{children}
</ClickStreamProvider>
);
}
ClickStreamProvider configures the Signals client. The pixel installer loads the tracker that creates _cs_vid (the visitor id; _cs_uid holds the cross-site clickstream id) / _cs_sid, sends events, and exposes window.clickstream.identify() and window.clickstream.trackEvent() for the React hooks.
'use client';
import { useVisitor, useIdentify, useTrack } from '@clickstreamhq/react';
export function HelpPanelButton() {
const { ctx: visitor } = useVisitor();
const identify = useIdentify();
const track = useTrack();
const isHuman = visitor?.behavioralClass === 'human' && visitor?.bot.isBot === false;
return (
<button
onClick={async () => {
track({ name: 'help_panel_opened', category: 'interaction' });
await identify('user@example.com');
}}
>
{isHuman ? 'Open help panel' : 'View help'}
</button>
);
}
Full reference: React adapter.
Next.js (App Router) — @clickstreamhq/next
Next.js apps usually combine three pieces: the browser pixel installer for tracking, the React provider for client components, and the Next adapter for Server Components / Route Handlers / edge middleware.
Two keys, two env vars, and the distinction matters: the browser pieces take the website key in NEXT_PUBLIC_CLICKSTREAM_KEY (public by design), while the middleware prefetch and getServerVisitor() take a server key in CLICKSTREAM_API_KEY — never prefixed NEXT_PUBLIC_, or you ship a provenance-exempt secret to every browser.
pnpm add @clickstreamhq/sdk @clickstreamhq/next @clickstreamhq/react @clickstreamhq/signals
// app/providers.tsx
'use client';
import { useEffect } from 'react';
import { installClickstreamPixel } from '@clickstreamhq/sdk';
import { ClickStreamProvider } from '@clickstreamhq/react';
export function Providers({ children }: { children: React.ReactNode }) {
useEffect(() => {
installClickstreamPixel({
apiKey: process.env.NEXT_PUBLIC_CLICKSTREAM_KEY!,
endpoint: 'https://t.example.com',
replay: true,
});
}, []);
return (
<ClickStreamProvider
apiKey={process.env.NEXT_PUBLIC_CLICKSTREAM_KEY!}
endpoint="https://t.example.com"
>
{children}
</ClickStreamProvider>
);
}
// middleware.ts — optional edge prefetch of VisitorContext
import { clickStreamMiddleware } from '@clickstreamhq/next/middleware';
export default clickStreamMiddleware({
apiKey: process.env.CLICKSTREAM_API_KEY!, // server key (cs_srv_live_*) when prefetch: true
endpoint: 'https://t.example.com',
prefetch: true,
});
export const config = { matcher: ['/docs', '/account/:path*', '/settings/:path*'] };
// app/docs/page.tsx — Server Component
import { getServerVisitor } from '@clickstreamhq/next/server';
export default async function DocsPage() {
const { visitor } = await getServerVisitor({
apiKey: process.env.CLICKSTREAM_API_KEY!, // server key: cs_srv_live_*
endpoint: 'https://t.example.com',
});
if (visitor && visitor.scores.intent >= 70) return <DetailedDocs />;
return <DefaultDocs />;
}
Full reference: Next.js adapter.
Vue.js
Vue apps can use either the script-tag install or @clickstreamhq/sdk. The browser pixel exposes window.clickstream for simple page code and window.cs for the tracker instance. Wrap the bridge in an idiomatic composable:
<!-- public/index.html -->
<script
src="https://t.example.com/sdk/v2.js"
data-key="cs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
async
></script>
// src/composables/useClickstream.ts
import { onMounted, ref } from 'vue';
export function useClickstream() {
const tracker = ref<any>(null);
onMounted(() => {
const cs = (window as any).clickstream;
if (!cs) {
console.warn('[clickstream] SDK loader not yet ready');
return;
}
tracker.value = cs;
});
return tracker;
}
Use tracker.value?.trackEvent({ name: 'help_panel_opened' }) inside components. Note that onMounted can run before the bundle finishes loading — read window.clickstream at call time (as above, with optional chaining) rather than caching it once at mount, or the composable can capture null for the life of the component. A dedicated @clickstreamhq/vue adapter is not currently shipped.
Mobile apps — @clickstreamhq/react-native
Native mobile is its own property type with its own key. Create a Mobile app property in Einstein (no URL, no DNS) — it mints a dedicated cs_mob_live_* key that is exempt from the browser Origin gate. That key, plus a device.clientPlatform field on every event, is the whole mobile contract. There is no spoofed Origin header and no fabricated web URL.
For React Native and Expo, use the supported package:
pnpm add @clickstreamhq/react-native @react-native-async-storage/async-storage
import AsyncStorage from '@react-native-async-storage/async-storage';
import { AppState } from 'react-native';
import { createClickStream } from '@clickstreamhq/react-native';
export const cs = createClickStream({
apiKey: 'cs_mob_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', // Mobile app key
endpoint: 'https://feynman.clickstream.com', // shared collector for mobile keys
storage: AsyncStorage,
appStateProvider: AppState,
});
cs.screen('CheckoutScreen', { title: 'Checkout' });
cs.tap('add_to_cart', { value: 1 });
cs.identify('user@example.com', { customerId: 'user_12345' });
The tracker is Hermes-safe (no crypto.subtle, no DOM), batches and persists events through AsyncStorage, rotates sessions on AppState foreground transitions, and re-exports Signals on the /signals subpath. For native iOS (Swift URLSession) and Android (Kotlin OkHttp), use direct REST with the same mobile key and device.clientPlatform.
For the full native pattern — React Native, Swift, Kotlin, identity hashing, native bot semantics, and Signals reads — see Mobile apps.
Server / backend — cs_srv_live_*
To send events from a backend, create a Server property in Einstein. It mints a cs_srv_live_* key that is provenance-exempt from browser Origin checks but must sign every event request — an unsigned request on a server key is rejected with 401 server_signature_required. Use a logical route as page.path (no http URL required); device.clientPlatform: 'server' is stamped from the key posture when you omit it, and device.userAgent / device.viewport are optional off the web path.
For Node.js 20+, use the official package. It freezes event IDs and exact wire bytes across retries, batches at the collector's 25-event cap, and exposes accepted/duplicate/dropped/rejected accounting:
npm install @clickstreamhq/node
import { createClickStreamServer } from '@clickstreamhq/node';
const clickstream = createClickStreamServer({
apiKey: process.env.CLICKSTREAM_SERVER_API_KEY!,
requireConsent: true,
consent: { analytics: true, identityResolution: true },
});
await clickstream.track({
type: 'custom',
visitorId: 'cs_visitor_abc',
sessionId: 'cs_session_xyz',
name: 'subscription_renewed',
page: { path: 'billing/renewal', title: 'Renewal' },
});
await clickstream.close();
See the full Node.js server SDK guide. The lower-level signing contract remains available for non-Node runtimes:
import { createHmac, randomUUID } from 'node:crypto';
const apiKey = process.env.CLICKSTREAM_SERVER_API_KEY!;
const idempotencyKey = 'billing-renewal-event-01JXYZ'; // persist across retries
const signatureTimestamp = String(Math.floor(Date.now() / 1000));
const body = JSON.stringify({
type: 'custom',
visitorId: 'cs_visitor_abc',
sessionId: 'cs_session_xyz',
timestamp: Date.now(),
name: 'subscription_renewed',
page: { path: 'billing/renewal', title: 'Renewal' },
device: { userAgent: 'acme-backend/1.0', viewport: { width: 0, height: 0 }, clientPlatform: 'server' },
});
const canonical = ['v1', signatureTimestamp, 'POST', '/v1/events', idempotencyKey, body].join('\n');
const signature = 'v1=' + createHmac('sha256', apiKey).update(canonical).digest('hex');
await fetch('https://feynman.clickstream.com/v1/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey,
'Idempotency-Key': idempotencyKey,
'X-CS-Timestamp': signatureTimestamp,
'X-CS-Signature': signature,
},
body,
});
The canonical signing payload is exactly:
v1\n<unix-seconds>\nPOST\n/v1/events\n<idempotency-key-or-empty>\n<wire-body>
and the header value is v1= followed by 64 lowercase hex characters.
X-CS-Timestampis Unix seconds (10–11 digits) and must be within five minutes of the collector, or you get401 server_signature_expired.- A missing signature is
401 server_signature_required; a bad one is401 invalid_server_signature. All three carryWWW-Authenticate: ClickStream-HMAC realm="events", version="v1". - The key must arrive in the
X-API-Keyheader. A query-string credential never satisfies the server contract. - Sign the exact bytes you send, including compressed bytes when using
Content-Encoding: gzip. - Idempotency is mandatory in production. Every event needs an
_eid, or the request needs anIdempotency-Keyheader (from which the collector derives per-event ids). Without either, the collector answers400 idempotency_required. TheIdempotency-Keyis part of the signature, so reusing the same key and body after a timeout replays the identical signed request and the durable ledger returns a duplicate instead of accepting the event twice. - A successful ingest is
202 Accepted, not 200.
Next.js apps can read VisitorContext server-side with @clickstreamhq/next/server using the same server key. See Next.js adapter and Event schema for the full server-to-server contract.
Plain HTML (no bundler)
The script-tag install works as-is. If you need to reach the tracker from inline handlers:
<script
src="https://t.example.com/sdk/v2.js"
data-key="cs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
async
></script>
<button onclick="clickstream?.trackEvent({ name: 'download_pressed' })">Download</button>
Verifying the install
- Open the dashboard, pick your site, and watch the Live Sessions panel.
- Load any page on your site in an incognito tab. You should see a session appear within 1–2 seconds.
- If nothing arrives after 30 seconds:
- Open DevTools → Network and filter for your tracking domain.
/sdk/v2.jsshould return 200 and/v1/eventsshould return 202 Accepted. A403 domain_not_allowedon/v1/eventsmeans the page's Origin doesn't match your configured domains. - Check that
/sdk/bundle.jsalso loaded. If/sdk/v2.jsreturned 200 but no bundle request follows, the loader could not see its own script element — that is the tag-manager case above. - Confirm the API key in
data-keymatches the key shown in the dashboard. - Confirm the loading origin is listed in the site's Allowed Domains. A registered domain already matches all of its subdomains; explicit
*.example.comwildcards work too but add nothing. - Re-run
dig +short t.example.comand confirm the answer resolves tofeynman.clickstream.com(see First-party tracking).
- Open DevTools → Network and filter for your tracking domain.
Next steps
- First-party tracking (required) — end-to-end CNAME + SSL provisioning walkthrough.
- Signals API — read live scores from page code.
- Mobile apps — native app event ingestion and Signals reads.
- Edge capture — measure crawlers and answer engines at the domain edge.
- Signals Feed (WebSocket) — real-time stream of every labeled event (Scale+).
- Event schema — exact shape of every event the SDK emits.
- API keys + auth — key rotation + reserved scope labels.
- Rate limits — per-tier caps.