# Widget SDK

The browser SDK mounts Datalumo search and chat on your site. It loads presentation settings from your widget, streams answers, stores chat history in the browser, and reports analytics. For custom UIs or server-side calls, use the [Widget API](/docs/widgets) instead.

Script URL:

```
https://datalumo.app/widget/v1/datalumo.js
```

The SDK exposes a global `Datalumo` object. The API origin is taken from the script URL, so one build works on any host.

## Before you embed

1. Create a **search** or **chat** widget in the dashboard.
2. Add your site domain under the widget's websites list (for example `example.com` or `www.example.com`). Browser requests are authorised by the page origin.
3. Copy the **widget key**. It looks like `{organisation}/{widget}` (two public ids). It is safe to put in HTML.

You can also copy a ready-made snippet from the widget page under **Add to your site**.

## Install

Paste before `</body>`:

### Chat bubble

```html
<script src="https://datalumo.app/widget/v1/datalumo.js"></script>
<script>
  Datalumo.chat('ORG_ID/WIDGET_ID').mount();
</script>
```

### Search (modal trigger in a container)

```html
<div id="search"></div>

<script src="https://datalumo.app/widget/v1/datalumo.js"></script>
<script>
  Datalumo.search('ORG_ID/WIDGET_ID', { target: '#search' }).mount();
</script>
```

Load the script once per page. Call `.mount()` after the DOM nodes you pass as `target` or `trigger` exist.

## Entry points

| Call | Use when |
|---|---|
| `Datalumo.chat(key, options?)` | Chatbot widget (bubble or inline) |
| `Datalumo.search(key, options?)` | Search widget (modal or inline) |
| `Datalumo.headless(key, options?)` | No UI — search, chat, events, and conversation APIs yourself |
| `Datalumo.markdown(text)` | Turn markdown from chat/summarize into sanitised HTML (headless UIs) |

Every call returns a handle. For chat and search, call `.mount()` to start. Headless is ready immediately.

## Shared options

These apply to `chat()`, `search()`, and `headless()`:

| Option | Type | Description |
|---|---|---|
| `user` | `{ id, hash? }` | Tag the visitor. With a server-signed `hash`, conversation history and ownership work (see [Identity](#signed-visitor-identity)). |
| `context` | `object` or `false` | Extra page context sent with chat. By default the SDK adds the current page URL and title. Pass an object to merge static fields over that, or `false` to send no context. |
| `token` | string | Preview Bearer token. Only for the in-app preview; do not use on your public site. |

## Mount options (chat & search)

On mount, the SDK loads config from the server, then merges your overrides. Dashboard settings are the defaults; anything you pass in the second argument wins **except** branding (plan-controlled).

| Option | Type | Description |
|---|---|---|
| `target` | CSS selector | Where to place the UI. Required for **inline** chat and for search when you want the SDK-rendered box. |
| `trigger` | CSS selector | Use your own element(s) instead of the default launcher or search box. See variants below. |
| `variant` | string | Override the shape from the dashboard. Chat: `bubble` \| `inline`. Search: `modal` \| `inline`. |
| `theme` | string | `auto` \| `light` \| `dark` |
| `accent_light` | hex string | Accent colour in light mode (for example `#18181b`) |
| `accent_dark` | hex string | Accent colour in dark mode |
| `placeholder` | string | Input placeholder |
| `greeting` | string | Chat only: first assistant message |
| `prompts` | `string[]` | Chat only: suggested questions shown as chips while the thread is empty (max 6). Click sends the question. Override from the SDK or set in the dashboard. |
| `avatar` | string | Chat only: image URL for the assistant |
| `summary` | boolean | Search only: AI summary above results (uses answer credits) |
| `speech_input` | boolean | Microphone button where the browser supports it |
| `debounce` | number | Search only: ms to wait after typing before searching (default `1000`) |
| `default_language` | string | Fallback UI language when the browser language is unsupported |

### Suggested questions (chat)

On an empty conversation the chat can show **chips** under the greeting. Visitors click a chip to send that question immediately.

Set them in the dashboard (**Suggested questions**, one per line), or pass `prompts` when mounting:

```js
Datalumo.chat('ORG_ID/WIDGET_ID', {
  variant: 'inline',
  target: '#help',
  prompts: [
    'How does pricing work?',
    'What is a refund?',
    'How do I get started?',
  ],
}).mount();
```

Limits: up to **6** prompts, **100** characters each. Chips hide after the first message and return on **New conversation**.

### Chat variants

**Bubble** (default) — floating launcher opens a panel. No `target` needed.

```js
Datalumo.chat('ORG_ID/WIDGET_ID').mount();
```

**Custom launcher** — hide the default bubble and open from your own control(s). Any matching element toggles the panel; you can also call `open()` / `close()` / `toggle()` on the handle:

```html
<button id="open-help">Ask us</button>

<script src="https://datalumo.app/widget/v1/datalumo.js"></script>
<script>
  const chat = Datalumo.chat('ORG_ID/WIDGET_ID', {
    trigger: '#open-help', // or '.open-chat' for several buttons
  }).mount();

  // Optional: open from your own code without a click binding
  // chat.open();
</script>
```

The default floating bubble is not rendered when `trigger` is set. Use a CSS selector that exists when `.mount()` runs.

**Inline** — panel fills a container (set a height on the container):

```html
<div id="help" style="height: 560px;"></div>
<script>
  Datalumo.chat('ORG_ID/WIDGET_ID', {
    variant: 'inline',
    target: '#help',
  }).mount();
</script>
```

### Search variants

**Modal** (default) — Cmd/Ctrl+K opens a full search overlay. Optionally render a search-box trigger into `target`, or bind your own `trigger`.

```js
// Keyboard only (Cmd/Ctrl+K)
Datalumo.search('ORG_ID/WIDGET_ID').mount();

// Trigger box inside #search
Datalumo.search('ORG_ID/WIDGET_ID', { target: '#search' }).mount();

// Your own button(s) open the modal
Datalumo.search('ORG_ID/WIDGET_ID', { trigger: '.open-search' }).mount();
```

**Inline** — input with results listed underneath.

```js
// SDK renders the input into #search
Datalumo.search('ORG_ID/WIDGET_ID', {
  variant: 'inline',
  target: '#search',
}).mount();

// Use an existing <input>; results appear after it (or in target)
Datalumo.search('ORG_ID/WIDGET_ID', {
  variant: 'inline',
  trigger: '#my-search-input',
  target: '#results', // optional
}).mount();
```

## Widget methods

After `mount()`, chat and search handles support:

| Method | Applies to | Description |
|---|---|---|
| `mount()` | both | Start loading config and render. Safe to call once; returns the same handle. |
| `destroy()` | both | Remove the widget from the page. |
| `open()` | both | Open the bubble panel or search modal. |
| `close()` | both | Close the panel or modal. |
| `toggle()` | chat | Open if closed, close if open. |

Imperative methods queue until the widget is ready, so you can call them right after `mount()`.

## Headless client

For a fully custom UI:

```js
const dl = Datalumo.headless('ORG_ID/WIDGET_ID', {
  user: { id: userId, hash }, // optional, signed
  context: { product: 'billing' }, // optional
});

// Search
const hits = await dl.search('refund policy', { limit: 8 });

// Chat (full reply)
const reply = await dl.chat('How do refunds work?');
// reply: { text, conversationId, answerId, sources }

// Chat (stream tokens)
await dl.chat('How do refunds work?', {
  conversationId: reply.conversationId,
  onToken: (delta, full) => { /* update UI */ },
  onStatus: (status) => { /* e.g. searching */ },
});

// Optional AI summary of search results
const summary = await dl.summarize('refund policy', {
  onToken: (delta, full) => {},
});
// summary: { summarized, text }

// Analytics (fire-and-forget)
dl.trackEvent('click', { url: 'https://…', rank: 0, source: 'result' });

// Render markdown safely
container.innerHTML = Datalumo.markdown(reply.text);
```

Useful properties and helpers on the client:

| Member | Description |
|---|---|
| `sessionId` | Analytics session for this tab (auto-created and reused) |
| `identify(user)` | Set or clear `{ id, hash }` after login |
| `fetchConfig()` | Presentation config JSON from the server |
| `listConversations()` | History for a signed visitor |
| `getConversation(id)` | One transcript |
| `keepConversation(id, kept?)` | Exempt from expiry (`kept: false` releases) |
| `deleteConversation(id)` | Delete one conversation |
| `deleteConversations()` | Delete all for this visitor |

When answer credits run out, chat returns **402** with free search hits in `data`. The official chat widget renders those results; a headless client should do the same from the 402 body (no need to call search separately).

## Signed visitor identity

To restore chat history for a logged-in user, or to list/keep/delete conversations, prove the user id on **your** server with the widget's **identity signing secret** (dashboard → widget → For developers):

```php
$hash = hash_hmac('sha256', (string) $userId, $signingSecret);
```

Pass the result into the SDK (never ship the signing secret to the browser):

```js
// At init (preferred so every message is tagged)
const chat = Datalumo.chat('ORG_ID/WIDGET_ID', {
  user: { id: String(userId), hash },
}).mount();

// Or after login
const headless = Datalumo.headless('ORG_ID/WIDGET_ID');
headless.identify({ id: String(userId), hash });
await headless.listConversations();
```

A bad hash is rejected; Datalumo will not treat a failed proof as an anonymous visitor.

See [Authentication](/docs/authentication) and [Conversations](/docs/conversations).

## Analytics beacon (server-rendered results)

If you search on your server and render your own result list, load the lightweight analytics script so clicks still join the same funnel. Reuse the `session_id` your server received from search.

```html
<script
  src="https://datalumo.app/widget/v1/datalumo-analytics.js"
  data-widget="ORG_ID/WIDGET_ID"
  data-session="SESSION_FROM_SERVER"
></script>

<a href="https://example.com/docs/refunds"
   data-dl-click
   data-dl-rank="0"
   data-dl-source="result">
  Refunds
</a>
```

| Attribute | Required | Description |
|---|---|---|
| `data-widget` | yes | Widget key `{organisation}/{widget}` |
| `data-session` | no | Session id from the search response; omit to mint a new tab session |
| `data-base` | no | API origin override (defaults to the script origin) |
| `data-dl-click` | on links | Marks a result link for auto click tracking |
| `data-dl-url` | no | Override URL (defaults to `href`) |
| `data-dl-rank` | no | Result position |
| `data-dl-source` | no | e.g. `result`, `summary` |

Or initialise manually:

```js
const analytics = DatalumoAnalytics.init({
  key: 'ORG_ID/WIDGET_ID',
  sessionId: '…',
});
analytics.trackClick({ url: 'https://…', rank: 0, source: 'result' });
```

Your domain must still be on the widget's website list.

## Presentation config

On mount the SDK requests:

```
GET /api/v1/{organisation}/widgets/{widget}/config
```

Typical payload:

```json
{
  "type": "chatbot",
  "name": "Help",
  "variant": "bubble",
  "accent_light": "#18181b",
  "accent_dark": "#fafafa",
  "theme": "auto",
  "branding": true,
  "summary": false,
  "speech_input": false,
  "placeholder": "",
  "greeting": "",
  "prompts": ["How does pricing work?"],
  "avatar": null,
  "debounce": 1000,
  "default_language": "en"
}
```

Responses may be cached for up to five minutes. `branding` follows your plan; clients cannot turn it off by override.

Most appearance and behaviour options are edited in the dashboard (shape, colours, greeting, suggested questions, AI summary, voice input, default language). Use SDK overrides for per-page differences (target, trigger, prompts, one-off theme).

## Languages

UI chrome (placeholders, buttons, status text) follows the visitor's browser language when supported, otherwise the widget's `default_language`, otherwise English.

Built-in locales include: `en`, `nl`, `de`, `fr`, `es`, `it`, `pt-BR`, `zh-Hans`, `zh-Hant`, `zh-HK`, `ja`, `ko`, `th`.

## Troubleshooting

| Symptom | Check |
|---|---|
| Widget does not load / config fails | Domain is on the widget's website list; key is `{org}/{widget}` public ids |
| Console: no element matches `target` / `trigger` | Selector exists when `.mount()` runs |
| Chat says wrong widget type | Use a chatbot widget with `Datalumo.chat`, not a search widget |
| Conversation history empty | Pass signed `user.id` + `user.hash`; signing secret must match |
| No analytics in the dashboard | Domain allowlist; wait for first search or chat; for custom results use the analytics beacon |
| Branding still shows | Controlled by plan, not by client override |

## Related

- [Widget API overview](/docs/widgets) — HTTP endpoints the SDK uses
- [Search](/docs/search) · [Chat](/docs/chat) · [Summarize](/docs/summarize) · [Events](/docs/events)
- [Authentication](/docs/authentication) — domains, secret keys, signed identity
- [Errors & limits](/docs/errors) — status codes and rate limits
