Quickstart
Three steps, about five minutes. You need a project key, which lives in Settings → Sources in your workspace. Public keys start with pk_ and are safe to ship in client code; server keys start with sk_ and are not.
- Add the snippet to your layout — auto-capture starts immediately
- Call
identify()once you know who the user is - Track the two or three events that define activation for your product
Install the SDK
Pick whichever fits your stack. All four clients share the same queueing behaviour: events are batched every two seconds, persisted across reloads, and retried with backoff for up to 48 hours.
<!-- add to your <head>, above other scripts --> <script src="https://cdn.cadence.dev/v2/c.js" data-key="pk_live_8f2c41d9e7" data-region="eu" async></script>
# install npm i @cadence/browser // then, once, at app start import { cadence } from '@cadence/browser' cadence.init({ key: 'pk_live_8f2c41d9e7', region: 'eu', autoCapture: true, flushInterval: 2000 })
import { CadenceProvider, useCadence } from '@cadence/react' export default function App({ children }) { return ( <CadenceProvider apiKey={process.env.CADENCE_KEY} region="eu"> {children} </CadenceProvider> ) } // anywhere below it const { track } = useCadence() track('board_created', { template: 'weekly-review' })
import Cadence // in application(_:didFinishLaunchingWithOptions:) Cadence.start( key: "pk_live_8f2c41d9e7", region: .eu, autoCapture: true ) Cadence.track("board_created", [ "template": "weekly-review", "seats": 4 ])
Identify users
Until you call identify(), Cadence tracks an anonymous ID stored in a first-party cookie. When you identify, every event that anonymous ID ever sent is stitched onto the real user — retroactively, including the sessions before signup.
cadence.identify('u_1842', { email: 'nora@northwind.co', // hashed at the edge plan: 'team', company_id: 'org_204', signed_up_at: '2026-03-14' }) // group calls attach the user to an account cadence.group('org_204', { name: 'Northwind', seats: 180 })
curl https://api.cadence.dev/v1/identify \ -H "Authorization: Bearer sk_live_…" \ -H "Content-Type: application/json" \ -d '{ "user_id": "u_1842", "traits": { "plan": "team", "company_id": "org_204" } }'
Track events
An event is a name plus a flat object of properties. Keep names in the past tense and lower snake case; keep property values scalar. Nested objects are flattened one level and arrays are stored as JSON, which makes them awkward to break down by — so prefer a separate event.
cadence.track('checkout_completed', { plan: 'business', seats: 12, amount_cents: 18300, currency: 'GBP', trial: false })
cadence.track('Checkout', { // tense and case drift data: { plan: { id: 3 } }, // nested, hard to break down items: ['a', 'b', 'c'], // array, stored as JSON ts: Date.now() // Cadence stamps this already })
Naming conventions
This is the only part of setup that's genuinely worth arguing about, because renaming later is cheap in Cadence but expensive in everyone's head. The rules we suggest:
- object_verb, past tense —
board_created, notcreate_boardorBoard Created - One event per meaning. If you need an
ifto read the chart, split it into two events - Properties describe the event, traits describe the person. Plan goes on the user; the plan they were on at purchase goes on the event
- Never put a value in the name.
plan_upgradedwithplan: 'business'beatsupgraded_to_business
Cadence flags near-duplicates automatically and offers to merge them. Merges apply to historical data too, so cleaning up in month three doesn't cost you month one.
Warehouse import
Connect Snowflake, BigQuery or Redshift read-only and Cadence will pull modelled tables on a schedule. This is how you get retention curves that start before your install date.
| Column | Type | Required | Notes |
|---|---|---|---|
event_name | string | Yes | Mapped through the same naming rules as SDK events |
user_id | string | Yes | Must match the ID you pass to identify() |
occurred_at | timestamp | Yes | UTC. Rows older than your retention window are skipped |
properties | variant / json | No | Flattened one level on ingest |
_row_hash | string | No | Supply it and re-syncs become idempotent |
Sensitive data
Mark a property sensitive and it is hashed or dropped at the edge — before it is written to storage, not after. Session replay masks every input value by default; you opt individual fields in.