@clickstreamhq/sdk
@clickstreamhq/sdk is the public browser pixel helper package. It does one job: install the ClickStream browser pixel from your verified first-party tracking domain.
The package does not expose ClickStream's internal tracker classes. The browser tracker itself is served by your tracking hostname at https://t.example.com/sdk/v2.js, then it sends events to https://t.example.com/v1/events and exposes the small runtime bridge on window.clickstream.
Use this package when a framework or build system makes a static <script> tag inconvenient.
Which key belongs here
Only a website key — cs_live_* or cs_test_*. A website key is designed to be readable: it is the data-key attribute in your own page HTML, so anyone who views source has it. The collector protects it with an Origin/Referer gate against your registered domains rather than with secrecy, which is why it is safe to ship in a browser bundle.
The dedicated cs_mob_live_* and cs_srv_live_* keys are exempt from that gate (Mobile apps, Node.js server SDK). They are real secrets — never put one in browser code, a public env var, or a bundled config.
Install
pnpm add @clickstreamhq/sdk
import { installClickstreamPixel } from '@clickstreamhq/sdk';
installClickstreamPixel({
apiKey: 'cs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
endpoint: 'https://t.example.com',
replay: true,
});
The helper appends this script for you:
<script
id="clickstream-sdk"
src="https://t.example.com/sdk/v2.js"
data-key="cs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
data-endpoint="https://t.example.com"
data-replay="true"
async
></script>
The key rides in data-key, not the URL: when you pass a first-party endpoint, the helper deliberately omits ?key= so the credential stays out of request URLs and proxy logs. ?key= is appended only in the two cases where the loader has no other way to read it — when you omit endpoint (shared-collector fallback) or when you pass an explicit scriptUrl.
Replace t.example.com with the tracking domain shown as active in Einstein. The collector rejects events when the request origin is not registered for the API key (403 domain_not_allowed). A registered domain also covers every subdomain, and localhost / 127.0.0.1 are always allowed so local development works with a production key.
installClickstreamPixel throws if there is no document (it is browser-only) and if apiKey is empty. If you omit both endpoint and scriptUrl it logs a console.warn and falls back to the shared loader at https://feynman.clickstream.com/sdk/v2.js — that works, but it is third-party to your site and posts to the shared collector, so you lose first-party cookie persistence.
Public API
import {
installClickstreamPixel,
uninstallClickstreamPixel,
isClickstreamPixelInstalled,
buildClickstreamPixelUrl,
} from '@clickstreamhq/sdk';
installClickstreamPixel(options)
Adds the pixel script to document.head and returns { script, alreadyInstalled, src } — the HTMLScriptElement, whether a script with the same id was already present, and the resolved loader URL.
const installed = installClickstreamPixel({
apiKey: 'cs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
endpoint: 'https://t.example.com',
replay: true,
followMe: true,
campaignParams: ['ref', 'promo'],
formBridgeOrigins: ['https://forms.example.com'],
compliance: 'standard',
});
console.log(installed.alreadyInstalled); // true → an existing tag was reused, nothing was added
console.log(installed.src); // "https://t.example.com/sdk/v2.js"
When a script with the same id already exists and replaceExisting is not set, the helper returns that element with alreadyInstalled: true and does not touch the DOM — calling it twice is safe.
Options:
| Option | Type | Notes |
|---|---|---|
apiKey | string | Required. Website key (cs_live_* / cs_test_*). Throws when empty. |
endpoint | string | Your first-party tracking domain, such as https://t.example.com. Omitting it warns and falls back to https://feynman.clickstream.com/sdk/v2.js. |
scriptUrl | string | Full loader URL override. Most sites should use endpoint instead. |
id | string | Script element id. Defaults to clickstream-sdk. |
replaceExisting | boolean | Replace an existing script with the same id. Default false. |
debug | boolean | Enable SDK debug logging. |
replay | boolean | Session replay capture. Opt-in — default off. The site's compliance profile can still block it server-side. |
followMe | boolean | Enable ClickStream link decoration across allowed domains. |
campaignParams | string[] | Extra query parameter names to capture as campaign codes. |
formBridgeOrigins | string[] | Customer-owned iframe origins allowed to post explicit form events. |
collectPii | ('address' | 'dob')[] | Opt-in form-field categories to capture. Default: none. Payment card / CVV / SSN / government ID / bank / health fields are a hard floor — no value here can enable them. |
compliance | 'standard' | 'gdpr_strict' | 'hipaa' | 'ccpa' | Initial compliance preset copied onto the script tag. |
nonce | string | CSP nonce for strict CSP sites. Propagated to the real bundle by the loader. |
integrity | string | SRI hash when using a pinned scriptUrl. |
crossOrigin | '' | 'anonymous' | 'use-credentials' | Cross-origin attribute for SRI installs. |
referrerPolicy | ReferrerPolicy | Referrer policy for the loader request. |
data | Record<string, string | number | boolean | null | undefined> | Extra data-* attributes. Keys may be camelCase or kebab-case. |
target | HTMLElement | Parent node. Defaults to document.head, then document.body. |
uninstallClickstreamPixel(id?)
Removes the script tag by id and returns true when a tag was removed, false otherwise. This is mainly useful in tests, previews, and single-page demos that mount/unmount complete app shells. Removing the tag does not unload an already-running tracker.
uninstallClickstreamPixel();
isClickstreamPixelInstalled(id?)
Returns true when a script tag with that id exists.
if (!isClickstreamPixelInstalled()) {
installClickstreamPixel({ apiKey, endpoint });
}
buildClickstreamPixelUrl(options)
Builds the loader URL without touching the DOM. Use it when you need to render a script tag yourself. It accepts apiKey, endpoint, and scriptUrl only — remember to also set data-key on the tag you render, because a first-party URL carries no ?key=.
const src = buildClickstreamPixelUrl({
apiKey: 'cs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
endpoint: 'https://t.example.com',
});
// "https://t.example.com/sdk/v2.js"
Runtime bridge
Once the bundle boots it sets window.clickstream (the scriptable bridge), plus window.cs and window.Tracker (the tracker instance itself). The bridge is exactly these methods:
interface ClickstreamBridge {
identify(email: string, phone?: string): Promise<void>;
identifyNow(email: string, phone?: string): Promise<void>; // identify + flush
identifyPhone(phone: string): Promise<void>;
identifyPhoneNow(phone: string): Promise<void>;
identifyProviderMatch(match: IdentityProviderMatch): Promise<void>;
trackEvent(options: CustomEventOptions): void;
trackEventNow(options: CustomEventOptions): Promise<void>; // track + flush
flush(): Promise<void>;
captureFormSubmission(
formId: string,
values: Record<string, unknown>,
options?: { source?: string; identify?: boolean; sendFormEvent?: boolean },
): Promise<void>;
getVisitorId(): string;
getClickstreamId(): string;
getSessionId(): string;
getDeliveryQueueStatus(): {
pendingEvents: number;
pendingBytes: number;
acceptingEvents: boolean; // false while the durable queue is in backpressure
state: string;
mode: string;
reason?: string;
};
}
window.clickstream?.trackEvent({ name: 'help_panel_opened', category: 'interaction' });
await window.clickstream?.identify('user@example.com');
await window.clickstream?.identifyPhone('+14155551234');
await window.clickstream?.captureFormSubmission('lead-form', {
email: 'user@example.com',
company: 'Example Inc',
});
const visitorId = window.clickstream?.getVisitorId();
const clickstreamId = window.clickstream?.getClickstreamId();
The bridge appears asynchronously — the loader injects the real bundle, which then boots. Always use optional chaining (window.clickstream?.…) or wait for it; there is no ready event.
A custom event carries exactly five deliverable fields: name, category, action, label (strings) and value (number). There is no free-form metadata slot anywhere in the pipeline — fold extra data into label yourself (for example a compact JSON string; the collector accepts up to 4096 characters there).
trackEventNow() and flush() matter when you want to read after you write: they flush the queue before resolving, so a following @clickstreamhq/signals read can reflect the event you just sent.
Use @clickstreamhq/signals to read scored visitor context. Use the runtime bridge to send events.
React example
'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>
);
}
The installer creates the visitor/session cookies and starts event capture. The provider configures Signals reads for React hooks.
Minimal tracker — @clickstreamhq/sdk/core
The package also ships a tiny self-contained tracker on the ./core subpath for sites that want a small bundle without loading the full pixel from the tracking domain. It is a published export (@clickstreamhq/sdk/core), built against a 15 KB-gzipped size budget (the current minified build is well under it), and trades away identity resolution, replay, consent management, and the behavioral interaction trackers for size.
pnpm add @clickstreamhq/sdk
import { IdentityTrackerCore } from '@clickstreamhq/sdk/core';
const tracker = new IdentityTrackerCore({
apiKey: 'cs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
endpoint: 'https://t.example.com',
});
tracker.init(); // idempotent; fires the first pageview
tracker.trackEvent({ name: 'signup_clicked' }); // `name` is the only field core accepts
void tracker.flush(); // returns a promise; await it where you can
Exports of the subpath: the IdentityTrackerCore class plus the types CoreConfig, CoreEvent, CoreDeliveryReport, and CoreDeliveryDiagnostic.
Instance methods: init(), trackEvent({ name }), trackPageview(), getVisitorId(), getSessionId(), flush() (async), destroy().
CoreConfig: apiKey (required), endpoint (defaults to https://feynman.clickstream.com), debug, batchSize (25), flushIntervalMs (5000), sessionIdleMs (30 min), maxQueueSize (1000), queueOverflowPolicy ('retain' — lossless by default), onDelivery, onDeliveryDiagnostic.
What core does: first-party visitor id in the _cs_vid cookie (365-day sliding expiry, SameSite=Lax), session id in _cs_sid (30-minute idle), pageview on init and on SPA route changes, click auto-capture for <button>, <a>, [role="button"] and [data-track], scroll depth, custom events, batch flush every 5 s and on visibility-hidden via sendBeacon, and a durable stable-id retry queue.
What core does not do: identity resolution (identify / HEM / phone / customer id), session replay, consent banner and CMP detection, device fingerprinting, form-fill capture, the eight behavioral trackers, cross-subdomain cookie sharing — and it does not install the window.clickstream bridge. It also has no identify() at all.
Compatibility runs one way. trackEvent({ name }) is a strict subset of the full SDK's custom-event surface, so code written against core keeps working after you switch to the full bundle; code that passes category / action / label / value will not type-check against core, which accepts { name } only.
@clickstreamhq/signals still works alongside core: core writes the _cs_vid / _cs_sid cookies that the Signals visitor-id resolver falls back to when no window.clickstream bridge is present.
Most browser apps should use
installClickstreamPixel()(above), which loads the full tracker from your first-party domain. Reach for@clickstreamhq/sdk/coreonly when bundle size matters more than identity, replay, and behavioral scoring.
What this does not do
- The pixel helper does not run in Node.js or native mobile screens. It needs
document. - The pixel helper does not expose an importable
IdentityTrackerclass. For a bundled tracker, use the@clickstreamhq/sdk/coresubpath above; for native apps use@clickstreamhq/react-native; for backends use@clickstreamhq/node. - Neither surface replaces Edge capture for crawlers and answer engines that do not execute JavaScript.
- Neither replaces direct native app ingestion. See Mobile apps.
See also
- Install — full install matrix.
- Signals API — read live visitor context from page code.
- React adapter — hooks over the Signals client and runtime bridge.
- Mobile apps — native screen/event ingestion.