# Identity from the server

> How ClickClacks decides which person a server event belongs to: distinct_id, anonymous_id and session_id, identify from your backend, and joining server events to a visitor's browser history.

- Canonical URL: https://clickclacks.io/docs/server-identity
- Section: Server-side
- Last updated: 2026-09-26

Server events land on the same people as browser events. Which person comes down to the IDs you send; this page is the exact rules.

## The one rule {#rule}

**Use the same user ID everywhere.** The ID you pass to [`identify`](https://clickclacks.io/docs/identify.md) in the browser is the `distinct_id` you send from your server. Pick a stable internal ID, such as a database key, not an email address.

## With distinct_id {#distinct-id}

An item with a `distinct_id` (and no `anonymous_id`) goes to the person who owns that ID:

- **If a browser has already identified with it,** the event joins that person, with all of their browser history.
- **If no browser has yet,** ClickClacks creates the person from the ID. When a browser later calls `identify` with the same ID, it’s linked to them, and its anonymous visits come along. The order doesn’t matter.

```ts title="webhook.ts"
// Your webhook handler, after a payment succeeds
clickclacks.track({
  event: 'Invoice paid',
  distinctId: invoice.userId,        // the same ID the browser passes to identify
  insertId: `inv_${invoice.id}_paid`,
  properties: { amount_cents: invoice.amountCents, $revenue: invoice.amountCents / 100, $currency: 'USD' },
})
```

## Identify from the server {#identify}

An `identify` item sets a person’s traits from your backend, which is often the better place: your database knows their plan and seat count for certain. Traits come from the newest `$identify`, whichever side sent it. Identify calls are free.

```json
{
  "items": [
    {
      "type": "identify",
      "distinct_id": "user_8412",
      "properties": { "plan": "pro", "seats": 12, "company": "Acme" }
    }
  ]
}
```

## With anonymous_id {#anonymous-id}

Sometimes a server event belongs to a specific visit before anyone has signed in: a cart checkout, a form handled by your backend. Send the browser’s anonymous ID (`per_…`) as `anonymous_id` and the event lands on that browser’s person.

The tracker keeps it in local storage under `ccp` (and the session under `ccs`, in session storage). Pass them to your server with the request:

```ts title="checkout.ts"
// In the browser: send the tracker's IDs along with a request.
// ccp (local storage) holds the anonymous ID; ccs (session storage) the session.
await fetch('/api/checkout', {
  method: 'POST',
  headers: {
    'content-type': 'application/json',
    'x-clickclacks-anonymous-id': localStorage.getItem('ccp') ?? '',
    'x-clickclacks-session-id': sessionStorage.getItem('ccs') ?? '',
  },
  body: JSON.stringify(cart),
})
```

```ts title="server.ts"
// On your server: attach the event to that browser, and its session.
const anonymousId = req.headers['x-clickclacks-anonymous-id']
const sessionId = req.headers['x-clickclacks-session-id']

clickclacks.track({
  event: 'Checkout started',
  distinctId: user?.id,                                     // when signed in
  anonymousId: /^per_/.test(anonymousId) ? anonymousId : undefined,
  sessionId: /^ses_/.test(sessionId) ? sessionId : undefined,
  properties: { items: cart.items.length },
})
```

- An `identify` item with both `distinct_id` and `anonymous_id` links that browser to the ID, exactly as calling `identify` in the browser would.
- A browser that already belongs to an ID isn’t moved to a different one.
- An anonymous ID that doesn’t start with `per_` is refused with `invalid_anonymous_id`. Before the tracker has run, or after the visitor [opted out](https://clickclacks.io/docs/consent.md), there isn’t one: leave the field out.

## Sessions {#sessions}

A server event without a `session_id` belongs to no browser session, so it never creates or lengthens one and never changes session counts. Pass the browser’s `ses_…` ID when the event is part of a visit and you want it in that session’s path, as in the example above.

## Timing {#timing}

Links between IDs are recorded just after the request is accepted, so a person’s timeline can take a moment to show a brand-new link. Reports read identity when they run, so a later `identify` also joins events that were sent before it.

## Next steps {#next}

- [Send events](https://clickclacks.io/docs/api.md): the item fields in full.
- [Identify people](https://clickclacks.io/docs/identify.md): the browser side, and reset on sign-out.
- [Node SDK](https://clickclacks.io/docs/node.md): the same fields in camelCase.
