# beat.js

> Full reference for the Kymo tracker — script attributes, the window.kymo API, the beacon payload, automatic goals, SPA support, and the owner opt-out.

Canonical: https://kymo.in/docs/reference/beat-js
Last updated: 2026-08-11

`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`.

| Name | Type | Required | Description |
|---|---|---|---|
| `data-site` | string | yes | The domain events are filed under. Must match your registered domain exactly — no scheme, no www. Defaults to location.hostname if omitted. |
| `data-goals` | string | no | Comma-separated paths to count as goals on pageview, e.g. "/signup,/thanks". |
| `data-api` | string | no | Override the ingest URL. Defaults to the /api/beat next to the script src. |
| `data-attribution` | string | no | Set to "off" to switch first-touch capture off. On by default; any other value is on. |

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.

| Name | Type | Required | Description |
|---|---|---|---|
| `name` | string | yes | Goal name, capped at 100 characters. |
| `opts.revenue` | number | no | Revenue to attach to this goal; feeds revenue totals. |

```js
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.

```js
window.kymo.attribution();
// { s: 'newsletter', m: 'email', c: 'launch', l: '/pricing', t: '2026-05-02T09:14:00.000Z' }
```

See [First-touch attribution](https://kymo.in/docs/guides/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:

| Name | Type | Required | Description |
|---|---|---|---|
| `site` | string | yes | From data-site. |
| `path` | string | yes | location.pathname at send time. |
| `kind` | string | no | Omitted for pageviews; "outbound" or "goal" otherwise. |
| `target` | string | no | Outbound URL, or goal name. |
| `value` | number | no | Goal revenue. |
| `ref` | string | no | document.referrer. |
| `us / um / uc` | string | no | utm_source / utm_medium / utm_campaign, on pageviews. |
| `w` | number | no | window.innerWidth, used to classify device. |
| `ft` | object | no | The stored first touch, on every event. Omitted when attribution is off. |

The payload carries no cookie and no identifier you set. See [/api/beat](https://kymo.in/docs/reference/api-beat) for what the server does with it.

## Automatic behavior

- **Pageviews** fire on load and on every `history.pushState` and `popstate`, de-duplicated on identical path plus query.
- **Outbound clicks** — a click on a link to a different hostname sends an `outbound` event.
- **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:

| Name | Type | Required | Description |
|---|---|---|---|
| `kymo_ft` | JSON | no | First touch: the UTM parameters, referrer and landing path of the first visit, plus its timestamp. Expires after 13 months. Written unless data-attribution="off". Holds no identifier. |
| `kymo_ignore` | flag | no | The owner opt-out, written only when someone visits with ?kymo=ignore. |

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=ignore` sets a per-browser `localStorage` flag that stops sending; `?kymo=track` clears it.
- On `localhost`, `127.*`, and `192.168.*`, `beat.js` does nothing.
