Skip to main content
The widget is one <script> tag. It loads a small loader (widget.js) from your desk, draws the button or island, and loads the chat itself only when it’s needed. Everything about its look comes from the desk, so you paste the snippet once and change colours, wording and mode later without touching your site.

Copy the snippet

1

Open the Install tab

In the desk, go to Chat widget → Install (/appearance/install). The Install the chat widget card shows the snippet with your desk’s address and your widget key filled in. Press Copy, or Send to a developer to copy the snippet with a few plain lines on where it goes and how to check it.Until the widget is on a site, the sidebar’s Chat widget entry has a small dot and opens on this tab, and the page header says Not on your site yet. The Get started checklist’s Put it on your site step shows the same snippet.
2

Paste it into your site's head

Put it inside <head>, before your own scripts:
data-key is your widget key. It is public by design: anyone can read it from your page source. Use Allowed websites to stop other sites reusing it.
3

Deploy your site and open a page

On a site builder or framework, Where to paste it on the same tab says where the head code goes in Webflow, WordPress, Shopify and Next.js.The widget appears within a second or so (the loader waits up to 1.5 seconds for your colour and mode settings, so it never flashes in the default colour).

Why defer, in <head>, before your scripts

The loader is written to run early, and the placement matters for two features:
  • Debug context. The loader listens for JavaScript errors and failed requests from the moment it runs. In <head> it sees problems from the start of the page load. See Debug context.
  • Page actions. If the browser has no native WebMCP, the loader installs a stand-in document.modelContext so your code can register actions. See Page actions.
defer doesn’t hold up the page, and deferred scripts (module bundles too) run in document order. So when the snippet comes before your app’s bundle, the loader has already run when your code starts, and document.modelContext exists when your code checks for it.
Don’t use async, and don’t put the tag after your bundle. Many sites feature-detect document.modelContext once on mount. With async (or a later tag), that check can run before the loader, find nothing, and register no actions. The chat still works; the AI just can’t do anything on your page.

Snippet attributes

All attributes go on the same <script> tag. The full list with every JunDesk.* method is in the JavaScript API.

Frameworks

The rule is the same everywhere: the tag must end up in the document’s <head>, with defer, ahead of your app’s own scripts. Add it to the HTML your server sends, not from JavaScript after the page has loaded.
index.html
The loader runs only once per page: if window.JunDesk already exists, a second copy does nothing. Single-page apps don’t need to re-add it on route changes. It follows history.pushState, replaceState and popstate itself.
For React, useJunAction from packages/widget-react registers page actions while a component is mounted and waits for the loader if needed. See Page actions.

Check that it works

1

Try the demo page first

Chat widget → Install has an Open demo page button. It opens /demo.html?key=wk_… on your desk: a fake customer site with buttons that fail on purpose (a 500 on “Pay invoice”, a JavaScript error on “Show usage chart”, a dead “Export CSV”), a cancel intent and three page actions. Use it to see the whole loop before touching your own site.
2

Send a message from your site

Open your site, open the chat, and write something. Open the desk’s Inbox in another window. The message arrives live, with a Browser section in the side panel showing the page and recent events.
3

Look for yourself on the visitor list

Visitors shows everyone on your site right now. Your own tab should be there (unless you use data-consent="required" and haven’t consented yet).

Install detection

The first time the loader connects from a site other than the desk itself, the desk notes that site. The Install tab’s Waiting for your site… line turns into Seen on with that site (it checks every few seconds while the tab is open), the header on every tab of Chat widget shows Live on that site, the sidebar dot goes away, and the Put it on your site step in Get started ticks itself. The demo page doesn’t count, because it runs on the desk’s own origin. The check rides on the loader’s live connection. With data-consent="required", nothing connects until a visitor consents, so the step may not tick on its own. Press I’ve added it (on the Install tab or in Get started) to mark it by hand. If nothing appears:
  • Open the browser console. Jun: no data-key means the attribute is missing.
  • Check the network tab for widget.js and /api/widget/<key>/config. A 404 on config means the key doesn’t match this desk.
  • If you set Allowed websites, check your site’s hostname is on the list (see below).

Limit which websites can use the widget

Because the key is public, anyone could copy your snippet onto their own site. Under Chat widget → Behaviour → Allowed websites, list your domains, separated by commas or spaces:
  • *.acme.com covers every subdomain and acme.com itself.
  • You can paste full URLs; the desk keeps only the hostname.
  • Up to 50 entries. An empty list means any website, which is handy while you try things out.
  • The desk’s own address always works, so the demo page and the Appearance preview keep working.
With a list in place, the chat frame refuses to load on other sites (through the frame’s frame-ancestors policy), the live visitor connection is refused, and proactive nudges aren’t offered there.

Next steps

Pick a mode

The island, or a chat window with or without a greeting card.

Set the look

Colour, logos, greeting and suggested questions.

Identify signed-in users

Verified customers, chats across devices.

Page actions

Let the AI do things on your page.