---
title: "Custom events, goals & UTM tracking"
description: "Track custom events with window.lytics.track() in the browser or POST /v1/events from a server, turn them into goal conversions, and capture UTM campaign parameters automatically."
last_updated: "2026-07-30"
---

# Custom events, goals & UTM tracking

Custom events record what automatic tracking misses: signups, checkouts, feature usage. Track them from the browser with `window.lytics.track()`, or from a server with the SDK or `POST /v1/events`; goals then turn event names or URL paths into conversion metrics.

## Track from the browser

Once `lytics.js` has loaded, call `window.lytics.track(name, props?)`:

```js
window.lytics.track("signup", { plan: "pro" });
```

| Constraint | Value |
|---|---|
| `name` | Must match `[a-zA-Z0-9_.:-]{1,64}`; truncated to 64 characters |
| `props` | Flat object of string/number/boolean values. Nested objects aren't supported |
| `props` size | Serialized to at most 2048 bytes |

### Track before the script has loaded

If you need to call `track()` before `lytics.js` has finished loading (for example from an inline script that fires early), install this stub first. It queues calls, and the real tracker drains the queue in order right after its own initial pageview:

```html
<script>
  window.lytics = window.lytics || { q: [], track: function (name, props) { this.q.push([name, props]); } };
</script>
<script src="https://cdn.analytics.canarycoders.es/lytics.js" data-domain="myapp.com" defer></script>

<script>
  // Safe to call immediately; queued if lytics.js hasn't run yet.
  window.lytics.track("landing_cta_click");
</script>
```

## Track from a server

Use the [SDK](/docs/api/sdk/) or a raw `POST /v1/events` call with a full (`cly_live_…`) key. Read-only keys get `403 read_only_key`:

```ts

const lytics = createClient({ apiKey: process.env.CANARYLYTICS_KEY });
await lytics.events.track("signup", { plan: "pro" }, { path: "/app/onboarding" });
```

```bash
curl https://api.analytics.canarycoders.es/v1/events \
  -H "Authorization: Bearer cly_live_…" \
  -H "Content-Type: application/json" \
  -d '{"name":"signup","props":{"plan":"pro"},"path":"/app/onboarding"}'
```

`path` defaults to `"/"`. `sessionId` is optional and lets you attach the event to an existing client-minted session. Omit it and the server mints a random one. Server-side events skip geo/UA classification entirely: `isBot` is always `false`, and there is no traffic-source label.

<Aside type="note">
  Custom events count against the project's daily event cap, same as any other event. A `429 daily_cap_exceeded` means today's cap is reached.
</Aside>

## Goals

Goals turn a custom event name or a URL path pattern into a conversion metric. They are created in the dashboard, under Settings then Goals. There is no public REST or SDK endpoint to create one, only to read the resulting stats:

- **Event goals** match an exact custom event name.
- **Path goals** match an exact path, or a path with a `/*` suffix for prefix matching (for example `/checkout/success/*`).

A project can have up to 20 goals. Read conversion counts and rates with `GET /v1/stats/goals` or `client.stats.goals()`. Conversion rate is converted sessions divided by total sessions in the window.

## UTM tracking

For **page_view** events only, `utm_source`, `utm_medium`, and `utm_campaign` are extracted from the query string of the landing path at ingest time and stored alongside the event. No snippet configuration is needed; it happens automatically for any URL like:

```
https://myapp.com/?utm_source=newsletter&utm_medium=email&utm_campaign=spring-launch
```

`utm_term` and `utm_content` are not captured. Read the breakdown with `GET /v1/stats/utm` or `client.stats.utm()`, which returns sessions and pageviews grouped independently by source, medium, and campaign.

## Related

<CardGrid>
  <LinkCard title="Script reference" description="Everything lytics.js tracks automatically." href="/docs/tracking/script-reference/" />
  <LinkCard title="REST API" description="POST /v1/events and GET /v1/stats/goals in full." href="/docs/api/rest/" />
</CardGrid>

## Sitemap

- [Docs sitemap](https://analytics.canarycoders.es/docs/sitemap.md)
- [Full documentation as a single file](https://analytics.canarycoders.es/llms-full.txt)
