Event schema
Every event ingested by the collector conforms to one of nine types below. All events share a common envelope (visitorId, sessionId, timestamp, page, device), and type-specific extensions add the fields that are meaningful for that event.
TypeScript definitions for every event type ship in the @clickstream/shared-types package; importing from there gives you the exact surface described below.
Event types
| Type | Emitter | When |
|---|---|---|
pageview | SDK | Every page load + SPA route change (if autoTrackPageviews: true). |
click | SDK | Every trusted click on an element the SDK decides to capture (see auto-capture rules). |
scroll | SDK | Throttled scroll-depth samples at 10 %, 25 %, 50 %, 75 %, 100 %. |
form | SDK | Form submit, focus, blur, and field change (universal form capture). |
custom | SDK / server | Anything you call trackEvent() on. Most common carrier for conversion events. |
identify | SDK / server | tracker.identify(email) / server-side POST /v1/events with identify payload. |
email_open | server | Pixel fetch from an email tracking pixel. |
email_click | server | Campaign redirect hit (wraps the destination URL). |
_consent_transition | SDK | Internal — fires when the visitor changes consent state. Never carries page content. |
Common envelope
Every event has these fields:
interface BaseEvent {
type: EventType; // one of the types above
visitorId: string; // first-party _cs_uid cookie value
sessionId: string; // SDK session id (resets after 30 min idle)
timestamp: number; // ms since epoch (server overrides if >5 min skew)
page: {
url: string; // full URL of the current page
path: string; // pathname only
title: string; // document.title at event time
referrer?: string; // document.referrer
};
device: {
userAgent: string; // navigator.userAgent (truncated to 512 chars)
viewport: { width: number; height: number };
fingerprint?: string; // composite device fingerprint
fingerprintConfidence?: number; // 0–100 — confidence the fingerprint is stable
gpuVendor?: string;
gpuRenderer?: string;
connectionType?: string; // '4g', '3g', 'wifi', …
pixelRatio?: number;
timezone?: string; // IANA, e.g. 'America/New_York'
language?: string; // 'en-US'
platform?: string; // 'MacIntel', 'iPhone', 'Linux'
};
}
Type-specific fields
pageview
Same as the envelope, with no extensions. The server derives scroll depth, time-on-page, and FCP / DCL / load timings from the next scroll / _attention_summary / _performance events the SDK fires later in the session.
click
interface ClickEvent extends BaseEvent {
type: 'click';
element: {
selector: string; // unique CSS selector (truncated to 256 chars)
text?: string; // inner text (PII-scrubbed, truncated)
href?: string; // for anchor clicks
tagName: string; // 'BUTTON', 'A', 'DIV', …
classList?: string[];
id?: string;
};
clickX?: number; // viewport-relative X coordinate
clickY?: number; // viewport-relative Y coordinate
}
Auto-capture rules: the SDK captures clicks on <button>, <a>, <input type="submit">, elements with role="button", and elements with data-track="1". Everything else is ignored unless you call trackClick(element) manually.
scroll
interface ScrollEvent extends BaseEvent {
type: 'scroll';
depth: number; // % of page scrolled (0–100)
}
form
interface FormEvent extends BaseEvent {
type: 'form';
action: 'submit' | 'focus' | 'blur' | 'change';
formId?: string; // <form id=…> or selector-derived
fieldCount?: number; // number of inputs in the form
formSelector?: string; // unique selector for the form
fieldName?: string; // for focus/blur/change events
textExcerpt?: string; // redacted text preview (universal form capture)
}
custom
interface CustomEvent extends BaseEvent {
type: 'custom';
name: string; // e.g. 'signup_started'
category?: string; // e.g. 'conversion'
action?: string; // e.g. 'click'
label?: string; // e.g. 'cta_primary'
value?: number; // numeric value, used for LTV modeling
}
identify
interface IdentifyEvent extends BaseEvent {
type: 'identify';
hem?: string; // SHA-256 hashed email (lowercase trim)
hemMd5?: string; // MD5 hashed email (for LiveRamp/TTD)
hashedPhone?: string; // SHA-256(E.164 phone)
customerId?: string; // your CRM ID
accountId?: string; // B2B account id
// Click-ID attribution:
gclid?: string; gbraid?: string; wbraid?: string;
fbclid?: string; msclkid?: string; ttclid?: string; twclid?: string;
sccid?: string; epik?: string; irclickid?: string; _kx?: string;
dclid?: string; li_fat_id?: string;
// Social login IDs:
googleId?: string; facebookId?: string; linkedinId?: string; appleId?: string;
// Mobile advertising:
maid?: string; maidType?: 'idfa' | 'gaid';
// Cross-site journey:
clickstreamId?: string; referringClickstreamId?: string;
// UTM attribution:
utmSource?: string; utmMedium?: string; utmCampaign?: string;
utmTerm?: string; utmContent?: string;
}
Hashing is client-side. The SDK hashes email + phone before they leave the browser. Raw PII never hits the wire (except on the decrypt flow, which is password-gated and audited).
email_open / email_click
interface EmailEvent extends BaseEvent {
type: 'email_open' | 'email_click';
campaignId: string;
campaignCode: string; // ClickStream cs_cid value
linkedId?: string; // cs_lid — resolves to a known visitor
}
These events are emitted by the collector when the campaign-redirect endpoint is hit. The campaign system wraps every tracked link in an email; when the user clicks, the cs_cid + cs_lid parameters flow through to the collector and land as email_click events, then 302 to the destination.
Analytics Engine field mapping
The collector writes every event into the clickstream_events dataset in Cloudflare Analytics Engine. Field mappings (20 blobs + 20 doubles + 1 index):
Index: clientId (multi-tenant scope).
Blobs (string fields — pipe-delimited encoding to maximize the 20-field limit):
| Blob | Fields |
|---|---|
blob1 | event_type | event_name |
blob2 | page_url |
blob3 | page_path |
blob4 | referrer |
blob5 | session_id |
blob6 | visitor_id |
blob7 | device_type | browser | os | gpu |
blob8 | country | city |
blob9 | element_selector | element_text |
blob10 | form_id | custom_category | custom_action | custom_label |
blob11 | hmacHem | hemMd5 — primary identity key |
blob12 | hmacPhone — secondary identity key |
blob13 | customer_id | account_id | user_id | crm_contact_id | order_id |
blob14 | clickstream_id | referring_clickstream_id |
blob15 | maid | maid_type |
blob16 | google | fb | linkedin | apple (social IDs) |
blob17 | reserved (freed — was ad-tech IDs; operator sites have no vendor pixels) |
blob18 | click IDs: gclid | fbclid | msclkid | ttclid | dclid | gbraid | wbraid | li_fat_id | campaignCode |
blob19 | UTM: source | medium | campaign | term | content |
blob20 | ip_hash (SHA-256 of IP; never the raw value) |
Doubles (numeric fields):
| Double | Field |
|---|---|
double1 | timestamp (ms) |
double2 | scroll_depth_percent |
double3 | time_on_page_ms |
double4, double5 | viewport_width, viewport_height |
double6, double7 | click_x, click_y |
double8 | form_field_count |
double9 | custom_value |
double10, double11, double12 | page_load_time_ms, dom_content_loaded_ms, fcp_ms |
double13 | fingerprint_confidence (0–100) |
double14, double15 | is_vpn, connection_type |
double16, double17 | latitude, longitude |
double18, double19 | bot_score, is_bot |
double20 | reserved |
The blob layout is stable across releases — events written under a given mapping always deserialize against the same schema, regardless of when they were ingested.
Ingestion endpoint
POST https://t.example.com/v1/events (use your registered first-party tracking domain).
- Auth:
X-API-Keyheader (withevents:writepermission) or?key=<apiKey>query param (forsendBeacon). - Batch: one event per request, or up to 25 events in a single
{ events: [...] }batch. - Rate limit: tier-dependent — see Rate limits.
- Validation: Zod schema applied server-side; invalid events return
400 validation_errorwith adetailsarray showing the failing field + message. - Domain gate: if the API key has configured domains, the request
Origin/Referermust match.403 domain_not_allowedotherwise.
Example request:
POST /v1/events HTTP/1.1
Host: t.example.com
X-API-Key: cs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Origin: https://example.com
Content-Type: application/json
{
"type": "pageview",
"visitorId": "vis_abc123",
"sessionId": "sess_xyz456",
"timestamp": 1713797640000,
"page": { "url": "https://example.com/pricing", "path": "/pricing", "title": "Pricing" },
"device": { "userAgent": "Mozilla/5.0 …", "viewport": { "width": 1280, "height": 800 } }
}
Response (202):
{ "success": true, "accepted": 1, "received": 1, "timestamp": 1713797640592 }
See also
- Signals API — read the scored snapshot derived from these events
- Signals Feed (WebSocket) — stream every scored event in real time
- API keys + auth — permission scopes + rotation
- Rate limits — per-tier ingestion caps + overage behavior