# Custom events

> Send your own events from the browser with clickclacks('event', name, properties): rules for names and properties, naming conventions and how events are sent.

- Canonical URL: https://clickclacks.io/docs/events
- Section: Tracking
- Last updated: 2026-10-17

Autocapture sees pageviews, clicks and scrolling. Custom events record what those can’t: a sign-up, an upgrade, an export. Each one is a single function call.

## Send an event {#send}

Call `clickclacks` with `'event'`, a name, and an optional object of properties:

```js
window.clickclacks('event', 'Signed up', {
  plan: 'pro',
  seats: 5,
  referred: true,
})
```

The call returns nothing and never throws. The event is queued and sent with the next batch, with the page’s anonymous ID, session ID and, if you’ve called [identify](https://clickclacks.io/docs/identify.md), the user ID.

The tracker also adds the page as `path`: the pathname only, never the query string or `#fragment`. So a custom event splits by **Page** in reports just like a pageview. Pass your own `path` and yours is kept.

## Names and properties {#rules}

- **The name** is a string of 1 to 128 characters, and can’t start with `$`, which is reserved for ClickClacks.
- **Properties** must be a plain object, or left out. An array, a string or `null` in its place isn’t accepted.
- Values can be strings, numbers, booleans, `null`, arrays or nested objects. Functions and `undefined` values are dropped.
- Properties are capped at **2 KB of JSON**. Over that, the event is still sent, without your properties (it keeps `path`).
- A call that breaks these rules is ignored silently rather than throwing, so a typo can’t break your page. Check new events arrive in **Realtime**.

Properties belong to the event, not to the person. A `plan` sent with `Signed up` describes that sign-up, not the person’s plan today; for that, pass traits to [identify](https://clickclacks.io/docs/identify.md).

## Naming conventions {#naming}

Names are how everyone on your team finds an event in a picker, so they’re worth a minute of thought. What works well:

- **Object and past-tense verb,** in sentence case: `Signed up`, `Invoice paid`, `Report exported`.
- **A small, stable set.** Tens of well-named events beat hundreds of near-duplicates.
- **Properties in `snake_case`,** with one type each: `amount_cents` is always a number, `plan` always a string.
- **No personal data** in names or properties. Use your user ID, not an email address.

| Prefer | Over | Why |
| --- | --- | --- |
| `Invoice paid` | `invoice_paid_4821` | IDs belong in properties. One name per kind of thing, or every invoice becomes its own event. |
| `Signed up` | `signup / SignUp / sign_up` | Pick one style and keep it. Three spellings are three events in every report. |
| `Report exported { format: "csv" }` | `CSV report exported` | Variants go in properties, so one insight can split by format. |
| `Plan changed { from: "growth", to: "pro" }` | `Button clicked` | Name the outcome, not the widget. Clicks are already autocaptured. |

Describe each event and property once in **Lexicon**, under **Data** in the app. Every picker in ClickClacks reads from it, so your descriptions show up wherever someone builds a report.

## Examples by framework {#examples}

The tracker is a global function, so there’s nothing to import. Use optional chaining (`?.`) so a call is skipped when the tracker isn’t running, for example on a hostname outside `data-domains` or before it has loaded.

**React**

```tsx title="UpgradeButton.tsx"
// A React component
export function UpgradeButton({ plan }: { plan: string }) {
  return (
    <button
      onClick={() => window.clickclacks?.('event', 'Upgrade clicked', { plan })}
    >
      Upgrade
    </button>
  )
}

// Once, for TypeScript (for example in global.d.ts):
declare global {
  interface Window {
    clickclacks?: (command: string, ...args: unknown[]) => void
  }
}
```

**Vue**

```vue title="ExportMenu.vue"
<script setup lang="ts">
function exportReport(format: 'csv' | 'pdf') {
  window.clickclacks?.('event', 'Report exported', { format })
  // …
}
</script>

<template>
  <button @click="exportReport('csv')">Export CSV</button>
</template>
```

**Svelte**

```svelte title="Share.svelte"
<script lang="ts">
  function share() {
    window.clickclacks?.('event', 'Board shared', { via: 'link' })
  }
</script>

<button on:click={share}>Share</button>
```

**HTML**

```html title="index.html"
<form id="newsletter">…</form>
<script>
  document.getElementById('newsletter').addEventListener('submit', () => {
    window.clickclacks?.('event', 'Newsletter joined', { placement: 'footer' })
  })
</script>
```

## Calling before the script loads {#before-load}

The tag is `async`, so `window.clickclacks` doesn’t exist until `c.js` has loaded, and never exists on a hostname missing from `data-domains`. When the tracker starts, it replays any calls waiting in `window.clickclacks.q`, in order, before it sends the first pageview. Define this stub above the script tag and early calls are queued instead of lost:

```html
<script>
  window.clickclacks = window.clickclacks || function () {
    (window.clickclacks.q = window.clickclacks.q || []).push(arguments)
  }
</script>
```

With the stub in place you can drop the `?.`. Without it, guard each call: `window.clickclacks?.('event', 'Signed up')`.

## How events are sent {#delivery}

- Events are batched in the browser and sent every 5 seconds, as soon as 25 are waiting, and when the page is hidden or closed.
- They go out with `fetch` and `keepalive` (or `sendBeacon` in older browsers), so a batch survives the page closing.
- If a batch fails, its events are kept and retried with the next one, up to 100 waiting events, so a brief network drop doesn’t lose them.
- Requests carry no cookies or credentials.

## Events for a company {#group}

If your customers are companies, the `group` command says which company the signed-in person is acting for. The tracker remembers it and adds it to every later event as `$groups`, so reports can count companies as well as people.

```js
// Once, after sign-in
window.clickclacks('group', 'company', 'cmp_311', { name: 'Acme', plan: 'pro' })

// Every later event from this browser counts for that company
window.clickclacks('event', 'Report exported', { format: 'csv' })
```

- The type is 1–64 lower-case letters, digits or underscores, such as `company`. The ID is your own ID for the company, 1–255 characters.
- A browser holds one ID per type, and at most 5 types. Pass `null` as the ID to leave a group; `reset` forgets them all.
- The optional third argument is the company’s traits. Sending them is free.
- Events sent before the call don’t count for the company.

[Companies and groups](https://clickclacks.io/docs/groups.md) covers the model, traits and what reports do with them.

## Events the browser can’t see {#server}

Some moments happen with no page open, or matter too much to leave to a browser: renewals, refunds, webhooks, background jobs. Send those from your backend with the [server API](https://clickclacks.io/docs/api.md) or the [Node SDK](https://clickclacks.io/docs/node.md). They join the same people, as long as you use the same user ID you pass to `identify`.

## Next steps {#next}

- [Identify people](https://clickclacks.io/docs/identify.md): tie events to your user IDs.
- [Companies and groups](https://clickclacks.io/docs/groups.md): count companies as well as people.
- [Tracker API](https://clickclacks.io/docs/tracker-api.md): every command in one place.
- [Events and properties](https://clickclacks.io/docs/properties.md): the reserved `$` names.
