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

# Events and identify with the server SDK

> track(), flush() and trackNow() for event delivery, identify() for identity and declare() for configuration, with the lifecycle hook each runtime needs.

## Track

```ts theme={null}
const { id, queued } = curious.track({
  userId: user.id,
  name: "purchased",
  value: 49.99,
  properties: { plan: "pro" },
  timestamp: new Date(order.createdAt), // optional; for buffered or backfilled events
  id: `ord_${order.id}_purchased`,     // optional; a stable id of your own
});
```

`track()` returns at once. It mints an `id` of the form `sdk-js:<uuid>` when you do not supply one, queues the event, and keeps the id across every retry, so a flaky network never double-counts. It validates `userId`, `name`, `value` and `timestamp` synchronously and throws `invalid_input` on a bad argument. When the queue holds `eventQueueLimit` events (1,000 by default), it returns `{ queued: false }` and reports `queue_full` through `onError`; nothing earlier is evicted.

## Flush

The queue is in memory and not durable. Hand `flush()` to your runtime's lifecycle:

```ts theme={null}
ctx.waitUntil(curious.flush());          // Cloudflare Workers
EdgeRuntime.waitUntil(curious.flush());  // Supabase Edge Functions
await curious.flush();                   // jobs, scripts and graceful shutdown
```

A flush also happens on its own every `eventFlushIntervalMs` (5 seconds) and whenever `eventFlushSize` (100) events are queued. `flush()` sends batches, retries transient failures with backoff under the same ids up to `eventMaxAttempts` (5), honours `Retry-After`, drops items the API rejects (reported as `event_rejected`), and resolves with a summary rather than throwing:

```ts theme={null}
type FlushSummary = { sent: number; duplicates: number; failed: number; pending: number; errors: CuriousErrorSummary[] };
```

`trackNow(event)` sends one event immediately and returns its item result, for the rare case that must not wait.

## Identify

```ts theme={null}
await curious.identify({
  userId: "user_123",
  anonymousId: "anon_456",          // omit for a profile-only update
  identifiers: { email: user.email },
  attributes: { plan: user.plan },
});
```

Identify is an ordered mutation: it is sent at once, never queued behind events, retried only on transient failures, and awaited. Call it from your server when a user logs in, and await it before the next `decide` that should see the merged profile. It throws a `CuriousError`: `identity_conflict` on a `409`, `authentication_error` on a bad key. See [Identify users](https://docs.trycurious.ai/guides/identify.md).

## Declare

```ts theme={null}
const declared = await curious.declare({
  element: "pricing_hero",
  elementType: "hero",
  control: "Ship better experiments without manual analysis",
  rewards: [{ name: "signed_up", windowSeconds: 7 * 86_400 }],
  options: { assignment: "static" },
});
// declared.experimentId, declared.experimentStatus, declared.warnings
```

The same camelCase declaration as `decide()`'s `declaration`, without a user. It throws a `CuriousError` on failure, so a deploy step that declares fails loudly. See [Declare ahead of traffic](https://docs.trycurious.ai/guides/declare.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.