# Web SDK chat API

> Control the Runbear chat widget from your page — mount, reset and reopen conversations, send messages, and listen for its lifecycle and session events.

Source: https://docs.runbear.io/api/web-sdk/chat-api

Last updated: 2026-09-30

`client.createChat()` returns the widget instance, typed `Chat`. Your page drives it with the
methods below and follows what it is doing through events.

```ts
const client = new Runbear({ assistantId: "…", auth: { mode: "session", fetchSessionToken } })

const chat = client.createChat()
chat.on("containerReady", ({ threadId }) => saveThreadId(threadId))
chat.on("sessionFailed", ({ reason }) => showRetry(reason))
chat.mount("#runbear-chat")
```

`createChat(threadId)` creates a widget that opens an existing conversation instead of starting a new
one. See [Saving and reopening a conversation](#saving-and-reopening-a-conversation).

## Methods

| Method                      | What it does                                                                                                                                                                                                                                                                                                                                                  |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mount(target)`             | Renders the widget into `target`, a CSS selector or an `HTMLElement`, and starts the conversation. Throws "Could not find element to mount chat widget" when the selector matches nothing, and "Chat widget is already mounted" on a second call. Does nothing outside a browser. Mount into an empty element: see [Limitations](/api/web-sdk.md#limitations) |
| `destroy()`                 | Removes the widget, removes every event listener, and stops its session renewals. Call it when the element goes away, such as in a React effect's cleanup                                                                                                                                                                                                     |
| `on(event, callback)`       | Subscribes to an event                                                                                                                                                                                                                                                                                                                                        |
| `off(event, callback)`      | Unsubscribes the same callback                                                                                                                                                                                                                                                                                                                                |
| `reset()`                   | Starts a new, empty conversation. In session mode it asks your endpoint for a new session, without a resume token                                                                                                                                                                                                                                             |
| `startWithMessage(text)`    | Starts a new conversation and sends `text` as its first message                                                                                                                                                                                                                                                                                               |
| `startWithThread(threadId)` | Opens an existing conversation and loads its history. In session mode it first mints a session bound to that thread                                                                                                                                                                                                                                           |
| `addUserMessage(text)`      | Sends `text` as if the visitor had typed it. Before the widget has a conversation it does nothing and logs a warning                                                                                                                                                                                                                                          |
| `addSystemMessage(text)`    | Queues `text` as a system message, sent together with the next user message. Before the widget has a conversation it does nothing and logs a warning. `reset()`, `startWithMessage()` and `startWithThread()` clear the queue                                                                                                                                 |
| `retrySession()`            | Session mode only: recovers after `sessionFailed`. Does nothing in any other state. See [Errors](/api/web-sdk/errors.md#session-failures)                                                                                                                                                                                                                     |

## Events

| Event            | Payload                      | When                                                                                                                                                                          |
| ---------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `chatCreated`    | `{ threadId }`               | The widget has a new conversation: created on mount or by `reset()`, `startWithMessage()` or, in session mode, `startWithThread()`                                            |
| `chatStarted`    | None                         | The first message appears in a conversation                                                                                                                                   |
| `rendered`       | None                         | The widget has rendered its UI. It renders again after `reset()`, `startWithMessage()`, `startWithThread()` and a session change, so this can fire several times              |
| `containerReady` | `{ threadId }`               | Once per mount, when the widget first has a conversation and can accept messages                                                                                              |
| `messagesLoaded` | `{ threadId, messageCount }` | After `startWithThread()` has loaded the history, and after `startWithMessage()` has created its conversation                                                                 |
| `sessionReady`   | `{ sessionId, threadId }`    | Session mode: the first session is live                                                                                                                                       |
| `sessionRenewed` | `{ sessionId }`              | Session mode: the pass was renewed; same conversation                                                                                                                         |
| `sessionChanged` | `{ sessionId, threadId }`    | Session mode: a different session is now active, after `reset()`, `startWithMessage()`, `startWithThread()`, or when your endpoint minted a new session instead of refreshing |
| `sessionFailed`  | `{ reason }`                 | Session mode: the session ended. `reason` is `"declined"`, `"expired"`, `"network"` or `"misconfigured"`. See [Errors](/api/web-sdk/errors.md#session-failures)               |

The `ChatEventSession*Data` payload types are exported from 0.2.0. `ChatEventCreatedData`,
`ChatEventContainerReadyData` and `ChatEventMessagesLoadedData` are exported from 0.5.0 (not yet
released); on 0.4.0 those payloads have the same shapes but no exported names. See [Types](/api/web-sdk/types.md#events).

## Lifecycle

```
mount()
  ├─ proxy or direct mode ─▶ POST /v1/threads ─▶ chatCreated ─▶ containerReady
  │                          (with createChat(threadId): containerReady only)
  └─ session mode ─▶ fetchSessionToken() ─▶ sessionReady ─▶ chatCreated ─▶ containerReady

while mounted
  ├─ session mode ─▶ sessionRenewed on each renewal
  ├─ reset() / startWithMessage() ─▶ (sessionChanged) ─▶ chatCreated
  ├─ startWithThread(id) ─▶ (sessionChanged ─▶ chatCreated) ─▶ messagesLoaded
  └─ a session ends ─▶ sessionFailed ─▶ retrySession()

destroy()
```

Two rules follow from it:

- **Subscribe before you mount.** Some events fire during `mount()` itself, such as
  `containerReady` for `createChat(threadId)` outside session mode.
- **Wait for `containerReady` before sending.** `addUserMessage()` and `addSystemMessage()` need a
  conversation; called earlier they do nothing and log a warning. In session mode `sessionReady`
  fires just **before** the widget adopts the session's conversation, so a message sent from a
  `sessionReady` handler is dropped. Use `sessionReady`, `sessionChanged` and `sessionFailed` to
  follow the session, and `containerReady` or `chatCreated` to know the widget can take a message.

```ts
chat.on("containerReady", () => {
  chat.addSystemMessage("The visitor is on the pricing page.")
  chat.addUserMessage("What does the Pro plan include?")
})
chat.mount("#runbear-chat")
```

## Saving and reopening a conversation

To let a visitor come back to a conversation later, on another device or after the stored session
has expired:

1. **Save the thread id.** Record the `threadId` from `chatCreated` in your own database, next to
   the visitor.
2. **Reopen it.** Create the widget with `client.createChat(savedThreadId)`, or call
   `chat.startWithThread(savedThreadId)` on a mounted one.
3. **In session mode, forward the thread id once.** The SDK passes it to `fetchSessionToken` as
   `threadId` for that one mint; your endpoint forwards it as `thread_id` to `POST /v1/sessions`.
   Forward it only when the SDK sends it. An endpoint that always adds the saved id makes `reset()`
   reopen the old conversation instead of starting a new one.

Reopening a conversation replaces the widget's current session, including the stored resume token.
The thread must belong to the same agent and your organization, or the mint returns `404`. If your
endpoint ignores `threadId`, the visitor lands in a new conversation and `sessionChanged` reports a
different thread id from the one you asked for.

Within one browser, you don't need this: the default `storage: "session"` already resumes the
conversation after a reload. See
[Where the session is stored](/api/web-sdk/authentication.md#where-the-session-is-stored).

## Related

- [Authentication](/api/web-sdk/authentication.md): session mode and your endpoint
- [Errors](/api/web-sdk/errors.md): recovering from `sessionFailed`
- [Types](/api/web-sdk/types.md): `Chat` and the event payload types
