# Custom events

Send named events with properties from the browser to count signups, upgrades and any other action as goals.

Call `window.productlense('event', name, props)` to record an action. Custom events appear in the events breakdown and can be used as goals and funnel steps.

```js
window.productlense('event', 'Signup', { plan: 'starter' });
window.productlense('event', 'Upgrade clicked', { from: 'pricing' });
```

## Rules for names and props

- The name must match `^[a-zA-Z0-9_\- ]{1,64}$`: letters, digits, spaces, underscores and hyphens, up to 64 characters. Any other name is ignored silently in the browser and rejected by the server.
- `pageview` and `$identify` are reserved.
- Props are optional and must be a JSON object, at most 2,048 bytes once serialised. The browser does not check props; the server rejects the whole batch an invalid event arrived in, so keep props small and flat.
- The whole request is limited to 8 KB.

> [!NOTE]
> Every custom event counts toward your plan's monthly events, the same as a pageview. `$identify` events do not.

## Call it before the script loads

The script loads with `defer`. If your code may run first, add the queue stub above the script tag. Calls made before the script loads are kept and replayed in order.

```html {1}
<script>window.productlense=window.productlense||function(){(window.productlense.q=window.productlense.q||[]).push(arguments)}</script>
<script defer data-site="SITE_ID" src="https://api.productlense.app/a.js"></script>
```

In TypeScript, declare the global once:

```ts title="src/productlense.d.ts"
declare global {
  interface Window {
    productlense?: (...args: unknown[]) => void;
  }
}
export {};
```

Then call it with optional chaining so server rendering never fails: `window.productlense?.('event', 'Signup')`.

## Examples

```js tab="Button click"
document.querySelector('#upgrade')?.addEventListener('click', () => {
  window.productlense('event', 'Upgrade clicked', { from: 'navbar' });
});
```

```js tab="Form submit"
document.querySelector('form#signup')?.addEventListener('submit', () => {
  window.productlense('event', 'Signup');
});
```

```tsx tab="React"
export function UpgradeButton() {
  return (
    <button onClick={() => window.productlense?.('event', 'Upgrade clicked', { from: 'pricing' })}>
      Upgrade
    </button>
  );
}
```

## Use events as goals

Goals are defined in the dashboard, not in code: a goal matches an event name (such as `Signup`) or a page path. See [Goals and funnels](/docs/dashboard/goals-and-funnels).

## From a server

To send events from your backend, for example a trial that starts after a webhook, use the [Events API](/docs/api/events).
