First-touch attribution
Most conversions are not won by the last click. Someone reads a newsletter in May, comes back through a Google search in July, and buys. Last-touch attribution credits Google. First-touch attribution credits the newsletter.
Kymo records both, and the Goals page shows them side by side.
How it works
The Kymo visitor hash rotates at UTC midnight, so it cannot connect a visit in May to a purchase in July, and that is deliberate — see Cookieless identity.
First touch solves the problem a different way. On the first visit, beat.js writes one first-party localStorage key on your own domain, holding the traffic source of that visit. Every later beacon carries that value along. The visitor is never identified across days. Only the traffic source persists, and a traffic source is not a person.
What is stored
One key, kymo_ft, holding a small JSON object. Each field is optional, is capped at 200 characters, and is left out when empty:
| Field | Type | Required | Description |
|---|
A real value looks like this:
{"s":"newsletter","m":"email","c":"launch","l":"/pricing","t":"2026-05-02T09:14:00.000Z"}Three things it is not:
- Not a cookie. A cookie travels with every request for every image and stylesheet. This value is read only when a beacon is built.
- Not an identifier. There is no visitor id, no random id, and nothing that describes a person. Two people who arrive from the same campaign store the same value.
- Not cross-domain.
localStorageis per origin. The key is written on your domain and can only be read by your domain.
Expiry
A stored first touch expires after 13 months, which is the CNIL limit for audience measurement exempt from consent. On the first pageview after it expires, beat.js discards it and records a fresh first touch from the current page.
Switching it off
Add data-attribution="off" to the script tag:
<script defer src="https://kymo.in/beat.js"
data-site="yoursite.com"
data-attribution="off"></script>With attribution off, beat.js reads no storage and writes none, and no beacon carries a first touch. Any other value, including leaving the attribute out, means on.
Reading it from your own code
window.kymo.attribution()It returns a copy of the stored object, or null when attribution is off or storage is unavailable. It is always safe to call.
Attributing a Stripe purchase
A purchase confirmed by a Stripe webhook arrives from Stripe's servers, not from the visitor's browser, so it carries no first touch of its own. Pass it through the Checkout Session metadata.
- Read the first touch in the browser
Stripe caps a metadata value at 500 characters. Trim before sending. Drop the lowest-value fields first —
utm_content, thenutm_term, then the referrer, which is the longest field and the one most often a long URL:function kymoFt() { const ft = window.kymo.attribution(); if (!ft) return undefined; let s = JSON.stringify(ft); for (const key of ['ct', 'tm', 'r']) { if (s.length <= 500) break; delete ft[key]; s = JSON.stringify(ft); } return s.length <= 500 ? s : undefined; } - Attach it to the Checkout Session
kymo_ftsits beside the two metadata fields Kymo already reads:const session = await stripe.checkout.sessions.create({ // … your line items and URLs … metadata: { kymo_site: 'yoursite.com', kymo_visitor: visitor, // from /api/whoami, the same-day attribution kymo_ft: kymoFt() // the first touch, optional } }); - Nothing else to configure
The webhook you already set up records the purchase with its first touch attached.
kymo_visitorkeeps its meaning and its behaviour — first touch is additive.If
kymo_ftis missing, or arrives cut short, or is not valid JSON, Kymo records the purchase without first-touch data and returns200. A purchase is never lost over its attribution.
See Goals for the webhook setup itself.
Two limits, stated plainly
- Clearing browser data resets the first touch. The value lives in the visitor's own browser. If they clear site data, use a different browser, or browse in a private window that they later close, the next visit becomes a new first touch.
- One device only. A visitor who lands on a phone and buys on a laptop is two first touches, and Kymo does not connect them. Connecting them needs a persistent cross-device identity, which is exactly what a cookieless analytics tool refuses to build.
Both limits pull the same direction: first touch under-reports campaigns rather than over-reporting them. Treat it as a strong signal, not an audited number.