> For the complete documentation index, see [llms.txt](https://docs.carpose.de/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.carpose.de/events/09_consent.md).

# Consent

Carposé emits JavaScript events and sends no data to any analytics provider on its own — see [Introduction](/events/01_introduction.md). It does keep functional data in the browser's `localStorage` and `sessionStorage`, which is unrelated to analytics and described under [Scope](#scope-what-the-gate-covers) below.

Optionally, Carposé can forward those events to Google Tag Manager for you, instead of you writing the listeners yourself. That built-in bridge **only ever forwards an event once the visitor has consented to analytics**.

## Enabling the tracking bridge

The bridge is off by default. There are two ways to switch it on.

### Option 1 — in the Carposé dashboard (recommended)

Enable **Google Analytics Tracking** in your account settings. Every page that embeds Carposé picks it up on the next page load — no change to your website is needed, and you can switch it off again the same way.

### Option 2 — on the integration script

Add the `data-carpose-google-analytics-tracking` attribute to the integration script tag. It overrides the account setting, which is useful to switch tracking on for one site only, or to keep it off on a staging system while it is enabled for the account:

```html
<script
  type="text/javascript"
  src="https://integration.carpose.app/integration.js"
  data-carpose-google-analytics-tracking="true"
  data-api-key="your-api-key"
></script>
```

### How the two combine

| `data-carpose-google-analytics-tracking` | Result                                           |
| ---------------------------------------- | ------------------------------------------------ |
| `"true"`                                 | Bridge active, whatever the account setting says |
| `"false"`                                | Bridge off, whatever the account setting says    |
| present but empty, or absent             | The account setting decides — off by default     |

{% hint style="info" %}
"Active" means the bridge is listening — not that anything is sent. Every single push is additionally gated by consent. Tracking enabled plus no consent still means no tracking.
{% endhint %}

The attribute is namespaced per destination. Further tracking destinations will get their own setting and their own attribute, so they can be switched independently.

## How consent is handled

The bridge **fails closed**. Under GDPR Art. 6(1)(a) and §25 TDDDG analytics requires prior opt-in, so no consent signal is treated as refusal, never as permission.

* **Nothing is forwarded until analytics consent is granted.** The listeners attach immediately — they store nothing — but each forward is checked at the moment the event fires.
* **Events fired before consent are dropped permanently.** They are never buffered and never replayed. A visitor who accepts halfway through a session is measured from that point on, not retroactively.
* **Withdrawal takes effect immediately**, on the next event, without a page reload.
* **The consent check itself writes no cookies and no storage.** It reads the consent state fresh on every page load.
* **There is no bypass.** No attribute, setting or global switches the gate off.

{% hint style="info" %}
This is stricter than delegating to Google Consent Mode alone. With plain delegation the events land in `window.dataLayer` regardless — readable by every other tag on the page — and Consent Mode replays queued tags when consent is granted later, so a "reject, then accept" visitor would still be measured retroactively. The gate prevents both.
{% endhint %}

## Scope: what the gate covers

The consent gate governs **one thing**: whether Carposé events are forwarded to an analytics destination. It does not switch parts of the app off.

Carposé keeps functional data in the browser to deliver the features the visitor is using. This storage is **not** affected by consent state and keeps working whether or not analytics consent was given:

| Stored                                                 | Where            | Purpose                                                         |
| ------------------------------------------------------ | ---------------- | --------------------------------------------------------------- |
| API response cache (5 min TTL)                         | `localStorage`   | Avoids re-fetching vehicle and settings data on every page view |
| Account settings and feature flags                     | `localStorage`   | Configuration of the embedded modules                           |
| Wishlist, comparison, recently viewed vehicles, budget | `localStorage`   | Features the visitor actively uses                              |
| Search form state, AI assistant history                | `localStorage`   | Restores the visitor's own inputs across page views             |
| Per-session UI state                                   | `sessionStorage` | Cleared when the tab closes                                     |

None of this is transmitted to Google or any other analytics provider, and none of it is used for profiling or cross-site recognition — it stays in the visitor's browser and serves the module they are looking at.

{% hint style="info" %}
Whether this functional storage requires consent on your site — under §25 (2) TDDDG, storage strictly necessary for a service the user explicitly requested is exempt — is an assessment for you and your data protection officer, based on which modules you embed. Carposé does not make that determination for you, and the analytics gate described here does not depend on it.
{% endhint %}

## Recognised consent signals

Consent counts as granted when **any** of the following reports it. A source that never reports anything is treated as unknown, which is not a grant.

### 1. Google Consent Mode v2

`analytics_storage` is read from the `consent` `default` / `update` commands in `window.dataLayer`, including commands issued before Carposé loads and any later updates. The most recent command wins.

```javascript
gtag('consent', 'update', { analytics_storage: 'granted' });
```

Supported out of the box by Usercentrics, Cookiebot, Consentmanager, Borlabs and most other commercial CMPs.

### 2. IAB TCF v2.2

Read via `window.__tcfapi`. Consent counts as granted when `gdprApplies` is `false`, or when both purpose **1** (store and access information on a device) and purpose **8** (measure content performance) are consented.

### 3. The Carposé consent API

For consent tools that speak neither of the above. Set the initial state before the Carposé script loads:

```javascript
window.carposeConsent = { analytics: true };
```

And report every change:

```javascript
window.dispatchEvent(new CustomEvent('carpose-consent-update', {
  detail: { analytics: true }   // false on withdrawal
}));
```

The event goes on `window`, not `document.body`, so it can be dispatched before the body exists. Call it from your CMP's own consent callback, both when consent is given and when it is withdrawn.

## CMP compatibility

| Consent tool             | Covered by                                      |
| ------------------------ | ----------------------------------------------- |
| Usercentrics             | Consent Mode                                    |
| Cookiebot                | Consent Mode                                    |
| Consentmanager           | Consent Mode                                    |
| Borlabs Cookie           | Consent Mode                                    |
| Complianz (WordPress)    | Consent Mode **only if configured** — see below |
| Custom / in-house banner | Carposé consent API                             |

### WordPress and Complianz

WordPress consent plugins commonly implement the **WP Consent API** (`wp_has_consent('statistics')`), which Carposé does **not** read.

This matters for Complianz, the most widespread WordPress CMP: its free version implements the WP Consent API natively, but places Google Consent Mode setup with Tag Manager behind Complianz Premium, and TCF behind a further paid add-on. A site on free Complianz may therefore emit none of the three recognised signals — in which case the gate stays shut and no analytics data is collected at all.

Such sites need the Carposé consent API wired into the plugin's consent callback:

```javascript
document.addEventListener('wp_listen_for_consent_change', function (e) {
  const changed = e.detail;
  if ('statistics' in changed) {
    window.dispatchEvent(new CustomEvent('carpose-consent-update', {
      detail: { analytics: changed.statistics === 'allow' }
    }));
  }
});
```

## Troubleshooting: no data in Google Analytics

Work through this in order:

1. **Is tracking enabled at all?** Check the **Google Analytics Tracking** setting in the dashboard, and make sure no `data-carpose-google-analytics-tracking="false"` on the script tag is overriding it.
2. **Did you consent yourself while testing?** Accept analytics in the banner, then reload. Events fired before you accepted are gone — they are not replayed.
3. **Does your CMP emit a recognised signal?** In the browser console, `window.dataLayer.filter(e => e[0] === 'consent')` should show a `consent` command containing `analytics_storage`. If it is empty and you are on WordPress, this is almost certainly the Complianz case above — wire the Carposé consent API.
4. **Is GTM itself configured?** The bridge fills `window.dataLayer`. Turning those pushes into GA4 hits is still your GTM container's job.

## Migrating from a custom tracking script

If you previously forwarded Carposé events with your own listeners or a hosted bridge script:

* **Remove the old script when you switch the toggle on.** Both will run, and every event will be counted twice.
* **Check that the old script was consent-gated.** Hand-written bridges typically push unconditionally and rely on GTM. The built-in bridge does not, which is the reason to switch.
* **Expect the numbers to drop.** After the switch you measure only consenting visitors. That is the intended outcome, not a regression, so compare periods with this in mind.
