Installing the widget on your site
One tag with the project ID in its URL puts the chat on any page. Optional data attributes and a JS API for identification and control are below.
Installation in 4 steps
- 1Get the codeIn the dashboard, open SettingsWidget Builder: the Embed snippet button at the bottom of the left panel opens ready-made code with your project ID. The same code is also in SettingsChannels, on the Web Widget card. The project ID is the UUID in the
?ws=parameter. - 2Add the code before </body>On every page that should have the chat:By default the code connects the widget in anonymous mode: no backend needed on your side. If you want to recognize a visitor across devices (an iPhone and a MacBook → one contact), uncomment theindex.htmlhtml
<script> (function(w, d){ function loadWidget(token){ var s = d.createElement('script'); s.async = 1; s.src = 'https://support.forestsnet.com/widget-bundle?ws=00000000-0000-0000-0000-000000000000'; if (token) s.setAttribute('data-visitor-token', token); d.head.appendChild(s); } // ── Optional: cross-device HMAC visitor token ───────────────── // Uncomment + implement /api/supporthub/token on your backend to // bind the same visitor across devices. Recipes per stack: // https://support.forestsnet.com/docs/widget/hmac-examples // // fetch('/api/supporthub/token', { credentials: 'include' }) // .then(function(r){ return r.ok ? r.json() : null; }) // .then(function(data){ loadWidget(data && data.token); }) // .catch(function(){ loadWidget(); }); // // Default — anonymous (no token): loadWidget(); })(window, document); </script>fetch('/api/supporthub/token')block and implement the endpoint on your side using one of the templates; the code passes the token to thedata-visitor-tokenattribute by itself. The script is added viadocument.createElement('script')withasync=1and doesn’t block page rendering.?ws=is the only required parameter; the widget already knows the API address. Full HMAC reference: /docs/widget/hmac; the whole identification flow: /docs/widget/identify. - 3Check that the button appearsOpen your site in an incognito window. The widget button should appear in the bottom right corner, by default with a chat icon and a label. If it’s missing or something doesn’t work, check the browser console. Common causes:
- A typo in
?ws=: the button still appears, but with the default look, and messages won’t get through. - The site’s CSP: it needs
support.forestsnet.cominscript-src,https://api.support.forestsnet.comandwss://api.support.forestsnet.cominconnect-src, andapi.support.forestsnet.comanddata:inimg-src. The widget adds its styles with a<style>tag, sostyle-srchas to allow inline styles. - An ad blocker or an extension that blocks third-party scripts.
- A typo in
- 4Open the chat and send a test messageClick the button → the panel opens → press “Send us a message” → send “hello”. A new conversation shows up right away in the dashboard’s Inbox, which means the code is connected correctly.
What the tag does
On load, /widget-bundle?ws=…:
- Reads
?ws=from the script URL and the optionaldata-*attributes on the tag. The server usually puts the published config and the API address right at the start of the bundle, so there’s no separate request for/config; if that didn’t work, the widget requests the config itself. - Applies the theme, colors and custom CSS, and adds the button and the panel to the page.
- Finds or creates the visitor ID, which is stored in five places (a cookie, localStorage, sessionStorage, IndexedDB and an older shared key); details are in the developer reference.
- If
data-email/name/phone/telegram-idare set, sendsPOST /api/webhooks/widget/{ws}/identify: the contact is created or updated right away, even before the first message. - Once the visitor has a conversation, connects a WebSocket to
/api/ws/widget/{visitor_id}?workspace_id={ws}for real-time replies.
Tag parameters
SupportHub.init().NEXT_PUBLIC_API_URL..example.com), so that a visitor keeps one conversation across subdomains. You can set the same thing in Widget BuilderAdvancedCookie domain (optional); the attribute on the tag takes precedence over the config.ru or en (tags like en-GB work too; the first two letters are used). Useful when your site has several language versions: give each one its own data-locale. A language set this way always applies. Without the attribute, the widget takes the page’s <html lang="…">, then the browser language, each only if the project’s texts exist in that language, and otherwise the project’s main language. Unsupported values (fr, xx) are skipped. If Widget BuilderBrandWidget language is set to Always Russian or Always English, the attribute has no effect (more on the Brand and header page). The programmatic equivalent is SupportHub.init({ locale: "en" }).identify along with the other data-* values. The step-by-step pre-chat form (the default style) doesn’t ask for fields it already knows."true" hides the launcher button. The widget then opens only programmatically: call window.SupportHub.open() from your own button or link. It applies to the automatic start via ?ws=; with manual initialization, use SupportHub.init({ hideLauncher: true }).| Field | Type | Default | Description |
|---|---|---|---|
| ?ws= (query) | uuid | — | Required. The project UUID in the script URL. Without it, the widget doesn’t start by itself, only via SupportHub.init(). |
| data-api-base | string | — | Your own API address. Usually not needed and not applied: the server that serves the bundle writes the API address at its start itself, and the attribute is then ignored. It only matters if the bundle is served without that, for example from your own build without NEXT_PUBLIC_API_URL. |
| data-cookie-domain | string | — | The domain for the visitor-ID cookie, with a leading dot (.example.com), so that a visitor keeps one conversation across subdomains. You can set the same thing in Widget BuilderAdvancedCookie domain (optional); the attribute on the tag takes precedence over the config. |
| data-locale | string (BCP-47) | — | The widget language: ru or en (tags like en-GB work too; the first two letters are used). Useful when your site has several language versions: give each one its own data-locale. A language set this way always applies. Without the attribute, the widget takes the page’s <html lang="…">, then the browser language, each only if the project’s texts exist in that language, and otherwise the project’s main language. Unsupported values (fr, xx) are skipped. If Widget BuilderBrandWidget language is set to Always Russian or Always English, the attribute has no effect (more on the Brand and header page). The programmatic equivalent is SupportHub.init({ locale: "en" }). |
| data-email | string | — | The visitor’s email. It’s sent to identify along with the other data-* values. The step-by-step pre-chat form (the default style) doesn’t ask for fields it already knows. |
| data-name | string | — | The visitor’s name. It only replaces a placeholder name such as “Guest”. |
| data-phone | string | — | Phone number (E.164 recommended). Saved if the contact doesn’t have a phone yet. |
| data-telegram-id | int | — | The visitor’s Telegram ID. Saved if the contact doesn’t have one yet. |
| data-visitor-token | string | — | The visitor’s HMAC token, issued by your backend (see /docs/widget/hmac). With it, conversations from different devices go to the same contact. |
| data-hide-launcher | boolean | false | The value "true" hides the launcher button. The widget then opens only programmatically: call window.SupportHub.open() from your own button or link. It applies to the automatic start via ?ws=; with manual initialization, use SupportHub.init({ hideLauncher: true }). |
| async | boolean | false | Recommended. The bundle loads in parallel with the rest of the page and doesn’t block rendering. |
Programmatic control
The bundle creates window.SupportHub. Right after the script loads, it only has init() and identify(); identify calls made before the widget starts are collected and sent once it does. The other methods (open, close, isOpen, setTab, applyConfig, showState, destroy) appear once the widget has started, so call them with a check: window.SupportHub?.open?.().
// A "Contact support" button somewhere on your site:
document.getElementById("contact-support").addEventListener("click", () => {
window.SupportHub?.open?.();
});Auto-open
The widget opens by itself in two cases, and both are configured in SettingsWidget BuilderWelcome.
- Auto-open delay: after the set number of seconds (1–3600) from page load, once per session, and only if the visitor hasn’t opened the widget themselves yet. Empty or 0 turns it off.
- Open the chat right away in a Telegram Mini App: if the site runs as a Mini App, the panel opens straight away and the button is hidden (closing is left to Telegram). Turn it off if the Mini App is your site and the chat is secondary there: the widget then stays a regular button. Links opened from Telegram chats (the in-app browser) aren’t a Mini App: the widget never opens by itself there.
- Where to open by itself: for the delay, choose everywhere, only in a regular browser (not inside apps like Telegram or Instagram, and not in a site installed to the home screen), or only on a computer. The widget never opens by itself for search engine bots.
- What to open: the screen the widget opens on by itself (after the delay or in a Mini App): a tab or, for Help and News, a specific section, article or post. A tab the visitor can’t see is ignored, and the widget opens as usual.
Removing and updating
Remove the widget from your site
Remove the <script> tag. After your next deploy, visitors won’t see the button. Open conversations stay in the system and operators keep working on them, but new visitors can’t reach you without the widget.
No updates needed
The bundle updates itself: the tag always points to the same URL, and visitors get new versions on their next page load. The browser caches the bundle for 5 minutes (max-age=300) and after that may show the old copy once more while it refreshes it in the background (stale-while-revalidate), so updates and published settings arrive within a few minutes.

