> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.pivolio.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.pivolio.com/_mcp/server.

# Identity resolution

A visitor browses on their phone, converts on a laptop a week later, and gives
you an email at checkout. Those are three anonymous IDs and one human. Identity
resolution is the step that works that out — and it's what lets a conversion
carry the click ID from a visit that happened days earlier.

## Identifiers

Every event is broken down into the identifiers it carries. Fifteen kinds
participate:

`anonymous_id`, `user_id`, `email`, `phone`, `device_id`, `fbclid`, `gclid`,
`ttclid`, `li_fat_id`, `msclkid`, `crm_contact_id`, `epik`, `rdt_cid`,
`sc_click_id`, `twclid`

Email and phone are **normalized before matching**, using the same rules as
[PII hashing](/documentation/core-concepts/consent-privacy) — so `Ada@Example.com ` and `ada@example.com` are one
identifier, not two.

## Matching

The extracted identifiers are looked up against your identity graph, and the
number of people they hit decides what happens:

| Matches | Outcome                                                           |
| ------- | ----------------------------------------------------------------- |
| **0**   | A new person is created, owning these identifiers.                |
| **1**   | The event links to that person; any new identifiers are attached. |
| **N**   | The people are **merged into the oldest** one.                    |

Merging into the oldest is deliberate. The oldest record has the longest
history, so keeping it preserves the earliest touchpoints — which is exactly
what first-touch and time-decay [attribution](/documentation/core-concepts/attribution) depend on. The
losing records' identifiers are relinked and their rollups absorbed, so nothing
is discarded.

Merges happen when a single event bridges two previously separate identifier
sets — the classic case being a logged-in checkout on a device that had only
ever been anonymous.

## What `identify()` does

`identify()` attaches known traits to the current person:

```ts
identify({
  user_id: 'usr_1041',
  email: 'ada@example.com',
  first_name: 'Ada',
});
```

Two things follow. A `user_id` marks the person **identified**, and the traits
become available for hashing and delivery on subsequent events — improving
[match quality](/documentation/core-concepts/match-quality) at every destination.

> **Note**
>
> `identify()` only emits an event when something actually changed. Calling it on
> every page render is harmless — repeated identifies with identical data are
> skipped rather than billed as volume.

## Click backfill

Ad platforms match conversions best when the conversion carries the click ID
from the ad that produced it. But the click happens on a landing page, and the
conversion often happens days later on a page with no query string at all.

So every click ID seen on a landing page is stored against the person with a
per-platform expiry. When a **conversion** event arrives carrying **no click
IDs**, unexpired clicks for that person are backfilled — most recent wins per
platform.

> **Warning**
>
> Backfill is skipped entirely if the conversion already carries *any* click ID.
> The click the visitor actually arrived on is always more truthful than one
> reconstructed from history.

### Expiry windows

Each platform's window matches its own attribution policy — sending a click ID
older than the platform accepts just produces an unmatched conversion.

| Platform  | Click IDs                   | Expiry  |
| --------- | --------------------------- | ------- |
| Google    | `gclid`, `gbraid`, `wbraid` | 90 days |
| Microsoft | `msclkid`                   | 90 days |
| LinkedIn  | `li_fat_id`                 | 90 days |
| Pinterest | `epik`                      | 30 days |
| Reddit    | `rdt_cid`                   | 30 days |
| TikTok    | `ttclid`                    | 28 days |
| Snapchat  | `sc_click_id`               | 28 days |
| X         | `twclid`                    | 14 days |
| Meta      | `fbclid`                    | 7 days  |

Meta's seven days is the tightest window by a wide margin. If your sales cycle
runs longer than a week, a Meta-attributed conversion frequently arrives after
its `fbclid` has expired — collect `email` and `phone` so there's still
something to match on.

Backfill only triggers on **conversion** events. See
[the taxonomy](/documentation/core-concepts/events) for which names qualify.

### gbraid and wbraid

Both are captured and stored, and both count for
[attribution](/documentation/core-concepts/attribution) — but neither is ever used to stitch identity.

They're Google's privacy-aggregated iOS click IDs, and aggregation is the
point: a single value can represent many users. Treating one as an identifier
would merge unrelated people into a single record, which is a far worse failure
than a missed match. `gclid` has no such problem and is used normally.

## Person segments

Resolved people are labelled with segments you can filter and report on:

* **`identified`** — a `user_id` has been seen for this person.
* **`paying`** — the person has a purchase in their history.
* **`anonymous_high_intent`** — not yet identified, but showing intent-tier
  activity. See [the taxonomy](/documentation/core-concepts/events).
* **`consent_denied`** — the person's consent state blocks delivery. See
  [Consent & privacy](/documentation/core-concepts/consent-privacy).