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

# JavaScript API

> Every method on window.JunDesk, the snippet's attributes, and the events the loader fires on your page.

The loader (`widget.js`) puts one object on your page, `window.JunDesk`. This page lists everything on it. The loader is MIT-licensed and readable in the repo at `public/widget.js`.

## Waiting for the loader

With the [recommended snippet](/widget/install.md) (`defer`, in `<head>`, before your scripts), `window.JunDesk` exists by the time your own deferred scripts run. If your code can run earlier, or the snippet loads some other way, wait for the `jundesk:ready` event:

```js theme={null}
function withJunDesk(fn) {
  if (window.JunDesk) fn(window.JunDesk);
  else window.addEventListener("jundesk:ready", () => fn(window.JunDesk), { once: true });
}

withJunDesk((jun) => jun.identify(userToken));
```

For one-off calls, optional chaining is enough: `window.JunDesk?.open()`. Every method is safe to call at any time; none of them throws into your page.

## Script attributes

| Attribute | Value | What it does |
| - | - | - |
| `src` | `https://<your-desk>/widget.js` | The loader. Your desk's address is taken from it. |
| `data-key` | `wk_…` | Required. Your widget key, from **Chat widget → Install**. |
| `data-user-token` | A JWT | Identifies the signed-in customer on load. Same as calling `identify()`. See [Identify users](/widget/identify-users.md). |
| `data-consent` | `required` | Stores nothing and stays off the visitor list until `consent(true)`. See [Consent and privacy](/widget/consent-and-privacy.md). |
| `data-capture` | `off` | Turns off error, request, rage-click and stuck capture, `reportError()` and `track()` (and the nudges that depend on them). See [Debug context](/widget/debug-context.md). |
| `data-color` | `#rrggbb` | Overrides the desk's brand colour on this site. |
| `data-theme` | `light`, `dark` or `auto` | The theme on this site from the first paint, over the desk's. Change it later with `setTheme()`. |
| `data-page-control` | `off` | Leaves this site out of page control (turned on for all sites on the desk). See [Page control](/widget/page-control.md). |

## Methods

### `JunDesk.open(options?)`

Opens the chat. With an `intent`, opens a purpose-built chat defined by a task ([Intents](/widget/intents.md)).

**options.intent** `string`

The intent's name: lowercase letters, digits, `_` and `-`, up to 40 characters. Anything else opens a plain chat.

**options.onExit** `function`

Called once when the visitor clicks the intent's exit button, after the chat closes. Required when the intent has an `exit`; without it, a plain chat opens and a console warning explains why.

```js theme={null}
JunDesk.open();
JunDesk.open({ intent: "cancel", onExit: () => { location.href = "/billing/cancel"; } });
```

### `JunDesk.close()`

Closes the chat. With the island, folds it back to its resting pill.

### `JunDesk.toggle()`

Opens the chat if it's closed, closes it if it's open.

### `JunDesk.identify(token)`

Tells the desk who the signed-in customer is.

**token** `string | object | null` *required*

A short-lived HS256 JWT your backend signed with the desk's identity secret, or an object `{ id, name, email, token? }`. Without `token`, the object only labels the visitor: agents see the id, name and email as not verified (browsers sharing the email or id show as one person on the desk), with none of what a verified token unlocks ([Without a token](/widget/identify-users.md#without-a-token)). Passing `null` forgets the identity in the chat; use `logout()` on sign-out instead.

The token goes to the desk over the loader's live connection and from the chat frame. If it's rejected, the frame logs `[Jun] identify failed: <reason>` in the console and the visitor stays anonymous.

### `JunDesk.logout()`

Call it when the customer signs out of your app. Forgets this browser's chat identity (the next person starts fresh), drops the customer's user secrets on the desk, and starts a new visitor session. Takes no arguments.

### `JunDesk.consent(granted)`

**granted** `boolean` *required*

`true` when the visitor accepts, `false` when they decline or withdraw.

Only matters with `data-consent="required"`. `true` lets the widget store its session and visitor token and connect to the visitor list. `false` disconnects and deletes what the widget stored: the session (`jun:session`) and debug events (`jun:e`) on your page, and what the chat frame stored. Consent isn't remembered across page loads: call it on every page where your consent tool says yes.

### `JunDesk.setTheme(theme)`

Makes the widget follow your site's own light / dark switch, instead of the theme set on the desk.

**theme** `"light" | "dark" | "auto" | null` *required*

`"light"` or `"dark"` forces one. `"auto"` follows the visitor's system setting. `null` (or anything else) goes back to the desk's **Theme**.

The button, the greeting card, the island and the open chat switch at once, with no reload. It lasts until the page unloads, so call it on each page load, or put `data-theme` on the tag for the first paint:

```js theme={null}
// Wherever your app switches its theme:
function applyTheme(dark) {
  document.documentElement.classList.toggle("dark", dark);
  window.JunDesk?.setTheme(dark ? "dark" : "light");
}
```

With the island on **Dark** (its default), the island stays near-black whatever the theme; the theme changes the island only on **Match theme**.

### `JunDesk.reportError(error)`

Adds your app's own description of a failure to the debug timeline, where the AI and your agents see it. It can also trigger a "Want a hand?" nudge.

**error.message** `string` *required*

What went wrong, in your app's words. Masked and cut to 300 characters.

**error.code** `string`

An optional code for your team, like `import.row_invalid`. Letters, digits, `_`, `.` and `-`, up to 60 characters. Agents and issue drafts see it; the AI answering visitors doesn't.

```js theme={null}
window.JunDesk?.reportError({ message: "Row 42: missing email", code: "import.row_invalid" });
```

Does nothing with `data-capture="off"`, before consent, or without a non-empty `message`.

### `JunDesk.track(name, props?)`

Records something the visitor did in your app, like starting an export or upgrading their plan. It joins the same debug timeline as errors, so when they write "where's my CSV?", the AI and your agents can see they started an export a minute ago.

**name** `string` *required*

What happened, like `export_started`. Letters, digits, spaces and `_ . : / -`, up to 60 characters.

**props** `object`

Up to 10 details. Keys use letters, digits, `_`, `.` and `-`, up to 40 characters. Values are strings (masked like everything else and cut to 100 characters), finite numbers or booleans. Anything else is dropped.

```js theme={null}
window.JunDesk?.track("export_started", { format: "csv" });
```

* The inbox timeline shows the name with its details. The AI sees a line like `In the app: export_started (format: csv)`.
* It isn't a problem: no ⚠ count, and it never triggers a "Want a hand?" nudge.
* Like other events, it stays in the tab and goes to the desk only with a message the visitor writes. It's masked again on the desk.
* Use it for the few actions that explain support questions (export started, plan upgraded, import finished), not as analytics. Don't put personal data in `props`.

Does nothing with `data-capture="off"` or before consent, and never throws.

### `JunDesk.registerAction(action)`

Registers a page action the AI can run for the visitor: a [WebMCP](https://developer.chrome.com/docs/ai/webmcp/imperative-api) tool plus Jun's extras. Returns a function that removes it.

```js theme={null}
const remove = JunDesk.registerAction({
  name: "export_csv",
  description: "Download this month's usage as a CSV file",
  risk: "auto",
  execute: () => { startExport(); return "usage-2026-10.csv (18 rows)"; },
});
// Later, e.g. when the component unmounts:
remove();
```

**action.name** `string` *required*

The action's name, up to 64 characters.

**action.description** `string` *required*

What it does, in one line.

**action.execute** `(input, signal) =\> any` *required*

Runs it in the visitor's browser.

**action.inputSchema** `object`

JSON Schema for the inputs.

**action.annotations** `object`

WebMCP hints (`readOnlyHint`, `consequentialHint`).

**action.pages** `string[]`

Paths it's offered on (`"/pricing"`, `"/plans/*"`).

**action.key** `string | number`

One instance per item on screen.

**action.element** `Element | RefObject`

Highlighted while it runs.

**action.context** `object | function`

Small facts for the AI.

**action.available** `() =\> boolean`

Checked before it's offered and before it runs.

**action.undo** `(result) =\> any`

Adds an Undo button.

**action.risk** `"auto" | "confirm" | "human"` (default: confirm)

How it runs.

**action.status** `string`

The line shown while it runs.

**action.widget** `string`

A desk widget to show the result as.

Without `name`, `description` or an `execute` function, it logs `Jun: registerAction needs name, description and execute` and isn't registered. Registering the same `name` (and `key`) again replaces the older one, with a console warning. [Page actions](/widget/page-actions.md) explains every field, risk levels and multi-step runs.

### `JunDesk.onWidgetAction(handler)`

Listens for card buttons meant for your page. A [widget](/ai/widgets.md) card's action with ChatKit's `handler: "client"` never goes to the desk: it comes here. Returns a function that removes the listener.

**handler** `(action) =\> void` *required*

Called with the action:

**type** `string`

The action's `type`, as written in the widget.

**payload** `any`

The action's `payload`, if it has one.

**values** `object`

The values of the card's form fields when it was pressed.

**widget** `string`

The widget's name.

```js theme={null}
const stop = JunDesk.onWidgetAction((action) => {
  if (action.type === "open_invoice") router.push(`/invoices/${action.payload.id}`);
});
```

## Events

The loader fires these on `window`.

| Event | Type | When | `detail` |
| - | - | - | - |
| `jundesk:ready` | `Event` | Once, right after `window.JunDesk` is defined | None |
| `jundesk:widget-action` | `CustomEvent` | A card action with `handler: "client"` was pressed | The same object `onWidgetAction` handlers get |

`onWidgetAction` is a thin wrapper around `jundesk:widget-action`, so either works:

```js theme={null}
window.addEventListener("jundesk:widget-action", (e) => console.log(e.detail.type, e.detail.values));
```

> [!NOTE]
>
> `jundesk:ready` fires during the loader's own run. A listener added after that misses it, which is why the pattern above checks `window.JunDesk` first.

## `document.modelContext`

If the browser has no native WebMCP, the loader defines `document.modelContext` before your scripts run, so you can register tools in plain WebMCP:

| Member | What it does |
| - | - |
| `registerTool(tool, { signal })` | Registers a tool. Throws if the name is taken or `name`/`description` is missing. Aborting `signal` removes it. |
| `getTools()` | A promise of the registered tools. |
| `executeTool(tool, input)` | Runs a tool by object or name. |
| `addEventListener("toolchange", …)` | Fires when tools are added or removed. |
| `polyfill` | `true` on the loader's stand-in. |

In browsers with native WebMCP, the loader leaves `document.modelContext` alone and reads from it. [Page actions](/widget/page-actions.md) has the details.

## Keyboard

* With the island, **`/`** anywhere on the page opens the chat, unless the visitor is typing in a field.
* **Esc** closes the open chat window, and folds the island back.

## Internal members

`JunDesk._q` and `JunDesk._a` are the loader's own plumbing for queued and registered page actions. Don't rely on them. The `postMessage` messages between the loader and the chat frame (`jun:*`) aren't a public API either: they can change in any release.

> **[Install](/widget/install.md)**
>
> The snippet and where it goes.

> **[Widget options reference](/reference/widget-options.md)**
>
> Every setting in one table.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.