> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flowsbuilt.com/llms.txt
> Use this file to discover all available pages before exploring further.

# event()

> Track a single product event for a user.

## Overview

`event()` records a single behavioral event. Events are the primary signal ChurnKit uses to calculate churn risk — frequent, diverse events indicate healthy engagement, while gaps or negative events (e.g. support tickets) raise risk scores.

## Signature

```typescript theme={null}
churn.event(
  userId: string,
  event: string,
  properties?: EventProperties,
  options?: CallOptions
): Promise<{ ok: boolean }>
```

## Parameters

<ParamField path="userId" type="string" required>
  The user who performed the action.
</ParamField>

<ParamField path="event" type="string" required>
  Event name — use `snake_case` by convention. Must be non-empty.
</ParamField>

<ParamField path="properties" type="EventProperties">
  Optional key-value metadata. Values must be `string | number | boolean | null`.
</ParamField>

<ParamField path="options.signal" type="AbortSignal">
  Cancel the request via an `AbortSignal`.
</ParamField>

## What events to track

Track events that signal **engagement** or **disengagement**:

<CodeGroup>
  ```typescript Positive signals theme={null}
  // Core feature usage — strongest retention signal
  await churn.event('user_123', 'feature_used', { feature: 'analytics_dashboard' })
  await churn.event('user_123', 'report_exported')
  await churn.event('user_123', 'team_member_invited')
  await churn.event('user_123', 'api_called')
  await churn.event('user_123', 'integration_connected', { provider: 'slack' })
  ```

  ```typescript Negative signals theme={null}
  // These raise risk scores
  await churn.event('user_123', 'support_ticket_opened', { severity: 'critical' })
  await churn.event('user_123', 'billing_failed')
  await churn.event('user_123', 'export_all_data')          // data portability = exit intent
  await churn.event('user_123', 'cancellation_page_viewed')
  await churn.event('user_123', 'downgrade_initiated')
  ```

  ```typescript Session events theme={null}
  // Used for recency/frequency scoring
  await churn.event('user_123', 'session_started')
  await churn.event('user_123', 'session_ended', { duration_seconds: 342 })
  ```
</CodeGroup>

## Examples

### Simple event

```typescript theme={null}
await churn.event('user_123', 'dashboard_viewed')
```

### Event with properties

```typescript theme={null}
await churn.event('user_123', 'feature_used', {
  feature: 'csv_export',
  rows_exported: 1500,
  format: 'csv',
})
```

### In a Next.js Route Handler

```typescript theme={null}
import { churn } from '@/lib/churnkit'

export async function POST(req: Request) {
  const { userId, feature } = await req.json()
  await churn.event(userId, 'feature_used', { feature })
  return Response.json({ ok: true })
}
```

### Fire and forget (non-blocking)

```typescript theme={null}
// Don't await if you don't need to block the response
void churn.event(userId, 'page_viewed', { path: '/settings' })
```

## Return value

```typescript theme={null}
{ ok: true }
```

## Errors

| Code               | When                                   |
| ------------------ | -------------------------------------- |
| `VALIDATION_ERROR` | `userId` or `event` is empty           |
| `UNAUTHORIZED`     | API key is invalid                     |
| `RATE_LIMITED`     | Too many requests — back off and retry |
| `TIMEOUT`          | Request exceeded timeout               |

<Tip>
  For high-throughput scenarios (e.g. tracking every API call), use the [EventBatcher](/churnkit/events/batcher) instead of calling `event()` individually.
</Tip>
