# Cookieless Tracking

> Plaintext version for LLMs. Canonical HTML: https://askbowtie.com/learn/tracker/cookieless-tracking/

askbowtie does not set cookies on your visitors' browsers. Not first-party, not third-party, not "analytics" cookies. This is by design, not by accident.

This page explains how it works, why it matters, and how to keep your integration cookie-free when you add server-side conversion tracking.

---

## How sessions work without cookies

The tracker stores a random session ID in the browser's **localStorage** under the key `bowtie_session`. This is fundamentally different from a cookie:

|   | Cookie | localStorage |
| --- | --- | --- |
| **Sent to server automatically** | Yes, on every HTTP request | No — never sent unless you explicitly read and pass it |
| **Readable by other domains** | Third-party cookies can be | No — same-origin only |
| **Consent obligation (EU)** | Usually yes, for analytics | Same rules apply — ePrivacy 5(3) covers any storage, not just cookies (see below) |
| **Survives incognito** | No | No |
| **Can be blocked by browsers** | Increasingly, yes | Rarely |

The session ID is random, anonymous, and not linked to any personal identity. A session ends after 30 minutes of inactivity, after which the next visit starts a fresh one. The stored value itself has no expiry we set: it lives in localStorage until the visitor clears site data, and browsers apply their own limits on top (Safari's tracking prevention clears script-writable storage after roughly a week of no interaction with the site).

---

## What this means for compliance

First, the thing most write-ups get wrong, including an earlier version of this page: **avoiding cookies is not what decides whether you need consent.**

The ePrivacy Directive's Article 5(3) is **technology-neutral**. It governs "the storing of information, or the gaining of access to information already stored, in the terminal equipment" — it does not say "cookies". Regulators have been explicit that this covers localStorage and similar mechanisms too. So the fact that we use localStorage rather than a cookie does not, by itself, put us outside the rules.

What actually matters is the *character* of the measurement:

- **Strictly first-party** — never shared with anyone, never used across sites
- **Anonymous** — a random id, no personal identifiers, no cross-device graph
- **Audience measurement only** — it exists to tell you about your own site
- **Removing the tracker leaves no trace** — no residual cookies, no orphaned data

Several EU regulators recognise an audience-measurement exemption on roughly those grounds; France's CNIL publishes explicit criteria and Germany's TTDSG is structured similarly. Those are the grounds askbowtie would rely on, and they would hold equally whether the id lived in localStorage or a first-party cookie.

**This is not legal advice, and you should confirm it against your own jurisdiction and your own configuration.** We would rather tell you the real basis for the argument than hand you a reassuring sentence that does not survive scrutiny.

Cookieless is still worth something on its own terms: nothing persists after you remove the tag, there is no third-party cookie to be blocked, and you are not adding to a cross-site profile. Those are real. They are just not, by themselves, a consent exemption.

If your site already has a banner for other tools (Google Analytics, ad pixels), whether askbowtie needs listing depends on the exemption above rather than on the absence of a cookie. Check it against your own setup.

---

## Server-side conversions without cookies

When you need to track conversions server-side (payment webhooks, form processing, CRM events), you need to get the session ID from the browser to your backend. **You do not need a cookie for this.**

### The right way: pass it explicitly

Read the session ID from localStorage and include it in whatever triggers your server-side code:

**Forms and checkouts** — hidden field:

```
<input type="hidden" name="bowtie_session" id="bowtie_session">
<script>
document.getElementById('bowtie_session').value =
  window._bowtie_session || window.bowtie?.getSessionId() || '';
</script>
```

**AJAX / SPAs** — include in the request body:

```
const sessionId = window._bowtie_session || window.bowtie?.getSessionId();

fetch('/api/checkout', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ ...formData, bowtie_session: sessionId })
});
```

**Stripe / payment processors** — pass it as metadata when creating the checkout session, then read it back in the webhook. The example is Stripe's Node SDK, but every processor has an equivalent metadata field:

```
// When creating the checkout session (server-side)
const checkout = await stripe.checkout.sessions.create({
  metadata: { bowtie_session: req.body.bowtie_session || '' },
  // ... line items, success_url, etc.
});

// In the webhook handler
const sessionId = event.data.object.metadata.bowtie_session || null;
```

**Client-side only** — if you can detect the conversion in JavaScript, no server-side code needed:

```
bowtie.converted('purchase', { value: 99.00 });
```

### Cookieless does not mean unattributable

The obvious worry with passing the id yourself is that it sounds fragile next to a cookie the browser sends automatically. Live data says otherwise, but it is worth knowing exactly what the number depends on.

One customer fires conversions from their backend on every lead. When they started, they were reading the wrong browser global, so the id arrived empty and their code fell back to a made-up one. Attribution sat at **68.3%**. They changed a single line to read the session id correctly, and it moved to **92.5%** with no cookie involved.

The reason the jump is so sharp is that attribution is binary, not gradual. Across those same conversions, a real session id was attributed **214 times out of 214**. A synthetic id was attributed **0 times out of 77**. There is no partial credit: the id either matches a real visit or it does not.

So the thing that costs you attribution is not the absence of a cookie. It is sending an id the browser never used. Read it the way shown above, carry it through your backend, and cookieless costs you nothing here. You can confirm your own number any time with the `attribution.unattributed_rate` field on `get_traffic`: under about 5% means you are wired correctly.

### What not to do

Do not mirror `bowtie_session` from localStorage to a cookie. This pattern has appeared in some integrations:

```
// Don't do this — it defeats the cookieless design
setInterval(() => {
  const s = localStorage.getItem('bowtie_session');
  if (s) document.cookie = `bowtie_session=${s};path=/;max-age=86400;SameSite=Lax`;
}, 1000);
```

This creates a cookie that:

- Gets sent to your server on every HTTP request (bandwidth waste)
- May require a cookie consent banner
- Polls every second (unnecessary CPU work)
- Undermines the "no cookies" claim you can make about your site

Every use case this cookie solves has a better alternative above.

---

## Decision guide

| Your situation | Solution | Cookie? |
| --- | --- | --- |
| Track conversions in JavaScript (thank-you page, SPA) | `bowtie.converted()` | No |
| Form submission triggers a conversion server-side | Hidden `` field | No |
| AJAX call triggers a conversion server-side | Include in request body | No |
| Payment webhook (Stripe, PayPal) | Pass as checkout metadata | No |
| Server-side error or guardrail tracking | Read from request (form/AJAX) | No |
| Backend-only event with no browser context | Send without session ID — event still tracked, just not linked to a session | No |

---

## Verifying your integration is cookie-free

Open your browser's DevTools, go to **Application > Cookies**, and check your domain. You should see:

- Your app's own session cookie, if it sets one — that's your framework, not askbowtie
- **No** `bowtie_session` cookie
- **No** `bowtie_debug` cookie (unless you manually enabled debug mode)

Under **Application > Local Storage**, you should see:

- `bowtie_session` — the anonymous session ID
- `bowtie_uid` — present only if you call `bowtie.identify()` to connect a visitor's sessions (see the JavaScript API). Sites that never call it won't have this key.

These localStorage entries are never sent to any server automatically.

---

## Related

- [Installing the Tracker](/docs/install/) — Setup instructions
- [Server-Side Conversions](/docs/) — Full server-side tracking guide
- [Privacy Policy](/privacy/) — What data is collected and how it's handled
- [Security & Data Privacy](/learn/tracker/security-and-csp/) — Technical security details
