Event properties

Every event carries a properties object. A few keys are special - they drive identity, timing, deduplication, and attribution - while everything else is free-form data you define. The SDKs set the special keys for you; when calling the HTTP API directly, set them yourself.

Required and reserved keys

KeyTypeMeaning
distinct_idstringRequired. Identifies the user or device the event belongs to.
timenumberUnix milliseconds. Optional over HTTP (defaults to server receipt time); the SDKs always set it at capture time.
$insert_idstringIdempotency key used for deduplication (see below).
$libstringWhich SDK produced the event: core, web, ios, etc.
$device_idstringSet by the client SDKs. The persisted anonymous device id (see below).
$user_idstringSet by the client SDKs, only once the visitor is identified (see below).
tokenstringPer-event project token. Overrides the header and is stripped before storage.
$distinct_id_before_identity_fixstringServer-stamped, not client-sent. Present on any event whose distinct_id was rewritten by an identity link - the id the row was originally stored under (see below).

Device and user identity

Client SDKs (@cohorly/web, React, Next.js, React Native, iOS, Android) stamp two identity keys alongside distinct_id, and the pair is what lets you tell a browser apart from the person using it:

  • $device_id is the anonymous id minted the first time the SDK runs and persisted from then on. It survives identify(), so pre-login and post-login activity stay attributable to the same device. reset() mints a new one - that is the "different person, same browser" case.
  • $user_id is only present once you have called identify(). Its absence is meaningful: an event without it came from a visitor you have not identified yet.

A caller-supplied value always wins over the SDK's, on both keys. Installs predating the anonymous-id flag are treated as anonymous rather than being given a fabricated $user_id.

Server SDKs do not set either key - there is no device to speak of, and every call already carries an explicit distinct_id.

The first post-identify event that carries a $device_id/$user_id pair Cohorly has not linked yet is enough to fold the device's earlier, anonymous history onto the identified user - no separate call is required, and this happens whether or not your SDK also calls /alias (see Identity). The rewrite stamps $distinct_id_before_identity_fix on every event it moves, so the original id an event was stored under is always recoverable even after the link.

Deduplication with $insert_id

Every SDK-generated event gets a unique $insert_id (a UUID). Because retries can resend a batch that already landed, the server treats $insert_id as an idempotency key: an event with an id that already exists for the project is not inserted twice. This makes the at-least-once delivery of the SDK queues safe - a flush that fails, is retried, and then succeeds twice still produces exactly one stored event.

json
{
"event": "Purchased",
"properties": {
"distinct_id": "user_123",
"time": 1751600000000,
"$insert_id": "b1f2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d",
"amount": 49
}
}

When calling /track directly, supply your own stable $insert_id if you want the same dedup guarantee across retries. Omit it and each request is treated as a distinct event.

Time is unix milliseconds

time is stored as a bigint of unix milliseconds. If you send seconds (a common mistake) your events will land in 1970. When omitted over HTTP, the server stamps receipt time; the SDKs stamp the moment track() is called so offline/queued events keep their true timestamp.

Auto-captured properties

The SDKs enrich every event with context so you do not have to. What gets added depends on the platform:

SDKAuto-added
@cohorly/web$browser, $os, $current_url, screen size, $lib = web
CohorlySwift (iOS)$lib = ios, $os, $os_version, $model, $screen_width/$screen_height
Core / React Nativedistinct_id, time, $insert_id, $lib, plus your super properties

Custom properties

Any keys beyond the reserved ones are stored verbatim in the jsonb properties column and become available for filtering and breakdowns in queries. Use consistent naming and value types per key so segmentation stays clean.

ts
cohorly.track("Item Added", {
sku: "A-100", // string
price: 29.0, // number
currency: "USD", // string
gift: false, // boolean
});
PreviousData model