# Chat

Chat adds an AI chatbot and live human chat to your website, with native SDKs for your mobile apps. The AI answers from your knowledge base around the clock, and your agents take over in the dashboard when a visitor needs a person.

## How it works

- A visitor opens the widget on your site or app and talks to the **AI assistant** first; it answers from the knowledge you've configured.
- When the AI can't help, or the visitor asks, the conversation **hands off to a human agent** in Dashboard → Chat, honouring your routing rules and agent availability.
- Conversations are real-time both ways (server-sent events with WebSocket fallback), support **file attachments**, typing indicators and unread counts, and survive page reloads.
- Prompts are screened by the platform's AI-injection guard; manipulation attempts are rejected before they reach the model.

## Integration surfaces

| Surface | How | Auth |
|---------|-----|------|
| Web | `embed.js` script tag or the `NInstanceChat` JavaScript API (below) | A customer token minted by your server (recommended), or the full key on trusted internal pages |
| Android / iOS / React Native / Flutter | Native chat SDKs (sub-pages below) with a ready-made chat UI | The same two modes |

## Two exclusive auth modes (SDK 0.4.0)

Every chat client is configured with exactly **one** credential. Passing both, or neither, is a configuration error (`E_CONFIG`).

- **Customer token (recommended).** Your backend mints a short-lived token with `POST /api/v1/chat/sessions` using the full `keyId:secret`, and hands only the token to the client. The token is the identity: the customer's details never have to be passed into the SDK, a tampered client cannot claim to be somebody else, and the secret never leaves your server. Use this for every app or website you distribute to the public.
- **Key mode (trusted internal apps only).** The client is given the full `keyId:secret` and names the customer with `setUser`. This ships your secret inside the client, so use it only for internal tools and apps you control, never for public apps.

The older bare key ID form (`sk_live_abc123` with no secret) is **deprecated**. Deployed web embeds and SDKs below 0.4.0 keep working; the 0.4.0 mobile SDKs reject it. See [Authentication and identity](/authentication-and-identity) for both modes, the token API, the migration notes and the diagnostic error codes; see [Theming](/theming) for how dashboard, local and built-in themes combine.

To let the assistant call your own tools for a signed-in customer, see [MCP servers](/mcp-servers).


## Configure your chat widget

1. Log in to the merchant dashboard
2. Navigate to **Dashboard → Org → Chat** (`/dashboard/org/[orgId]/chat`)
3. Configure your widget: appearance, AI chatbot, routing rules, agent availability
4. Create a chat API key under **Dashboard → Org → API Keys** and store the full `keyId:secret` on your server. Your backend uses it to mint customer tokens; it never goes into public client code.


## Embed the widget via script tag (auto-init)

Add this to your HTML `<head>` or just before `</body>`, with a token your server minted for the signed-in customer:

```html
<script
  src="https://widget.newinstance.cloud/embed.js"
  data-customer-token="TOKEN_FROM_YOUR_BACKEND"
  data-theme="dark"
  data-position="bottom-right"
></script>
```

**data attributes**
| Attribute | Type | Default | Description |
|-----------|------|---------|-------------|
| `data-customer-token` | string | none | Customer token minted by your server. Selects customer token mode (recommended). Pass this or `data-api-key`, not both |
| `data-api-key` | string | none | The full `keyId:secret` selects key mode, for trusted internal pages only. A bare key ID is the deprecated legacy form |
| `data-theme` | `dark` or `light` | `light` | Widget colour scheme (only these two values) |
| `data-position` | `bottom-right` or `bottom-left` | `bottom-right` | Button position |
| `data-transport` | `sse` or `ws` | inherited | Realtime transport override (testing) |
| `data-customer-name` / `-email` / `-id` | string | none | Deprecated bare key ID embeds only. In token mode the identity comes from the token; in key mode call `setUser` |
| `data-auto-open` | boolean | false | Open the panel on load |
| `data-gql-endpoint` / `data-widget-url` | URL | derived | Self-host overrides for API endpoint and iframe origin |


## Embed via JavaScript API (full control)

```html
<script src="https://widget.newinstance.cloud/embed.js"></script>
<script>
const widget = NInstanceChat.init({
  customerToken: tokenFromYourBackend,
  theme: 'dark',
  position: 'bottom-right',
  autoOpen: false,
  callbacks: {
    onAuth: (info) => console.log('Chat auth:', info.valid, info.status),
    onOpen: () => console.log('Chat opened'),
    onClose: () => console.log('Chat closed'),
  },
});

// Programmatic control
widget.open();
widget.close();
widget.toggle();
widget.isOpen();
widget.setUserToken(freshTokenFromYourBackend);
widget.updateConfig({ theme: 'light' });
widget.destroy();

// File attachments (before opening composer)
widget.attachFile(file);
widget.attachFiles([fileA, fileB]);
widget.removeAttachment(id);
widget.clearAttachments();
widget.getAttachments();
widget.openComposer();
</script>
```

**`NInstanceChat.init(config)` options**
| Option | Type | Description |
|--------|------|-------------|
| `customerToken` | string | Customer token minted by your server. Selects customer token mode (recommended) |
| `apiKey` | string | The full `keyId:secret`. Selects key mode, for trusted internal pages only. Pass `customerToken` or `apiKey`, not both. A bare key ID is the deprecated legacy form |
| `theme` | `dark` or `light` | Widget theme |
| `transport` | `sse` or `ws` | Realtime transport; inherited from platform settings (SSE when nothing is advertised), override for testing |
| `gqlEndpoint` / `widgetUrl` | URL | Self-host overrides for the API endpoint and iframe origin |
| `customerName` / `customerEmail` / `customerId` | string | Deprecated bare key ID embeds only. Use the token, or `setUser` in key mode |
| `position` | `bottom-right` or `bottom-left` | Button position |
| `autoOpen` | boolean | Open the chat on load |
| `buttonSize` | number | Launcher button size in px |
| `panelWidth` | number | Chat panel width in px |
| `panelHeight` | number | Chat panel height in px |
| `zIndex` | number | CSS z-index for the widget |
| `callbacks` | object | `onReady`, `onAuth`, `onOpen`, `onClose`, `onDestroy` |


The instance `init` returns also has `setUserToken(token)`, which swaps the customer token without reloading the iframe, and `setUser({ customerName, customerEmail, customerId })`, which names the customer in key mode. `setUserToken` is a configuration error in key mode.

Defaults: `buttonSize` 60, `panelWidth` 400, `panelHeight` 620, `zIndex` 2147483647, `autoOpen` false, `position` bottom-right. Calls made before the auth handshake are queued and flushed after validation; every bridged call times out after 15 seconds. `attachFile` accepts a File, Blob, data URL or bare base64 plus an optional name; attachment snapshots carry id, name, type, size, status, progress and an error reason or URL. `openComposer()` also opens the panel locally, so `isOpen()` flips synchronously. `updateConfig` reloads the iframe and resets an in-progress conversation; only theme, customerName and customerEmail are re-applied.

Widget behaviour beyond these options (welcome and offline messages, input placeholder, file-upload, emoji and typing toggles, email-required, sound, online status, branding) is configured in the dashboard, not from the embed.

## Conversation model

Flow states run `idle → collecting_info → bot_conversation → handoff_pending → live_chat → ended | error`; conversation statuses are BOT_ACTIVE, WAITING, ACTIVE, RESOLVED and CLOSED; channels are ai, live and ticket. The pre-chat info form is skipped when the customer is already known: in customer token mode, or when a non-blank `customerName` was supplied. The transport heartbeats roughly every 10 seconds; a missed beat within about 16 seconds tears down, reconnects and replays missed messages from the server event log; connectivity is re-checked on online, visibilitychange and pageshow.

## Widget lifecycle

- Auto-init runs on window load (immediately if the document is already complete) and only when `data-customer-token` or `data-api-key` is present.
- A second `init()` returns null and warns that the widget is already initialized.
- If the iframe never answers, a 10 second safety timeout removes the widget silently; on auth failure the root is removed, `onDestroy` fires, queued calls reject, and the console explains that chat is unavailable, disabled or not configured.
- `destroy()` notifies the iframe, then rejects in-flight bridged calls with "widget destroyed".
- The host page CSP must allow the widget origin in both `frame-src` and `script-src`.

## How the iframe bridge works

The widget loads in a sandboxed `<iframe>`. All communication between the host page and the widget uses `postMessage` with source-tag and origin validation:

- Host → widget messages are tagged with `ninstance-chat-host`
- Widget → host messages are tagged with `ninstance-chat`

The widget fires `onAuth` after credential validation. The button stays hidden until `onAuth` confirms `valid: true`.


## Permission scopes for Chat API keys

| Scope code | Description |
|------------|-------------|
| `full-access` | All chat permissions (recommended) |
| `ai-chatbot-access` | Interact with AI chatbot |
| `live-chat-operations` | Manage live chat sessions |
| `chat-configuration` | Configure widget settings |
