> 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 and consent

Everything on this page is client-side behaviour, shared by every
[SDK binding](/documentation/sd-ks/overview). For what the backend does with the identifiers once
they arrive, see [Identity resolution](/documentation/core-concepts/identity-resolution).

## What the SDK stores

Keys are prefixed with `storagePrefix`, which defaults to `__ut_`. Cookies are
written `SameSite=Lax`, with `Secure` per the `secureCookies` option. Reads try
cookie first, then `localStorage`, then `sessionStorage` — so clearing one layer
doesn't lose the visitor.

| Key      | Where                   | Lifetime   | Holds                              |
| -------- | ----------------------- | ---------- | ---------------------------------- |
| `aid`    | Cookie + localStorage   | 365 days   | `anonymous_id` (`anon_<uuid>`)     |
| `sid`    | Cookie + sessionStorage | 30 minutes | Session id (`sess_<uuid>`)         |
| `ss`     | Cookie + sessionStorage | 30 minutes | Session start time                 |
| `ls`     | Cookie + sessionStorage | 30 minutes | Last-seen timestamp                |
| `uid`    | Cookie + localStorage   | 365 days   | `user_id` after `identify()`       |
| `cmp`    | Cookie + localStorage   | 30 days    | Campaign / UTM parameters          |
| `cids`   | Cookie + localStorage   | 30 days    | Captured click IDs                 |
| `optout` | Cookie + localStorage   | 365 days   | Durable opt-out flag               |
| `q`      | localStorage only       | Until sent | Offline queue, max 100 events FIFO |

The day figures track `anonymousIdTTL` and `attributionTTL`, the minutes track
`sessionTimeout` — see [Configuration](/documentation/sd-ks/configuration).

## The `/id` call and Safari ITP

On init the SDK issues `GET /id` and adopts the `anonymous_id` the server
returns, alongside an httpOnly cookie the server sets for 365 days.

The reason is Safari's Intelligent Tracking Prevention: **a cookie written by
JavaScript is capped at roughly 7 days**. A visitor who returns after a fortnight
looks brand new, breaking the link between the ad click and the purchase. A
cookie set by the server in an HTTP response header isn't subject to that cap, so
the server-issued id survives.

The fetch is **non-blocking**. Events sent before it resolves use the
client-generated id, and the backend stitches the two together during identity
resolution. Nothing is dropped waiting for it.

> **Note**
>
> This only helps when the collector is same-site with your pages. Serving it from
> your own subdomain via [first-party domains](/documentation/guides/first-party-domains)
> is what makes the server cookie first-party rather than third-party.

## Cross-domain linking

Cookies can't cross registrable domains. To carry one visitor between
`example.com` and `example-checkout.com`, append the linker parameter to outbound
links:

```ts
import { getLinkerParam } from '@pivolio/browser';

const href = `https://example-checkout.com/cart?${getLinkerParam()}`;
```

`getLinkerParam()` returns `_ut=<url-encoded anonymous_id>`. On the receiving
page the SDK reads `_ut` from the URL — and then applies one rule that matters:

> **Warning**
>
> An inbound `_ut` is adopted **only when the visitor has no stored id yet**. A
> returning visitor's id is never overridden.

Without that rule, anyone could craft a link carrying their own id, share it, and
merge every recipient into one person — corrupting the identity graph and, since
identity drives conversion attribution, your reporting with it. Seeding a *fresh*
visitor is safe; overwriting a known one is not.

`_ut` is **always stripped from the address bar**, adopted or not, so a third
party's id can't bleed into the captured page URL and get logged as page context.

For subdomains of one site you don't need the linker at all — set
`cookieDomain: '.example.com'` and the cookie is shared.

## Click ID capture

Click IDs arrive by two routes, and the SDK treats them differently.

**From the URL**, on any page load — 11 parameters: `fbclid`, `gclid`, `gbraid`,
`wbraid`, `ttclid`, `li_fat_id`, `msclkid`, `epik`, `rdt_cid`, `sc_click_id`,
`twclid`. These are persisted for `attributionTTL` days and **merged**, not
replaced. A visitor who arrives from Google and later returns from Meta keeps
both, because the server can only backfill a click it still knows about.

**From platform cookies**, re-read fresh on every single event — `fbp` from
`_fbp`, `fbc` from `_fbc`, `ttp` from `_ttp`. These are written by Meta's and
TikTok's own pixels if you also run them. They're read live rather than cached
because the platform pixel can rewrite them mid-session, and a stale value is
worse than none: it lowers your match quality while looking like a signal.

> **Note**
>
> Only 4 of the 9 platforms Pivolio recognizes are delivery destinations — Meta,
> Google Ads, TikTok and Reddit. The other click IDs are captured purely for
> [attribution](/documentation/core-concepts/attribution).

## UTM parameters

`utm_source`, `utm_medium`, `utm_campaign`, `utm_content` and `utm_term` map to
`campaign.source`, `.medium`, `.name`, `.content` and `.term`.

Unlike click IDs, **a new campaign replaces the stored one**. The asymmetry is
deliberate. Click IDs are per-platform credentials — losing one loses a delivery
match. A campaign is a single answer to "how did this visitor last get here", and
merging two campaigns produces a chimera that belongs to neither.

## Consent

`setConsent()` takes a Google Consent Mode v2 object. Three categories change
behaviour, and **unset is treated as not-denied**:

```ts
import { setConsent } from '@pivolio/browser';

setConsent({
  ad_storage: 'granted',
  ad_user_data: 'granted',
  ad_personalization: 'denied',
});
```

* **`ad_storage: 'denied'`** — the event is not sent at all.
* **`ad_user_data: 'denied'`** — PII traits are stripped; the event still sends.
* **`ad_personalization: 'denied'`** — ad click IDs are stripped.

Denying `ad_storage` also **immediately discards anything still buffered**, so
events captured before the visitor declined are never flushed afterwards.

## Three independent kill switches

They are not variations of one setting, and turning one off doesn't turn the
others off.

* **`respectDNT`** — init-time. When the browser sends Do Not Track and this is
  enabled, the tracker never initializes.
* **`optOut()`** — durable and persisted. Survives reloads via the `optout` key.
  Reverse it with `optIn()`; query it with `hasOptedOut()`.
* **`ad_storage: 'denied'`** — runtime, and **not persisted**. Re-apply it on
  each page load from your own consent store.

An opted-out visitor is fully inert on later loads: no `/id` call, no auto page
view, no events. The local `anonymous_id` is kept — it backs the public getters —
but nothing ever leaves the device.

## Next steps

* [Configuration](/documentation/sd-ks/configuration) — TTLs, cookie domain, `respectDNT`.
* [Identity resolution](/documentation/core-concepts/identity-resolution) — what the backend does next.
* [Attribution](/documentation/core-concepts/attribution) — how a click becomes credit.