> 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.

# Consent & privacy

Pivolio implements Google **Consent Mode v2**. Consent isn't a single on/off
switch — each category removes a specific part of the event, and the effects are
independent.

## The four categories

`consent` on an event carries `ad_storage`, `analytics_storage`, `ad_user_data`
and `ad_personalization`.

Consent is enforced in **two different places**, and conflating them causes a lot
of confusion. The browser SDK trims what it collects; the server decides whether
a destination may be sent to at all.

### In the browser SDK

| Category             | Denied means                                              |
| -------------------- | --------------------------------------------------------- |
| `ad_storage`         | The event is **not sent at all**                          |
| `ad_user_data`       | **PII traits are stripped** — email, phone, name, address |
| `ad_personalization` | **Ad click IDs are stripped**                             |

These run client-side, before the event leaves the page. An `ad_storage` denial
means there is no delivery to inspect. The other two produce an event that still
arrives, just with less signal — and correspondingly lower
[match quality](/documentation/core-concepts/match-quality).

### On the server

The backend does not strip fields. It uses consent for one decision only: the
**per-destination gate** described below. With enforcement on, a denied required
category means the destination is skipped entirely rather than sent a
reduced payload.

So an `ad_user_data` denial reaching a destination that requires it — the
default — produces a `skipped` delivery, not a delivery with the PII removed.

> **Note**
>
> **Unset is treated as not-denied.** Only an explicit `denied` restricts
> anything. If you never call `setConsent()`, events flow with full signal — which
> is the right default for regions without a consent requirement, and the wrong
> one if you need opt-in. Set consent explicitly before the first event where
> consent is required.

Denying `ad_storage` through `setConsent()` also **immediately discards pending
events** from the buffer. Events already sent are gone; anything still queued
locally is not sent.

## Three independent kill switches

Three separate mechanisms stop tracking, and they don't substitute for one
another. Knowing which one is active saves a lot of confused debugging.

**`respectDNT`** `boolean`

Init-time config. When `true` and the browser sends Do Not Track, tracking is
disabled entirely. Evaluated once, at init.

---

**`optOut()`** `method`

**Durable and persisted.** Survives reloads and new sessions. An opted-out
visitor is fully inert on later page loads — no identity call, no automatic
page view, no events at all. Reverse it with `optIn()`; check it with
`hasOptedOut()`.

---

**`ad_storage: 'denied'`** `consent`

Runtime, and **not persisted**. It applies to the current page's tracker and
is gone on the next load unless you set it again — so a consent banner must
restore its stored decision on every page.

---

`optOut()` is the one to use for a durable user request. Consent state is for
per-page-load regulatory gating, and forgetting that it isn't persisted is the
most common consent bug.

## PII hashing

No raw PII reaches an ad platform. Traits are normalized and **SHA-256** hashed
to hex before delivery. Normalization is what makes the digest match the
platform's — the same person hashed two different ways is two different people.

Three rules cover everything:

* **`strip()` then `lower()`** — email, first name, last name, city, state, zip,
  country.
* **Digits and a leading `+` only** (E.164-ish), **not** lowercased — phone.
* **Byte-for-byte, no normalization at all** — Meta's `external_id` only, per
  Meta's spec. Your own ID must arrive exactly as you store it.

> **Warning**
>
> The empty check runs on the **normalized** result, not the input. A phone
> recorded as `"N/A"` normalizes to an empty string — hashing that would produce a
> real-looking digest matching nobody, and every such record would look identical.
> Values that normalize to empty hash to nothing instead and are simply omitted.

That guard is why placeholder junk in your CRM degrades gracefully rather than
poisoning match rates with a phantom shared identity.

## Per-destination enforcement

Consent enforcement is configured **per destination**, so you can hold a
platform to stricter requirements than another. When enforcement is on and a
required category is denied, the send is skipped — the event still records, with
a `skipped` [delivery status](/documentation/core-concepts/delivery-statuses).

The default required categories are **`ad_user_data`** and **`ad_storage`**.

> **Note**
>
> A skipped-for-consent event is not a failure and is never retried. Retrying
> wouldn't help: the visitor's answer isn't going to change because you asked the
> API again.

People whose consent state blocks delivery are labelled with the
`consent_denied` segment — see [Identity resolution](/documentation/core-concepts/identity-resolution).