# The banner runtime and the consent cookie

`banner.js` installs script blocking, loads your product's consent settings and exposes `window.LepstoConsent` to your page. It stores the visitor's choice in `lepsto_consent` and applies that choice on later visits.

Load it synchronously before trackers. The runtime cannot block a tracker that has already executed.

## Three fixed things about the snippet

Copy the snippet from `consent_embed_get` or **Install on your site** in the app. Replace `<YOUR_PRODUCT_ID>` below with your product id.

```html
<!-- Lepsto consent: must be the first script in <head>. Do not add async or defer — trackers would run before consent is known. -->
<script src="https://public.lepsto.com/<YOUR_PRODUCT_ID>/consent/banner.js" data-product-id="<YOUR_PRODUCT_ID>"></script>
```

Keep three properties of this snippet:

- **Synchronous loading.** Do not add `async` or `defer`; blocking must be installed before tracker scripts or inline loaders execute.
- **First script in `<head>`.** Trackers parsed before the banner are outside its reach.
- **Matching product and URL.** The runtime reads `data-product-id`, falling back to the UUID in `src`, and derives its base URL from that same element. This prevents accidentally duplicated tags from mixing one product's id with another tag's origin.

Keep the installation comment with the tag.

## The bundle

`GET https://public.lepsto.com/<YOUR_PRODUCT_ID>/consent/banner.js` returns the shared runtime with a product-specific prelude containing:

- Non-necessary provider hosts and their categories, used by the blocker.
- The theme and customization, used for the banner's first frame.

The response is cached for **five minutes** in the browser and any CDN. Its strong `ETag` is computed from the assembled bytes. Provider and theme changes alter it; revalidation returns `304` when the bytes are unchanged. An edit appears on the next request reaching the server, or within about five minutes as caches refresh.

If configuration cannot be read while serving the bundle, the server still returns it with the default theme.

## What the runtime does in the page

Startup proceeds in this order:

1. Install the blocker synchronously. See [Script blocking](/privacy/script-blocking).
2. Set Google Consent Mode storage signals to `denied`, with `wait_for_update`.
3. Install `window.LepstoConsent`.
4. Read the stored choice. Apply a valid choice for a returning visitor, or show the banner when no valid choice exists.

### The banner is isolated from your page

The runtime appends a `<div>` to `<body>` and renders the banner in an open shadow root. `:host { all: initial }` and explicit inherited-property overrides isolate it from page styles.

It checks that its stylesheet applied. If a policy such as strict `style-src` prevents that, it removes the host element and renders no banner. Check your browser console and policy if the UI does not appear.

Errors during blocker installation, startup, configuration fetching, Consent Mode updates, resource restoration or storage cleanup are caught and warned about once instead of being thrown into your page. Errors relevant to the visitor appear in the preferences panel.

### The cookie

The browser writes `lepsto_consent` after a decision. The server does not send it through `Set-Cookie`. It reads this name first, then the legacy `lessly_consent` cookie. If only the legacy cookie is present, the banner migrates its saved choice automatically to `lepsto_consent`.

| Attribute | Value |
|---|---|
| `Path` | `/` |
| `Max-Age` | 31536000 seconds (12 months) |
| `SameSite` | `Lax` |
| `Secure` | Enabled on `https:` |
| `HttpOnly` | No; the runtime must read it |
| `Domain` | Derived as described below |

Its base64url-encoded JSON value contains the category flags, method, format version, timestamp, region, stamp, record id and provider-set versions used for the choice.

On a custom domain, the runtime probes candidate domains with a cookie and uses the first the browser accepts. The browser rejects public suffixes, leaving the registrable domain you own. Non-domain hosts and IP addresses use a host-only cookie. A derivation failure also falls back to host-only.

You can [narrow the cookie domain](/privacy/configuration). A cookie from another product on the same registrable domain is ignored, since its choice applies to that product's providers.

### The perimeter note

When the cookie covers a domain and its subdomains, the banner displays *Your choice applies to this site and its subdomains*. This notice has a fixed position and cannot be configured.

## Returning visitors

A valid stored choice normally suppresses the banner. The runtime applies it to Consent Mode and shows the floating icon or connects your custom preferences trigger.

Adding a provider to an allowed category increments that category's provider-set version. Returning visitors with an older version are asked again, with their earlier choice filled in. The old choice stays in force until they decide again. Moving a provider into a category also changes that category's version.

Renaming, changing hosts or cookies, reordering and deleting providers do not prompt again. Visitors who denied every optional category are not re-prompted for additions because no allowed category has expanded. Cookie expiry still requires a new choice.

## `window.LepstoConsent`

| Member | Result |
|---|---|
| `openPreferences()` | Opens preferences |
| `getDecision()` | Returns a frozen snapshot of the current choice, or `null` |
| `withdrawAll()` | Withdraws all optional categories |
| `onChange(fn)` | Calls `fn` when the choice changes |

Connect a settings button after the runtime is available:

```html
<button onclick="window.LepstoConsent.openPreferences()">Cookie settings</button>
```

For analytics you start yourself, apply both the current choice and later updates. Supply your own `startAnalytics` and `stopAnalytics` implementations:

```js
// startAnalytics and stopAnalytics are yours to write.
let started = false;

function applyDecision(decision) {
  const allowed = decision?.statistics === true;
  if (allowed && !started) {
    startAnalytics();
    started = true;
  } else if (!allowed && started) {
    stopAnalytics();
    started = false;
  }
}

applyDecision(window.LepstoConsent.getDecision());
window.LepstoConsent.onChange(applyDecision);
```

The initial call covers returning visitors. The subscription covers first-time decisions and later withdrawals. `onChange` can fire even when `statistics` has not changed; `started` avoids starting the same integration twice. Omit that flag only if your start and stop functions already handle repeated calls safely.

Before a choice, `getDecision()` returns `null`, which this example treats as not allowed. Later calls return new snapshots; an older snapshot does not update itself.

### Calling it before the runtime installs

If code executes before the runtime is installed, initialize a queue:

```html
<script>
  window.LepstoConsent = window.LepstoConsent || [];
  window.LepstoConsent.push(['openPreferences']);
</script>
```

`window.LesslyConsent` remains a legacy alias for the same runtime object; use `window.LepstoConsent` in new code. The runtime drains the queue on installation and accepts only the four members above. A queued `getDecision` cannot return a value to the caller; call it directly once the runtime is ready. After installation, use direct method calls instead of the queue.

API changes follow the same process as panel changes: create a linked record, rewrite the cookie, update Consent Mode, block withdrawn categories and attempt to clear their storage.

Newly denying a category reloads the page because previously executed trackers cannot be undone. If your app handles stopping them itself, disable that reload:

```js
await window.LepstoConsent.withdrawAll({ reload: false });
```

## Next steps

- [Script blocking](/privacy/script-blocking): resource interception and restoration.
- [Configure the banner](/privacy/configuration): appearance, preferences trigger and cookie domain.
- [Consent records](/privacy/consent-records): the server record behind each choice.
