---
url: 'https://docs.kitbase.dev/track-events.md'
description: >-
  Send custom events with channels, tags, and notifications using the Kitbase
  SDK.
---

# Track Custom Events

Kitbase lets you track custom events with rich metadata to understand what your users are doing, monitor business metrics, and trigger real-time notifications. Events are sent asynchronously to the Kitbase API and processed in the background.

All events are sent via `POST /sdk/v1/logs` with API key authentication (`X-API-Key` header) and return `202 Accepted` for async processing.

## Basic Usage

```typescript
import { init } from '@kitbase/analytics';

const kitbase = init({ sdkKey: 'YOUR_SDK_KEY' });

await kitbase.track({
  channel: 'payments',
  event: 'Purchase Completed',
  tags: {
    amount: 99.99,
    currency: 'USD',
    plan: 'premium'
  }
});
```

## Track Options

The `track` method accepts a `TrackOptions` object with the following fields:

```typescript
interface TrackOptions {
  channel: string;              // Required: event category (e.g., 'payments', 'users', 'errors')
  event: string;                // Required: event name (e.g., 'Purchase Completed')
  user_id?: string;             // Optional: explicit user identifier
  icon?: string;                // Optional: emoji icon for dashboard
  notify?: boolean;             // Optional: trigger real-time notification
  description?: string;         // Optional: human-readable event description
  tags?: Record<string, unknown>; // Optional: metadata (string, number, boolean values)
}
```

| Parameter     | Type      | Required | Description                                      |
|---------------|-----------|----------|--------------------------------------------------|
| `channel`     | `string`  | Yes      | Category for the event (e.g., `"payments"`, `"users"`) |
| `event`       | `string`  | Yes      | Name of the event (e.g., `"Purchase Completed"`) |
| `user_id`     | `string`  | No       | Explicit user identifier                         |
| `icon`        | `string`  | No       | Emoji icon displayed in the dashboard            |
| `notify`      | `boolean` | No       | Trigger a real-time notification                 |
| `description` | `string`  | No       | Human-readable event description                 |
| `tags`        | `object`  | No       | Key-value metadata (string, number, boolean)     |

## Response

A successful call returns a `TrackResponse` with the event's unique identifier and timestamp:

```typescript
interface TrackResponse {
  id: string;        // Unique event ID (UUID)
  event: string;     // Event name
  timestamp: string; // ISO 8601 timestamp
}
```

## Channels

Channels organize your events into logical categories. Use consistent channel names across your application to keep your dashboard clean and filterable.

| Channel          | Use Case                                    |
|------------------|---------------------------------------------|
| `payments`       | Purchases, refunds, subscription changes    |
| `users`          | Signups, logins, profile updates            |
| `errors`         | Application errors, failed requests         |
| `notifications`  | Alerts, messages, delivery status           |
| `api`            | API calls, webhooks, integrations           |
| `system`         | Infrastructure events, deployments          |

You are not limited to these defaults. Custom channels are fully supported -- use whatever names make sense for your domain.

::: tip Channel Naming
Use lowercase, plural nouns for channel names (e.g., `payments`, `errors`, `users`). This keeps your events organized and easy to filter in the dashboard.
:::

## Tags

Tags are key-value metadata attached to events. They allow you to store additional context that you can filter and analyze later.

### Supported Types

| Type      | Example   |
|-----------|-----------|
| `string`  | `"premium"` |
| `number`  | `99.99`   |
| `boolean` | `true`    |

### Example

```typescript
tags: {
  plan_name: 'premium',      // string
  amount: 99.99,             // number
  is_upgrade: true,          // boolean
}
```

::: tip Tag Naming
Use `snake_case` for tag names:

* `plan_name`, `user_type`, `error_code`
* Not: `PlanName`, `planName`, `ERROR-CODE`
  :::

## Server-Side Tag Enrichment

The Kitbase backend automatically enriches every event with additional context derived from the request. These enriched fields are prefixed with `__` and stored as dedicated columns, available as first-class filter dimensions in the dashboard.

### Browser and Device

| Tag                  | Description                                  |
|----------------------|----------------------------------------------|
| `__browser`          | Browser name (Chrome, Firefox, Safari)       |
| `__browser_version`  | Browser version                              |
| `__os`               | Operating system (Windows, macOS, iOS)       |
| `__os_version`       | OS version                                   |
| `__device`           | Device type — see the taxonomy below         |
| `__brand`            | Device brand (Apple, Samsung, Google)        |
| `__model`            | Device model (iPhone 15, Pixel 8)            |
| `__device_confidence`| Detection confidence, `0.0`–`1.0` (see bands below) |
| `__signal_source`    | Which request signal produced the device type |
| `__in_app`           | `1` when the request came from an in-app WebView/Electron context |

**Device taxonomy** (`__device`): `desktop`, `mobile`, `tablet`, `smart_tv`, `set_top_box`, `game_console`, `handheld_game_console`, `wearable`, `automotive`, `xr`, `e_reader`, `voice`, `home_appliance`, `bot`, `unknown`. Device type is detected entirely server-side (no client-provided hints are trusted) and an unidentifiable client stays `unknown` — it is never silently coerced to `desktop`.

**Detection precedence & confidence.** Detection runs a bot gate first, then prefers the strongest available signal:

| Signal (`__signal_source`)         | Source                                    | `__device_confidence` |
|------------------------------------|-------------------------------------------|-----------------------|
| `BOT_RULE`                         | known bot ruleset / robot UA              | `0.99`                |
| `FORM_FACTORS`                     | `Sec-CH-UA-Form-Factors` client hint      | `0.95`                |
| `LOW_ENTROPY_HINTS`                | `Sec-CH-UA-Mobile` + platform hints       | `0.85`                |
| `UA_FUSED` / `UA_ONLY`             | User-Agent parse (with / without hints)   | `0.75`                |
| `UA_FUSED` / `UA_ONLY` (Unknown)   | parsed but class unresolved               | `0.60`                |
| `NONE`                             | no usable signal                          | `0.30`                |

To recover accurate detection on Chromium (whose User-Agent is frozen by User-Agent Reduction), the backend advertises UA Client Hints via `Accept-CH`, with the device-critical subset (`Sec-CH-UA-Form-Factors`, `Sec-CH-UA-Mobile`, `Sec-CH-UA-Platform`) also sent as `Critical-CH` so even first-hit requests can be classified from hints. Filter low-confidence rows by `__signal_source`/`__device_confidence` in the dashboard.

> **Known limitation:** iPadOS Safari presents a macOS desktop User-Agent and sends no distinguishing hints, so some iPads are counted as `desktop` (a documented undercount). We do not fingerprint to work around this.

### Bot & Crawler Attribution

Every request is classified by a server-side attribution pipeline that not only detects bots but tries to **verify** their claimed identity — because a bot User-Agent is trivially spoofable.

| Tag | Description |
|-----|-------------|
| `__actor_type` | `human`, `verified_bot`, `spoofed_bot`, `suspected_bot`, or `unknown` |
| `__is_bot` | `1` for any kind of bot (the flag used to exclude bots from human/billing counts) |
| `__bot_name` | Attributed bot name (e.g. `GPTBot`, `Googlebot`) |
| `__bot_vendor` | Operating vendor (e.g. `OpenAI`, `Google`) |
| `__actor_category` | `ai_training`, `ai_search`, `ai_user_fetch`, `search_engine`, `social_preview`, `seo`, `monitoring`, `security_scanner`, `archiver`, `feed_fetcher`, `automation` |
| `__actor_verified` | `1` when the bot's identity was cryptographically/network confirmed |
| `__verification_method` | `web_bot_auth`, `cidr`, `reverse_dns`, or `none` |
| `__actor_confidence` | Classification confidence, `0.0`–`1.0` |
| `__is_ai` | `1` when this is an AI crawler/agent |

**Verification hierarchy** (strongest → weakest), short-circuiting on the first decisive result:

1. **Web Bot Auth** — RFC 9421 Ed25519 HTTP Message Signatures, verified against the signer's `/.well-known/http-message-signatures-directory`. Unspoofable.
2. **Exact CIDR membership** — the client IP must fall inside the vendor's published IP ranges (not just the same ASN, which spoofers share).
3. **Forward-confirmed reverse DNS** — for vendors that publish no IP list (Yandex, Baidu, Amazon, Apple).

A recognized bot UA that **fails** verification (e.g. claims `GPTBot` from a non-OpenAI IP) is flagged `spoofed_bot` — the high-value fraud signal. A UA-only match with no verification is `suspected_bot`.

**Drop vs. store** is controlled by your project's **bot detection** setting:

* **Bot detection ON** → bot events are **stored**, flagged with the attribution above (`is_bot`), and **excluded** from human visitor and billing counts.
* **Bot detection OFF** → bot traffic is **dropped** entirely.

> **Capturing non-JS crawlers:** crawlers like GPTBot and Googlebot fetch your pages without running the JavaScript SDK, so they never reach this enrichment path. To detect them, forward your server/edge request signals to Kitbase — see the [Bot & Crawler Detection setup guides](/crawler-detection) for your platform, or the [endpoint reference](/api-reference#server-side-crawler-detection). Both run the same attribution pipeline.

### Geolocation

| Tag          | Description              |
|--------------|--------------------------|
| `__country`  | Country (derived from IP) |
| `__region`   | Region or state          |
| `__city`     | City                     |

### Page and Attribution

| Tag              | Description          |
|------------------|----------------------|
| `__path`         | Page path            |
| `__referrer`     | Referrer URL         |
| `__utm_source`   | UTM source parameter |
| `__utm_medium`   | UTM medium parameter |
| `__utm_campaign` | UTM campaign parameter |
| `__utm_term`     | UTM term parameter   |
| `__utm_content`  | UTM content parameter |

::: tip Query Filters
These enriched fields are available as filter dimensions in the dashboard. Filter events by browser, country, device, UTM parameters, and more directly from the events list.
:::

## Notifications

When `notify: true` is set, the event triggers a real-time notification in the Kitbase dashboard. Combine with `icon` and `description` to make notifications informative at a glance.

```typescript
await kitbase.track({
  channel: 'payments',
  event: 'Large Purchase',
  icon: '💰',
  notify: true,
  description: 'A customer just made a $500+ purchase',
  tags: { amount: 549.99 }
});
```

Good candidates for notifications:

* New signups and subscriptions
* Failed payments and billing issues
* Critical application errors
* Security events (login from new device)
* Large or unusual transactions

## Revenue Tracking

Track revenue with the dedicated `trackRevenue` method. Revenue events are automatically sent to the `__analytics` channel.

```typescript
await kitbase.trackRevenue({
  amount: 99.99,
  currency: 'USD',
  orderId: 'order_123',
  productId: 'prod_premium',
  tags: { plan: 'premium' }
});
```

This provides structured revenue data in the dashboard, separate from general custom events, so you can track MRR, ARPU, and other financial metrics out of the box.

## Timed Events

Measure how long user flows take by starting a timer and letting the SDK automatically attach the duration when the event is tracked.

```typescript
// Start timing
kitbase.timeEvent('Checkout Flow');

// ... user completes checkout ...

// Track with automatic $duration tag (in milliseconds)
await kitbase.track({
  channel: 'conversions',
  event: 'Checkout Flow',
  tags: { items: 3 }
});
// The $duration tag is automatically added to the event
```

### Methods

| Method                       | Description                                  |
|------------------------------|----------------------------------------------|
| `timeEvent(eventName)`       | Start timing an event                        |
| `cancelTimeEvent(eventName)` | Cancel a timed event without tracking        |
| `getTimedEvents()`           | Get list of event names currently being timed |
| `getEventDuration(eventName)`| Get current elapsed duration in milliseconds |

### Example

```typescript
// Check active timers
const activeTimers = kitbase.getTimedEvents();
// => ['Checkout Flow', 'Video Watched']

// Get duration without tracking
const duration = kitbase.getEventDuration('Checkout Flow');
// => 45000 (ms)

// Cancel a timer
kitbase.cancelTimeEvent('Video Watched');
```

## Super Properties

Super properties are automatically included with every event you track. They are useful for attaching persistent context like app version, environment, or user segment.

```typescript
// Register properties (overwrites existing)
kitbase.register({ app_version: '2.1.0', environment: 'production' });

// Register only if not already set
kitbase.registerOnce({ first_visit_source: 'google' });

// Remove a specific super property
kitbase.unregister('first_visit_source');

// Get all registered super properties
kitbase.getSuperProperties();

// Clear all super properties
kitbase.clearSuperProperties();
```

### Methods

| Method                           | Description                              |
|----------------------------------|------------------------------------------|
| `register(properties)`          | Set properties (overwrites existing)     |
| `registerOnce(properties)`      | Set properties only if not already set   |
| `unregister(key)`               | Remove a specific property               |
| `getSuperProperties()`          | Get all registered properties            |
| `clearSuperProperties()`        | Remove all super properties              |

::: warning In-Memory Storage
Super properties are stored in memory and reset on page reload. For persistent properties, store them externally (e.g., `localStorage`) and call `register()` on initialization.
:::

## Data Attribute Events

Track events declaratively from HTML without writing JavaScript. The SDK automatically binds event listeners based on `data-kb-*` attributes.

### Click Tracking

```html
<button
  data-kb-track-click="Add to Cart"
  data-kb-click-channel="ecommerce"
>
  Add to Cart
</button>
```

When the button is clicked, an event named `"Add to Cart"` is tracked on the `"ecommerce"` channel.

### Visibility Tracking

```html
<section
  data-kb-track-visibility="Pricing Section Viewed"
  data-kb-visibility-channel="marketing"
  data-kb-visibility-threshold="0.75"
>
  <!-- content -->
</section>
```

When 75% of the section becomes visible in the viewport, an event named `"Pricing Section Viewed"` is tracked on the `"marketing"` channel.

## Error Handling

The SDK provides typed error classes so you can handle specific failure modes precisely.

```typescript
import {
  AuthenticationError,
  ValidationError,
  TimeoutError,
  ApiError,
  KitbaseError
} from '@kitbase/analytics';

try {
  await kitbase.track({ channel: 'payments', event: 'Purchase' });
} catch (error) {
  if (error instanceof AuthenticationError) {
    // 401: Invalid or missing API key
  } else if (error instanceof ValidationError) {
    // 400/422: Missing or invalid fields - check error.field
  } else if (error instanceof TimeoutError) {
    // Request timed out (default: 30s)
  } else if (error instanceof ApiError) {
    // Other API error - check error.statusCode and error.response
  } else if (error instanceof KitbaseError) {
    // Generic SDK error
  }
}
```

| Error                 | HTTP Status | Description                            |
|-----------------------|-------------|----------------------------------------|
| `AuthenticationError` | 401         | Invalid or missing API key             |
| `ValidationError`     | 400/422     | Missing or invalid required fields     |
| `TimeoutError`        | --          | Request exceeded 30s timeout           |
| `ApiError`            | 4xx/5xx     | API returned an error response         |

For detailed error handling patterns including retry logic and best practices, see [Error Handling](/error-handling).

## REST API

You can send events directly to the Kitbase REST API without the SDK. This is useful for server-side integrations or languages without an official SDK.

### Single Event

```bash
curl -X POST https://ingest.kitbase.dev/sdk/v1/logs \
  -H "X-API-Key: YOUR_SDK_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "Purchase Completed",
    "properties": {
      "channel": "payments",
      "tags": { "amount": 99.99 }
    }
  }'
```

**Response** (`202 Accepted`):

```json
{
  "id": "evt_abc123",
  "event": "Purchase Completed",
  "timestamp": "2026-02-13T12:00:00.000Z"
}
```

### Batch Events

Send multiple events in a single request for better performance:

```
POST /sdk/v1/logs/batch
```

**Request body:**

```json
{
  "events": [
    {
      "event": "Page Viewed",
      "properties": { "channel": "analytics", "tags": { "path": "/pricing" } }
    },
    {
      "event": "CTA Clicked",
      "properties": { "channel": "conversions", "tags": { "button": "signup" } }
    }
  ]
}
```

**Response** (`202 Accepted`):

```json
{
  "accepted": 2,
  "total": 2
}
```

The batch endpoint accepts an array of events and returns the count of accepted events vs. total submitted.

## Next steps

* [Autocapture](/autocapture) — every event Kitbase tracks for you automatically.
* [Identify Users](/identify-users) — attach events to real users instead of anonymous IDs.
* [Events dashboard guide](/guide/events) — viewing, filtering, and analyzing events.
* [API reference](/api-reference#events) — track events over REST without an SDK.
