> ## Documentation Index
> Fetch the complete documentation index at: https://docs.jun.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Consent and privacy

> What the widget stores and sends, how consent mode holds it back until a visitor agrees, and what is never captured.

The widget is built to collect as little as it can, and to mask what it does collect twice: in the visitor's browser and again on your desk. This page lists exactly what it stores and sends, and how to make it wait for cookie consent.

## Consent mode

If your site asks for consent before storing anything (a cookie banner), add `data-consent="required"` to the snippet:

```html theme={null}
<script src="https://your-desk.example.com/widget.js" data-key="wk_…" data-consent="required" defer></script>
```

Then call `JunDesk.consent(true)` when the visitor accepts, and `JunDesk.consent(false)` if they withdraw:

```js theme={null}
// Wire this to your consent banner's callbacks.
onConsentChange((accepted) => window.JunDesk?.consent(accepted));
```

`consent()` takes a boolean. You can call it as often as the visitor changes their mind.

> [!NOTE]
>
> The loader doesn't remember consent between page loads. Call `JunDesk.consent(true)` on every page where your consent tool says the visitor has already agreed.

### Before and after consent

| | Before `consent(true)` | After |
| - | - | - |
| Browser storage | Nothing. The chat's visitor token is kept in memory only, so a chat started now is lost on reload | The session in the page's `localStorage`; recent debug events in the page's `sessionStorage`; the chat's visitor token in the chat frame's `localStorage` |
| **Visitors** list | Not shown. No live connection is opened | Shown, with the page they're on |
| Chat | Works, for this page load | Works, and survives reloads |
| "Something broke" nudges and page openers | Never shown | Shown, if turned on |
| Rage clicks, dead clicks, "stuck on a page" | Not recorded | Recorded |
| `JunDesk.reportError()` and `JunDesk.track()` | Ignored | Recorded |
| JavaScript errors, failed requests, page trail | Kept in memory on the page, sent only with a message the visitor writes | Kept in this tab's `sessionStorage`, so they survive a reload; still sent only with a message the visitor writes |

`JunDesk.consent(false)` closes the live connection (the visitor drops off your **Visitors** list), deletes `jun:session` and `jun:e` from your page's storage, and deletes what the chat frame stored in `localStorage`.

Without `data-consent="required"`, the widget acts as if consent was given from the start.

> [!TIP]
>
> The **Get started** checklist detects your install through the live connection, so with consent mode it ticks only after a visitor consents. Press **I've added it** if you want to tick it yourself.

## What the widget stores

Even without consent mode, the widget stores very little, and nothing on the chat side until a visitor writes.

| Where | Key | What | When |
| - | - | - | - |
| Your page's `localStorage` | `jun:session` | The visit, shared by the browser's tabs: a random session id (96 random bits), when it started, the last activity, pages viewed, the referrer if it's another site, and the landing page's campaign tags | On page load (after consent) |
| Your page's `sessionStorage` | `jun:e` | The last 40 masked debug events from this tab, from the last 30 minutes ([Debug context](https://docs.jun.so/widget/debug-context.md)) | When an event is recorded (after consent) |
| The chat frame's `localStorage` (your desk's origin) | `jun:visitor:<widget key>` | A random visitor token. The desk keeps only its SHA-256 hash | When the visitor sends their first message |
| The chat frame's `localStorage` | `jun:ended:<widget key>` | Ids of the last 20 chats the visitor ended with the island's × | When they end one |

A session ends after 30 minutes without a page view or a visible tab, or after 24 hours; the next page load starts a new one. Older versions of the loader used `jun:s` and `jun:t` in `sessionStorage`; they're no longer used.

No cookies. Opening the chat without writing stores nothing in the chat frame. `JunDesk.logout()` replaces the session and forgets the visitor token ([Identify users](https://docs.jun.so/widget/identify-users.md#signing-out)).

## What the widget sends

**While the visitor browses** (the live connection behind the **Visitors** list):

* the page address, with query values removed (campaign tags kept, see below), and the page title, masked;
* the referrer, cleaned the same way;
* when the session started, how many pages it has viewed, its campaign tags, the browser's language and time zone.

The desk adds the country, city and a rough location (rounded to 0.1°) from Cloudflare's request data, and the user agent.

**When the visitor sends a message**, a debug snapshot goes with it: the page, browser, viewport, language, time zone, and up to 40 recent events. See [Debug context](https://docs.jun.so/widget/debug-context.md) for the full list. Set `data-capture="off"` and the snapshot has no events at all.

**When something breaks** and nudges are on, the loader sends the one masked event and the page to the desk to word a "Want a hand?" line. Nothing is stored for it.

## What is never captured

* Request or response bodies, for any request.
* Cookies, `localStorage`, `sessionStorage` or anything else your page stores.
* What visitors type into your forms.
* The text of your page. Rage clicks and dead clicks keep only the element's tag, `id`, `aria-label`, `name` and `role`, and a button's or link's visible label when it's 40 characters or shorter. "Stuck" events keep only the page path.
* Query string values. URLs keep their path and the names of query parameters (`/search?q=…`), never the values. The one exception is campaign tags: `utm_source`, `utm_medium`, `utm_campaign`, `utm_term` and `utm_content` keep their values, masked and cut to 80 characters.
* Clicks inside the widget itself, or scripted clicks.

## Masking, twice

Everything that leaves the browser is masked by the loader first. The desk then masks it again with the same rules, drops unknown fields, and caps every length, so a modified loader can't slip anything through.

| Pattern | Becomes |
| - | - |
| Email addresses | `[email]` |
| JWT-shaped tokens (`eyJ….….…`) | `[token]` |
| `Bearer …` values | `Bearer [token]` |
| Stripe-style keys (`sk_live_…`, `pk_test_…`, `rk_…`) | `[key]` |
| `password=`, `secret:`, `token=`, `api_key=`, `auth…=`, `session…=`, `cookie=` values | `[redacted]` |
| 13 to 19 digits in a row, with or without spaces or dashes (card numbers) | `[number]` |

Error messages are cut to 500 characters, stacks to their first five lines, and app error messages from `reportError()` to 300. App events from `track()` keep a name of up to 60 characters and up to 10 properties; string values are masked and cut to 100 characters, and anything that isn't a string, number or boolean is dropped. Don't send personal data in them.

## Who sees what

* **Your agents** see the debug context in the conversation's **Browser** panel, and the contact's details.
* **The AI** gets the events as plain lines in its prompt. The optional error codes from `reportError()` are for your team only: the AI answering visitors never sees them, so it can't repeat one.
* **Other visitors** never see anything about anyone else.
* **Issue drafts** ([Issues](https://docs.jun.so/inbox/issues.md)) are built from the masked context and masked again before they're sent to GitHub or Linear.

Internal notes, tags and topics never reach visitors or the AI's answers.

## Turning capture off

`data-capture="off"` stops the loader from listening for errors, failed requests, rage clicks, dead clicks and stuck pages, makes `reportError()` and `track()` do nothing, and turns off the "something broke" nudges with them. Page openers, the live visitor list and the chat still work.

> **[Debug context](https://docs.jun.so/widget/debug-context.md)**
>
> What's captured and how agents and the AI use it.

> **[Security](https://docs.jun.so/reference/security.md)**
>
> How the desk handles secrets and tokens.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.