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

# Messages and status

> Send messages on the visitor's behalf, handle busy rejections, and follow the assistant's status.

## Send a message

`sendMessage(text)` submits a message as if the visitor had typed it. The message appears in the chat, is saved to the conversation, and the assistant answers it.

```javascript theme={null}
const chatbot = tai.getInstance("YOUR_ASSISTANT_ID");
const result = await chatbot.sendMessage("Summarize this page for me.");

if (!result.success) {
  console.warn(`Not sent (${result.code}): ${result.message}`);
}
```

Call it any time after the agent is ready. The SDK handles the timing: it waits for the iframe, for any pending `identify()` to verify, and for the conversation history to load before the message goes in. You do not need to sequence those yourself.

The Promise never rejects. It resolves when the message is accepted or rejected, not when the answer finishes. Ignoring the Promise is safe.

### Result shape

```typescript theme={null}
type ChatbotSendResult =
  | { success: true }
  | {
      success: false;
      code: "busy" | "not_ready" | "timeout" | "destroyed" | "invalid_message";
      message: string;
    };
```

| Code              | Cause                                                                                  | What to do                                                                                          |
| ----------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `busy`            | The assistant is answering a previous message, or a specialist handoff is in progress. | Wait for `answerComplete`, then send again. See [Handle busy](#handle-busy).                        |
| `invalid_message` | The text was empty or only whitespace.                                                 | Fix the caller.                                                                                     |
| `not_ready`       | The iframe accepted the request but the conversation was not loaded when it ran.       | Rare, because the iframe waits for history first. Retry after `conversationReady()`.                |
| `timeout`         | No reply from the iframe within 35 seconds.                                            | Do not resend automatically. The message may have gone through. Check the assistant ID and network. |
| `destroyed`       | `tai.destroyInstance()` ran before the message was posted.                             | Re-initialize the agent before sending.                                                             |

### Messages are not queued

The agent accepts one message at a time. A second `sendMessage()` while the assistant is still answering is rejected with `busy` instead of being queued or interrupting the current answer. Typing in the chat input while busy is also blocked, so the SDK and the visitor share the same slot.

## Handle busy

Track the assistant's status with the events below and send only when it is idle. This example queues messages on the page side and drains the queue after each answer.

```javascript theme={null}
const chatbot = tai.getInstance("YOUR_ASSISTANT_ID");
const queue = [];
let idle = true;

async function flush() {
  if (!idle || queue.length === 0) return;
  idle = false;
  const result = await chatbot.sendMessage(queue[0]);
  if (result.success) {
    queue.shift(); // answerComplete sets idle and calls flush again
    return;
  }
  idle = true;
  if (result.code !== "busy") {
    queue.shift(); // drop messages that can never succeed
    console.warn(result.code, result.message);
  }
}

chatbot.onAnswerComplete(() => {
  idle = true;
  flush();
});

function enqueue(text) {
  queue.push(text);
  flush();
}
```

A simpler pattern for a single button is to disable the button on `thinking` and re-enable it on `answerComplete`.

## Follow the assistant's status

The iframe reports its status to the SDK, and the SDK emits one named event per transition.

| Event            | Fires when                                                   | Typical use                             |
| ---------------- | ------------------------------------------------------------ | --------------------------------------- |
| `thinking`       | A message was submitted and the first token has not arrived. | Show a spinner, disable input.          |
| `answering`      | The first token arrived and the reply is streaming.          | Hide the spinner.                       |
| `answerComplete` | The reply finished and the assistant is idle.                | Re-enable input, send the next message. |

The events fire for every message, whether it came from `sendMessage()`, the visitor typing, or a suggested question click. No event fires when a request fails with an error; the chat shows the error inline instead. If you need a fallback, start a timer on `thinking` and clear it on `answering` or `answerComplete`.

```javascript theme={null}
chatbot.onThinking(() => spinner.hidden = false);
chatbot.onAnswering(() => spinner.hidden = true);
chatbot.onAnswerComplete(() => input.disabled = false);
```

The `onThinking`, `onAnswering`, and `onAnswerComplete` helpers wrap `chatbot.on(eventName, callback)`. The instance is a Node-style `EventEmitter`, so `on`, `once`, and `off` all work with the event names in the table.

```javascript theme={null}
chatbot.once("answerComplete", () => console.log("First answer done"));
```

## Wait for the conversation

`conversationReady()` resolves once the iframe has applied the current chat's history, including an empty history that shows the agent's opener. Use it before `getConversation()`, or when you want to know that a returning visitor's previous messages are on screen.

```javascript theme={null}
await chatbot.conversationReady();
const conversation = await chatbot.getConversation();
```

It rejects with an `Error` whose `code` is `timeout` if history does not load within 30 seconds, or `destroyed` if the instance was destroyed. `sendMessage()` already includes this wait, so you do not need both.

## Read the conversation

`getConversation()` returns the messages currently shown in the chat.

```javascript theme={null}
const { assistantId, messages } = await chatbot.getConversation();
for (const message of messages) {
  console.log(message.role, message.content);
}
```

```typescript theme={null}
interface MessageEntry {
  id: string;
  role: "user" | "assistant";
  content: string;   // text parts joined with a space; empty for tool-only messages
  parts: unknown[];  // raw message parts, including tool calls
  timestamp: number; // Unix ms; falls back to the time of the call for messages without a stored time
}

interface ConversationData {
  assistantId: string;
  messages: MessageEntry[];
}
```

The request times out after 5 seconds and the Promise rejects. Call it after `ready()`; before that there is no iframe to answer.

## Reset the conversation

`resetConversation()` deletes the current chat, starts a new one, and shows the agent's opener.

```javascript theme={null}
await chatbot.resetConversation();
await chatbot.sendMessage("Let's start over. What can you help with?");
```

Resetting while the assistant is answering stops the in-flight response and frees the busy slot, so the next `sendMessage()` is accepted. A send issued right after `resetConversation()` resolves waits for the new chat's history and lands in the new chat. The reset request times out after 5 seconds and the Promise rejects.

## Sending after identify()

`identify()` verifies the user with the server asynchronously. A `sendMessage()` issued right after it is held until verification finishes, then posted with the identity attached. If verification fails, the SDK logs the error and the message goes to the anonymous chat. See [Identity verification](/sdks/embed-sdk/identity-verification).

## Popover mode

`sendMessage()` does not open a closed popover panel, and it does not clear text the visitor has typed. The SDK has no public open method. The button is the element with ID `teamai-chatbot-bubble-button-YOUR_ASSISTANT_ID`, and calling `.click()` on it toggles the panel. That ID is an implementation detail and may change.

## Multi-agent events

When the agent hands a task to a specialist, the SDK emits two more events. Neither replaces the status events above, which continue to fire for the visible answer.

```javascript theme={null}
chatbot.onAgentHandoff((data) => {
  // data.targetAgentId, data.targetAgentName, data.task, data.reason, data.context
  showBanner(`${data.targetAgentName} is working on: ${data.task}`);
});

chatbot.onSpecialistReturn((data) => {
  // data.completedTask, data.taskResults, data.taskSummary, data.additionalContext
  hideBanner();
});
```

The raw event names are `agentHandoff` and `returnToMainAgent`. Internal coordination messages between agents are hidden from the visitor. While a handoff is in progress, `sendMessage()` returns `busy`.
