JavaScript Chat Widget
Embed Deskwoot live chat on any website with a single <script> tag. No build step, no dashboard login required from visitors.
On this page
Install
Paste this snippet into your page template, ideally just before the closing </body> tag. It loads the widget asynchronously, so it won't block rendering.
<script>
window.deskwootSettings = {
widgetToken: "YOUR_WIDGET_TOKEN"
};
(function (d, t) {
var g = d.createElement(t);
var s = d.getElementsByTagName(t)[0];
g.src = "https://deskwoot.com/widget/deskwoot-loader.js";
g.async = true;
g.defer = true;
s.parentNode.insertBefore(g, s);
})(document, "script");
</script>That's it. The chat bubble appears on your page right away. No npm package, no bundler, no build step: it works in Next.js, React, Vue, Shopify themes, WordPress, plain HTML and anywhere else JavaScript runs.
Find your widget token
- Log into the Deskwoot dashboard with an admin account. Your visitors never need a login.
- Go to Settings → Inboxes.
- Create a new inbox of type Live Chat, or open your existing one.
- On the Widget tab, the Widget Installation card shows the snippet with your own
widgetToken. Copy it as it is.
widgetToken = one inbox. Conversations from that widget always land in that inbox, where your agents pick them up. To route to a different team or set of agents, create a second inbox and use its token on a different page.Configuration
All options live on window.deskwootSettings. Only widgetToken is required.
| Option | Type | Default | Description |
|---|---|---|---|
| widgetToken | string | none | Required. Inbox identifier from the dashboard. |
| user | object | none | Signed identity of a logged in visitor. See Identity verification. |
| embedded | boolean | false | Fills a container or the whole screen instead of floating in a corner. See Embedded mode. |
| container | string | Element | none | Only with embedded: the CSS selector or element to fill. Without it, the widget fills the whole viewport. |
| openAnchor | string | Element | { x, y } | none | Element the panel grows out of on desktop: a CSS selector, an element or an { x, y } point. |
| openAlign | string | "auto" | Which panel corner sits on the anchor: auto, bottom-left, bottom-right, top-left or top-right. |
window.deskwootSettings = {
widgetToken: "YOUR_WIDGET_TOKEN",
// Optional: a verified, logged in visitor (see Identity verification)
user: {
identifier: "user_4711",
identifierHash: "<HMAC-SHA256 of identifier, from your server>",
email: "jane@example.com",
name: "Jane Doe"
},
// Optional: open the panel out of your own button on desktop
openAnchor: "#support-button",
openAlign: "auto"
};Identity verification
When visitors are logged in on your site, tell the widget who they are. Their conversations then arrive with name, email and history instead of as an anonymous visitor.
The widget only accepts an identity that your server has signed. You sign the user id with a secret that belongs to the inbox. Verified visitors chat under their real name and email and skip the pre chat form and the email step. The secret stays on your server, so Deskwoot confirms the visitor from the signature without any access to your systems.
Find your signing secret in your website inbox settings, on the Pre Chat Form tab, in the Identity Verification card.
import crypto from "crypto";
const identifierHash = crypto
.createHmac("sha256", process.env.DESKWOOT_IDENTITY_SECRET)
.update(userId) // your stable user id, as a string
.digest("hex");window.deskwootSettings = {
widgetToken: "YOUR_WIDGET_TOKEN",
user: {
identifier: "user_4711", // your stable user id
identifierHash: "<HMAC-SHA256 of identifier, computed on your server>",
email: "jane@example.com", // optional
name: "Jane Doe", // optional
attributes: { plan: "Pro" }, // optional, shown on the contact
attributesIssuedAt: 1760781600, // optional: unix seconds you signed them at
attributesHash: "<HMAC-SHA256 of the signed attributes, from your server>" // optional
}
};identifier is your stable user id and the only field that is signed. email, name and attributes (a flat map of strings, such as a plan or a company id, shown on the contact) are optional details. A verified visitor lands on the same contact every time, with past conversations, notes and attributes in view. The AI bot answers this visitor in the live chat only from signed attributes (see below), and only when its Data Access includes contacts.
If your app signs the user in after the page has loaded, call window.deskwoot.identify(user) once you have the signed user. It updates the current visitor without a reload. If the user is already signed in when the page loads, set deskwootSettings.user before the widget script instead: the chat then stays open across reloads. With identify() after the load, each reload starts with an empty chat that returns as soon as the user is verified.
Signed attributes
identifierHash signs the user id only, so a visitor can change attributes in the browser. To let the AI bot answer from them, sign them on your server and send attributesHash and attributesIssuedAt with them. Unsigned attributes, and attributes written through the API or by your team, still show on the contact, but the bot never reads them. The bot may tell the visitor what a signed attribute says, so only sign what the visitor may see.
attributesHash is the lowercase hex HMAC-SHA256, with the same inbox secret, of four lines joined by a line break (\n): the text deskwoot-attributes-v1, the user id, attributesIssuedAt and the canonical JSON of the attributes. Canonical JSON: keys sorted by Unicode code point, no spaces, characters outside ASCII written as they are (not escaped), and only strings, numbers, true, false and null as values (no nested objects or lists). That is exactly what Python's json.dumps(attributes, sort_keys=True, separators=(",", ":"), ensure_ascii=False) writes.
Send decimal numbers and very large integers (above 9007199254740991) as strings. Python writes 1.0 where JavaScript writes 1, so such numbers would not give the same hash on both sides.
attributesIssuedAt is the unix time in whole seconds when you signed the set. Deskwoot uses a set up to 24 hours old and up to 5 minutes ahead of its clock, and never one older than the last set it used for that user, so sign again on every page load. Outside that, the visitor is still verified, but the attributes of that request are ignored (nothing on the contact changes) and the verification answer says attributesIgnored with out_of_range or stale. A wrong hash is refused like a wrong identifierHash, and the visitor then chats anonymously. So is a signed set over the limits: at most 30 keys, keys up to 64 characters, values up to 1024 characters, and no key Deskwoot reserves for itself, such as source.
Each signed set replaces what the bot may read for that user: a key you leave out stays on the contact, but the bot no longer reads it, and a key you sign as null is removed from the contact.
import crypto from "crypto";
// Keys sorted by Unicode code point, no spaces, non ASCII characters raw.
// Values: strings, integers, true, false or null.
function canonicalJson(attributes) {
const keys = Object.keys(attributes).sort((a, b) => {
const x = Array.from(a), y = Array.from(b);
for (let i = 0; i < Math.min(x.length, y.length); i++) {
const d = x[i].codePointAt(0) - y[i].codePointAt(0);
if (d !== 0) return d;
}
return x.length - y.length;
});
return "{" + keys.map((k) => JSON.stringify(k) + ":" + JSON.stringify(attributes[k])).join(",") + "}";
}
const attributesIssuedAt = Math.floor(Date.now() / 1000);
const message = "deskwoot-attributes-v1\n" + userId + "\n" + attributesIssuedAt + "\n" + canonicalJson(attributes);
const attributesHash = crypto
.createHmac("sha256", process.env.DESKWOOT_IDENTITY_SECRET)
.update(message)
.digest("hex");import hashlib, hmac, json, os, time
attributes_issued_at = int(time.time())
canonical = json.dumps(attributes, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
message = "deskwoot-attributes-v1\n" + user_id + "\n" + str(attributes_issued_at) + "\n" + canonical
attributes_hash = hmac.new(
os.environ["DESKWOOT_IDENTITY_SECRET"].encode(),
message.encode("utf-8"),
hashlib.sha256,
).hexdigest()secret test_identity_secret
identifier user_4711
attributesIssuedAt 1760781600
attributes { "plan": "Pro", "status": "verified", "trades": 12, "vip": true,
"referrer": null, "city": "Zürich", "Zone": "EU" }
canonical JSON {"Zone":"EU","city":"Zürich","plan":"Pro","referrer":null,"status":"verified","trades":12,"vip":true}
signed message (4 lines, joined by \n, no line break at the end):
deskwoot-attributes-v1
user_4711
1760781600
{"Zone":"EU","city":"Zürich","plan":"Pro","referrer":null,"status":"verified","trades":12,"vip":true}
attributesHash bd363335977152a3b73cfb6c042c9afc823f18eab73faff36acddfffd0b1e1f5JavaScript API
As soon as the loader has run, window.deskwoot (also available as window.deskwootWidget) lets you drive the widget from your own interface, for example a Contact us link in your navigation that opens the chat instead of a support page.
| Method | What it does |
|---|---|
| open(options?) | Opens the chat panel. The optional anchor, align and offset let it grow out of your own element on desktop. |
| close() | Closes the panel. |
| toggle(options?) | Opens the panel, or closes it when it is already open. Takes the same options as open(). |
| identify(user) | Signs in a verified user after the page has loaded. See Identity verification. |
| window.__deskwootUnread | Number of unread agent messages for this visitor. It updates while the panel is closed. |
Calls made while the widget is still starting wait in a queue and run once it is ready, so there is no need to wait or poll.
// window.deskwoot (alias window.deskwootWidget) exists as soon as the
// loader script has run. Calls made while the widget is still starting
// are queued, so there is no need to wait or poll. Only code that runs
// before the loader itself needs the guard below.
function openDeskwoot() {
if (window.deskwoot) {
window.deskwoot.open();
}
}
openDeskwoot(); // open the chat panel
// window.deskwoot.close(); // close it again
// window.deskwoot.toggle(); // open or close
// Unread agent messages for this visitor:
console.log(window.__deskwootUnread);Embedded mode
Set embedded: true when the widget lives inside a container that should own the full screen, typically a mobile app WebView, a full page Contact screen, or a modal that brings its own frame.
In embedded mode the launcher bubble, trigger popup and close button are hidden, and the panel fills position:fixed; inset:0 with no shadow, no rounded corners and no 20px offset. Point container at an element to fill only that element. The Deskwoot mobile app uses this mode on its Contact Support screen.
<div id="support-pane" style="height: 600px"></div>
<script>
window.deskwootSettings = {
widgetToken: "YOUR_WIDGET_TOKEN",
embedded: true,
container: "#support-pane" // leave out to fill the whole viewport
};
</script>Opens automatically and fills its container.
In embedded mode the panel opens on load and fills its container. The launcher bubble and close button are hidden, so your app or page owns navigation. The JavaScript API is still available to open, close, or toggle it:
<button onclick="window.deskwoot.open()">Contact Support</button>Recommended placements
Three placements that work well for support inside an app:
Sidebar or bottom navigation entry (how Deskwoot does it)
Place a Contact Support item in your existing navigation, next to Settings or Help. It looks like any other link but opens the chat instead of a new page.
<a href="#" onclick="window.deskwoot.open(); return false;" class="nav-item">
<svg class="icon"><!-- chat bubble icon --></svg>
Contact Support
</a>Help menu or dropdown entry
If you already have a Help or Account menu, add a Chat with support row at the top. It sits well next to items like Docs, Keyboard shortcuts or Feedback.
<div role="menuitem" onclick="window.deskwoot.open()">
Chat with support
<span class="kbd">?</span>
</div>Floating button in the corner
A small floating button that stands in for the hidden bubble. Useful when your app has no navigation bar that is always visible.
<button
id="support-fab"
onclick="window.deskwoot.open({ anchor: this })"
style="position:fixed;bottom:20px;right:20px;border:0;border-radius:9999px;padding:12px 18px;cursor:pointer;font-weight:600"
>
Help
</button>
<!--
open() and toggle() take an optional anchor, so the panel grows out of
your own element instead of the default corner:
open({ anchor: this }) // grow from this button
open({ anchor: "#support-fab" }) // or from a CSS selector
open({ anchor: { x: 120, y: 640 } }) // or from a viewport point
anchor CSS selector, DOM element or { x, y } (null = default corner)
align "auto" (default), "bottom-left", "bottom-right",
"top-left" or "top-right": the corner it grows from
offset { x, y } in px, to fine tune the position
window.deskwootSettings.openAnchor and .openAlign make this the
default for every open(); open({ anchor: null }) opens one time in
the default corner. Desktop only; phones always get full screen.
-->Tips
- Call
deskwoot.toggle()instead ofopen()if you want the same trigger to close the panel when it's already open. - Show an unread badge on your trigger with
window.__deskwootUnread, a number that updates while the panel is closed. - Put the trigger in the same spot on every page behind your login, so users always know where support is.
For the standard website install you do not need a trigger of your own: deskwoot-loader.js draws the bubble and handles the click.
Content Security Policy
If your site sends a Content Security Policy header, allow Deskwoot's origin. A typical minimal policy:
script-src 'unsafe-inline' https://deskwoot.com;
connect-src https://deskwoot.com;
frame-src https://deskwoot.com;
style-src 'unsafe-inline';The install snippet is an inline script, so script-src needs 'unsafe-inline', a nonce or a hash. style-src 'unsafe-inline' is needed because the widget adds a small stylesheet to your page, and frame-src because the chat runs in frames served from deskwoot.com. Questions about your policy? Write to hello@deskwoot.com.
Frequently asked questions
The short answers are here. For anything else, ask Fynn below.
Do my visitors need a Deskwoot account to chat?
No. Only you and your team log into the dashboard to answer. Visitors just type into the widget.
Does the widget work in SPAs that switch routes without a reload?
Yes. Load the script once when your app starts (for example in _app.tsx or root.tsx) and it stays through navigation. Nothing needs to run again when the route changes.