# 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.                                                                                                              |
| `$device_id`                       | string | Set by the client SDKs. The persisted anonymous device id (see below).                                                                                                |
| `$user_id`                         | string | Set by the client SDKs, only once the visitor is identified (see below).                                                                                              |
| `token`                            | string | Per-event project token. Overrides the header and is stripped before storage.                                                                                         |
| `$distinct_id_before_identity_fix` | string | Server-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](/reference/data-model#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.

This rewrite is retroactive and cannot be undone, and it is why `reset()`
on logout matters: without it, the next person on a shared device inherits
the previous person's `$device_id` and their history gets linked into it on
first login.

## 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:

| 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](/api/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
});
```
