> ## 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.

# Widgets & chat

> Track funnel events from MCP-app widgets and chat host pages with the same client the server uses.

<Note>
  **Platform feature.** The browser surfaces authenticate with credentials they already hold: a widget token injected by `withWaniwani`, or the chat widget's public `wwp_...` token. No API key ships to the browser. [About the Platform](/sdk/platform/overview).
</Note>

Widgets are where funnel steps actually happen: the user compares plans, picks an option, clicks the buy link. Both browser surfaces expose the same `track` client as the server, with session identity attached automatically.

## MCP-app widgets: `useWaniwani()`

For widgets rendered inside tool responses (ChatGPT apps, Claude MCP apps). Requirements: the MCP server is wrapped with `withWaniwani(server)`, which injects the tracking endpoint, a short-lived widget token, and the session id into each tool response under `_meta["waniwani/widget"]`.

`useWaniwani()` is host-agnostic: it takes that metadata as data and opens no connection to the widget host itself. How you hand it the metadata depends on your host.

**skybridge-hosted widgets.** Import the hook from the skybridge adapter entry and call it bare. It reads skybridge's `useToolInfo().responseMetadata` and feeds it to the hook for you:

```tsx theme={null}
import { useWaniwani } from "@waniwani/sdk/mcp/react/skybridge";

function PlanPicker({ plans }) {
  const wani = useWaniwani();

  return plans.map((plan) => (
    <button
      key={plan.id}
      onClick={() =>
        wani.track.optionSelected({ id: plan.id, amount: plan.price, currency: "EUR" })
      }
    >
      {plan.name}
    </button>
  ));
}
```

`skybridge` is an optional peer dependency of the SDK; a skybridge project already has it.

**Any other host.** Import from `@waniwani/sdk/mcp/react` and pass the tool-response `_meta` your host exposes as `toolResponseMetadata`:

```tsx theme={null}
import { useWaniwani } from "@waniwani/sdk/mcp/react";

const wani = useWaniwani({ toolResponseMetadata });
```

The returned widget surface:

| Member                      | What it does                                                                                    |
| --------------------------- | ----------------------------------------------------------------------------------------------- |
| `track(event)`              | Send any typed event, session attached: `track({ event: "link.clicked", properties: { url } })` |
| `track.priceShown()` etc.   | The five flat revenue helpers, same as the server                                               |
| `identify(userId, traits?)` | Emits `user.identified` and stamps `externalUserId` on later events                             |
| `sessionId`                 | The id stamped on this widget's events, for correlating with your own systems                   |
| `flush()`                   | Flush buffered events immediately                                                               |

One `widget_render` event is emitted automatically when the hook initializes with resolved config, so "widget shown" exists as a funnel step even if you track nothing manually.

With no resolved endpoint and source (no metadata, no explicit options), the hook returns a no-op widget, so rendering never breaks.

### Explicit configuration

When the widget cannot resolve config from the host (or you proxy events through your own backend), pass the endpoint directly. `source` is required in this mode so events attribute to a channel:

```tsx theme={null}
const wani = useWaniwani({
  endpoint: "https://yourapp.com/api/waniwani/track",
  source: "web",
});
```

Pair this with [`createTrackingRoute`](/sdk/configuration/wrap-server) on your backend when you want zero Waniwani credentials in the browser.

## Chat host pages: `chat.track`

The chat embed and `WaniwaniChat` hold the public token and the server-assigned session, so the host page can send funnel events with zero setup. Events carry the `sessionId` once the first exchange assigns one, and the anonymous `visitorId` before that.

With the `<script>` embed:

```html theme={null}
<script>
  document.querySelector("#buy").addEventListener("click", () => {
    WaniWani.chat.track.converted({ amount: 85, currency: "EUR" });
  });

  // When your page knows who the user is:
  WaniWani.chat.identify("user_123", { plan: "pro" });
</script>
```

With the React component, the same surface lives on the ref handle:

```tsx theme={null}
import { useRef } from "react";
import { WaniwaniChat, type ChatHandle } from "@waniwani/sdk/chat";

function Page() {
  const chat = useRef<ChatHandle>(null);

  return (
    <>
      <WaniwaniChat ref={chat} token="wwp_..." channelId="51c3658a-..." />
      <button onClick={() => chat.current?.track?.optionSelected({ id: "pro", amount: 49, currency: "EUR" })}>
        Choose Pro
      </button>
    </>
  );
}
```

`track` and `identify` are absent on the bare `ChatEmbed` primitive, which is bring-your-own-backend and holds no Waniwani credential.

To mirror chat lifecycle events (opened, closed, message sent/received) into your own analytics instead, use the embed's `onEvent` callback; it runs in the page's JS context and carries the `sessionId`.

## Custom surfaces: `createFrontendClient`

Both surfaces above are wrappers around one primitive, exported for anything they do not cover:

```ts theme={null}
import { createFrontendClient } from "@waniwani/sdk";

const client = createFrontendClient({
  endpoint: "https://app.waniwani.ai/api/mcp/events/v2/batch",
  token: "wwp_...",                        // public token or widget JWT
  source: "web",
  identity: () => ({ sessionId: currentSessionId, visitorId }),
});

await client.track.priceShown({ amount: 49, currency: "EUR" });
```

It shares the server client's mapper and batching transport, adds keepalive flushing on page hide/unload, and reads `identity()` at emit time so late-arriving ids (a session assigned mid-page) are picked up without recreating the client.

## Delivery behavior

* Events batch in memory and flush on a short timer, on batch size, and with `keepalive` requests when the page is hidden or unloading.
* Tracking never throws into the host page; failures are logged and dropped.
* On an auth failure (expired widget token, revoked public token) the transport stops and logs the reason once, instead of retrying forever.
