beat.js
beat.js is the client tracker. It is 1,515 bytes gzipped, minified as served, and it sets no cookies. It writes two localStorage keys on your own domain: the first-touch attribution key, and the owner opt-out flag.
Script attributes
Set on the <script> tag, read via document.currentScript.
| Field | Type | Required | Description |
|---|
Because beat.js reads document.currentScript, it must be a parser-inserted tag. Do not load it through a bundler-injected loader like next/script, which leaves currentScript null.
window.kymo
After load, window.kymo is a function for custom goals.
| Field | Type | Required | Description |
|---|
window.kymo('signup');
window.kymo('purchase', { revenue: 49 });If the owner opt-out is active, window.kymo is still defined but is a no-op, so your calls never error.
window.kymo.attribution()
Returns a copy of this browser's stored first touch, or null when attribution is off, when the browser has no stored value, or when storage is unavailable. It is always safe to call.
window.kymo.attribution();
// { s: 'newsletter', m: 'email', c: 'launch', l: '/pricing', t: '2026-05-02T09:14:00.000Z' }See First-touch attribution for every field and for passing the value to Stripe.
The beacon payload
Each event is a text/plain POST to /api/beat via navigator.sendBeacon (falling back to fetch with keepalive). The body is JSON:
| Field | Type | Required | Description |
|---|
The payload carries no cookie and no identifier you set. See /api/beat for what the server does with it.
Automatic behavior
- Pageviews fire on load and on every
history.pushStateandpopstate, de-duplicated on identical path plus query. - Outbound clicks — a click on a link to a different hostname sends an
outboundevent. - Auto-goals —
tel:,mailto:, WhatsApp links, and form submits fire goals with no configuration.
Size
beat.js is 1,515 bytes gzipped. That is the minified file kymo.in serves, measured from the built artifact when these docs are built, so the figure here cannot drift from the file your visitors download.
The script loads on every page view, before the rest of your page finishes rendering. A smaller file is a smaller file. We have not measured what it does to your page speed scores, and we make no claim about that.
Storage
beat.js writes two localStorage keys, both on your own domain, and no cookie:
| Field | Type | Required | Description |
|---|
An opted-out browser writes neither key. Storage that is blocked or full is caught, and the beacon still sends.
Opt-out and local development
?kymo=ignoresets a per-browserlocalStorageflag that stops sending;?kymo=trackclears it.- On
localhost,127.*, and192.168.*,beat.jsdoes nothing.