# Custom events

> Send your own events with statsy.goal(): naming, properties, what never to include, common patterns and how to debug what is sent.

Section: Tracking script  
Canonical page: https://statsy.co/docs/tracking-script/custom-events  
Last updated: 2026-09-22

Anything the automatic events miss is one call. A custom event is a goal with a name you choose and optional properties, and it shows up in the same places: the Goals page, funnels, filters, alerts and the API.

```js
statsy.goal('signup', { plan: 'pro', trial: true });
```

## Naming

- Lowercase with underscores: `signup`, `add_to_cart`, `invite_sent`. Names are case sensitive and cut at 120 characters.
- Name the action, not the place. `pricing_cta` ages badly; `start_trial` does not.
- Keep to a few dozen names. Put variation in properties (`plan: 'pro'`), not in names (`signup_pro`).
- Avoid the automatic names (`download`, `form_submit`, `site_search`…) unless you mean to add to them.

## Properties

Values are strings, numbers or booleans; anything else is dropped. Keep them short: a whole event is limited to 64 KB, and server-side events cap keys at 64 characters and values at 500. Split any event by a property from its detail sheet on the Goals page, or through `GET /api/v1/sites/:site/events/breakdown?name=&prop=`.

> **Never send personal data in an event.** No emails, names, addresses, card numbers or free text a person typed. Properties are stored in plain form and appear in exports. The place for who a visitor is, is [statsy.identify()](https://statsy.co/docs/tracking-script/visitor-identification), which is stored separately and never leaves your workspace.

## Patterns

### Signup, with the plan

```js
statsy.goal('signup', { plan, source: 'pricing_page' });
statsy.identify(user.id, { email: user.email, plan });
```

### A step inside your own checkout

```js
statsy.goal('checkout_step', { step: 2, items: cart.length });
```

### Search in a single-page app

```js
statsy.search(query, results.length); // records site_search with results
```

### Anything without JavaScript

```html
<a href="/demo" data-statsy-goal="book_demo" data-statsy-prop-plan="team">Book a demo</a>
```

## Before the script loads

```js
window.statsyq = window.statsyq || [];
statsyq.push(['goal', 'signup', { plan: 'pro' }]);
```

## From your server

Anything that happens after the browser is gone, an invoice paid, a trial converted, a job finished, can be sent as a server-side event with the same name and properties, and the same visitor id when you have it. See [the REST API](https://statsy.co/docs/developers/api) for `POST /api/v1/sites/:site/events`.

## Seeing what is sent

Turn on debug logging in the console and every beacon prints its type, name and properties as it leaves.

```js
statsy.debug();          // start logging
statsy.goal('test_event');
// [statsy] goal test_event {…}
statsy.debug(false);     // stop
```

Then open **Live**: a goal reaches the dashboard within a few seconds. If it does not, see [Troubleshooting](https://statsy.co/docs/troubleshooting).

## Frequently asked questions

### Can I attach a value to a custom event?

A goal created on the Goals page can carry a default value, and that value is counted as goal value, separately from real revenue. Amounts sent from the browser are ignored on purpose because they can be forged; revenue comes from your payment provider webhooks.

### Does an event need a goal to be created first?

No. Any name you send is stored and can be filtered or split at once. Creating a goal with that name adds it to the Goals table with a conversion rate and lets you give it a value.

Previous: [Events captured automatically](https://statsy.co/docs/tracking-script/auto-captured-events)  
Next: [Site search](https://statsy.co/docs/tracking-script/site-search)