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

# Identify Users

> Attach an external user id to the current session.

<Note>
  **Platform feature.** Requires `WANIWANI_API_KEY`. Works whether your MCP server is self-hosted or on Managed Hosting. [About the Platform](/sdk/platform/overview).
</Note>

`identify()` sends a `user.identified` event and attaches an `externalUserId` to the current session's correlation ids. Use it once you know who the user is (an email, a database id, a Stripe customer id, or any stable identifier in your system).

## Signature

```ts theme={null}
identify(
  userId: string,
  properties?: Record<string, unknown>,
  meta?: Record<string, unknown>,
): Promise<{ eventId: string }>;
```

Under the hood it calls `track()` with `event: "user.identified"` and passes `userId` as `externalUserId`. The call returns `{ eventId }` immediately, the same as `track()`.

<ResponseField name="userId" type="string" required>
  A stable external identifier for the user. Database id, CRM record id, Stripe customer id, or similar. Emails work but are not ideal because they change.
</ResponseField>

<ResponseField name="properties" type="Record<string, unknown>">
  User-level traits (name, email, plan, signup date, and so on). These land in the event `properties` payload.
</ResponseField>

<ResponseField name="meta" type="Record<string, unknown>">
  MCP request metadata, typically `extra._meta`. Required so the identify call can be correlated with the current session.
</ResponseField>

## Usage

```ts theme={null}
await wani.identify(
  "user_01HX123ABC",
  { email: "customer@example.com", plan: "pro" },
  extra._meta,
);
```

A typical pattern is to call `identify()` from the tool that first learns the user's identity:

```ts theme={null}
server.registerTool(
  "submit_email",
  { /* ... */ },
  async ({ email }, extra) => {
    await wani.identify(email, { email }, extra._meta);
    return { content: [{ type: "text", text: "Thanks" }] };
  },
);
```

## Identify inline with `track()`

If the same tool call both learns the identity and produces a domain event, pass `externalUserId` on the `track()` call instead of making a separate `identify()` call:

```ts theme={null}
await wani.track({
  event: "quote.succeeded",
  properties: { amount: 99, currency: "USD" },
  externalUserId: "user_01HX123ABC",
  meta: extra._meta,
});
```

This produces one event instead of two.

## Inside tools and flows

When the server is wrapped with `withWaniwani()`, the scoped client on `context.waniwani` (in `createTool`) or `waniwani` (in flow nodes) already has the request's `_meta` attached, so `identify()` takes only `userId` and optional `properties`:

```ts theme={null}
// Inside a flow node
.addNode({
  id: "record_email",
  run: async ({ state, waniwani }) => {
    if (state.email) {
      await waniwani?.identify(state.email, { email: state.email });
    }
    return {};
  },
})
```

## Anonymous vs. identified sessions

Sessions start anonymous. They get a `sessionId` from `_meta` but no `externalUserId`. When you call `identify()` (or pass `externalUserId` on a `track()` call), future events in the session carry the user id. How the backend back-fills earlier events in the same session for a newly identified user is a server-side concern, not something the SDK controls.

<Tip>
  Calling `identify()` multiple times with the same `userId` is fine. Each call is an independent `user.identified` event.
</Tip>
