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
| Key | Type | Meaning |
|---|---|---|
distinct_id | string | Required. Identifies the user or device the event belongs to. |
time | number | Unix milliseconds. Optional over HTTP (defaults to server receipt time); the SDKs always set it at capture time. |
$insert_id | string | Idempotency key used for deduplication (see below). |
$lib | string | Which SDK produced the event: core, web, ios, etc. |
token | string | Per-event project token. Overrides the header and is stripped before storage. |
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.
{
"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:
| SDK | Auto-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 Native | distinct_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.
cohorly.track("Item Added", {
sku: "A-100", // string
price: 29.0, // number
currency: "USD", // string
gift: false, // boolean
});