Skip to main content
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

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().
userId
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.
properties
Record<string, unknown>
User-level traits (name, email, plan, signup date, and so on). These land in the event properties payload.
meta
Record<string, unknown>
MCP request metadata, typically extra._meta. Required so the identify call can be correlated with the current session.

Usage

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:
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:
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:
// Inside a flow node
.addNode("record_email", 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.
Calling identify() multiple times with the same userId is fine. Each call is an independent user.identified event.