The widget doesn't work: what to check
Real cases we see in production. For each one: what you'll see, what causes it and how to fix it.
The launcher doesn’t appear
Symptom
You added the <script> tag and opened the site, but there’s no round button in the corner.
1. Did the script actually load?
DevTools → Network → filter by widget-bundle. You should see GET 200 (or 304 from the cache).
- 404: a typo in the URL (check
support.forestsnet.com/widget-bundle?ws=…). - No
?ws=in the script URL: the bundle loads, but the widget doesn’t start; it starts only with the project ID in thewsparameter. - No request at all: an extension or CSP is blocking it. See below.
- 500: an error on our side. Contact support right away and send us the URL.
A wrong or deleted project ID doesn’t produce an error: the bundle still returns 200 and the launcher may appear with the default look, but messages don’t go through. Check the ID in the snippet.
2. Is the launcher hidden on purpose?
The data-hide-launcher="true" attribute on the script tag or init({hideLauncher: true}) removes the button; the widget then opens only through window.SupportHub.open(). Your custom CSS can hide it too.
3. Any errors in the Console?
The widget itself writes nothing to the Console. Look for the browser’s messages:
Refused to load the script…,Refused to connect to…: CSP is blocking it. See the next step.WebSocket connection to 'wss://…' failed: see The WebSocket fails.
4. CSP (Content Security Policy)
If your site sets a CSP, add our hosts:
Content-Security-Policy:
script-src 'self' https://support.forestsnet.com 'unsafe-inline';
style-src 'self' 'unsafe-inline';
connect-src 'self' https://api.support.forestsnet.com wss://api.support.forestsnet.com;
img-src 'self' data: blob: https://api.support.forestsnet.com;
media-src 'self' https://api.support.forestsnet.com;script-src: to load the bundle.'unsafe-inline'is needed because the loader snippet is itself an inline script. If you don’t want unsafe-inline, add the widget with a direct<script async src="…">tag.style-src 'unsafe-inline': the widget adds its styles as<style>tags.connect-src: REST calls and the WebSocket to our API.img-src/media-src: attachments, images, video and audio are served from the API host; a logo and icon uploaded in the builder are embedded asdata:, and previews in the message box useblob:. If you set images by URL (logo, avatars, cover), add those hosts too.
If something is still blocked, the Console names the directive and the host.
5. Blockers
Extensions such as Adblock, NoScript or Privacy Badger can block the bundle for individual visitors. If one particular visitor doesn’t see the widget, ask them to check their extensions.
The WebSocket fails or keeps reconnecting
The widget opens the WebSocket only when the visitor has a conversation: after their first message, or right on load if they already have an open ticket. Before that there’s no connection, so nothing can fail. If the Console shows WebSocket connection to wss://… failed again and again (the widget reconnects with a growing pause, up to 30 seconds), check:
- The CSP
connect-src: it must includewss://api.support.forestsnet.com. - A corporate proxy or firewall that doesn’t let WebSockets through: REST works, but the socket doesn’t.
- An expired or invalid
visitor_token: the server closes the connection with code4401. See The token isn’t accepted (401).
Without the socket, the widget still opens and sends messages, but operator replies don’t appear right away; the visitor sees them when they open the conversation again. There’s no fallback channel (long polling).
One visitor → two contacts (cross-subdomain)
A visitor writes from example.com, then moves to app.example.com, and the dashboard shows two different contacts with two parallel conversations.
Cause
The visitor session ID is stored in a cookie scoped to the host and in localStorage, which is separate for every subdomain. By default, subdomains don’t share it.
Fix
Set a cookie domain with a leading dot:
<script
async
src="https://support.forestsnet.com/widget-bundle?ws=YOUR_WORKSPACE_UUID"
data-cookie-domain=".example.com"
></script>You can set the same thing in the “Cookie domain (optional)” field in SettingsWidget BuilderAdvanced, and the snippet stays the same on every subdomain. data-cookie-domain wins over the config (handy if staging and production are on different domains).
The visitor is “forgotten” after a few days (Safari ITP)
A visitor comes back in Safari a week later, and SupportHub shows them as new: the old history isn’t picked up.
Cause
Safari’s Intelligent Tracking Prevention caps cookies set by scripts at 7 days, and if the visitor hasn’t used the site for a while, it can delete the site’s other data too. Without the ID, the backend treats the visitor as new.
Fix
The widget keeps the ID in several places at once (cookie, localStorage, sessionStorage, IndexedDB) and on every load restores the cookie from whichever copy survived. That helps when only the cookie is gone; if Safari cleared everything, the visitor is new.
To keep the link reliably, use identify with a visitor_token from your system.
The panel opens empty or broken
Cause 1: custom CSS
Custom CSS applies to the whole page and can hide or cover parts of the panel. Clear the “Custom CSS” field in SettingsWidget BuilderAdvanced, publish and check again.
Cause 2: requests to the API are blocked
The panel is built from the config baked into the bundle, while the history, articles and news load from the API. If CSP or the network blocks requests to api.support.forestsnet.com, those parts stay empty. Check the Network tab for blocked requests.
The builder’s “Discard” button drops only unpublished draft changes. If the problem is in the published version, fix the field and publish again.
The pre-chat step asks despite identify
Cause
The widget skips only the required fields it already knows. Required fields are set in SettingsWidget BuilderForms & Topics; the email, name and phone from data-* attributes or identify() count as known (the Telegram ID doesn’t), as do the contact’s details from earlier conversations. If the phone is required and you passed only the email and name, the widget asks for the phone.
The check happens when the visitor opens a new conversation, so an identify() call after that doesn’t affect a conversation that’s already open.
Fix
Pass every required field and call identify() before the visitor opens the chat, for example right after sign-in:
window.SupportHub.identify({
email: user.email,
name: user.fullName,
phone: user.phone, // if the phone is required too
});“Powered by SupportHub” won’t hide
Cause
The Hide "Powered by SupportHub" toggle in the Brand section is available only on plans with custom branding. On other plans it’s locked, and the line stays.
Fix
Move to a plan with custom branding, through the “View plans” link under the toggle or in SettingsPlan & billing. The toggle then unlocks: turn it on and publish.
Builder changes don’t reach visitors
Cause
The builder saves changes to a draft, while visitors see only the published version. The Publish button is in the builder’s top bar.
Cache
The published config is built into the bundle, and the bundle is cached for 5 minutes with background revalidation (Cache-Control: public, max-age=300, stale-while-revalidate=86400). For up to 5 minutes after publishing, a visitor can get the old version, and after that the browser may show the cached copy once more while it fetches the new one in the background. The /config response is cached for 60 seconds. To check your changes right away, reload the page without the cache (a hard refresh).
Reactions / inline keyboard don’t work
Both are on by default.
- Reactions. The visitor reacts to an operator’s message: the emoji panel appears on hover, or on a long press on a touch screen. The builder has no switch for it; reactions work as long as
conversation.reactions_enabledisn’t turned off in the config. - Inline keyboard. Buttons under a message appear only if the message itself has them. The “Enable inline keyboards” switch is in SettingsWidget BuilderReply Keyboards: check that it’s on and the change is published.
The token isn’t accepted (401)
Requests with a visitor_token get 401: the conversation doesn’t load, the visitor sees “Couldn’t send — please try again” when sending, and the WebSocket is closed with code 4401. The detail field shows the reason:
curl "https://api.support.forestsnet.com/api/webhooks/widget/YOUR_WS/recent?visitor_token=TOKEN"invalid visitor_token signature: the wrong secret (say, it was rotated in SettingsChannelsWeb Widget and your server still signs with the old one), or the HMAC was computed over something other than the bytes in the token’s first part (over the base64 string, for example).invalid visitor_token: the token isn’t<base64url>.<hex>, or the payload isn’t JSON.visitor_token expired:exphas passed; check the TTL and your server’s clock.invalid visitor_token exp:expisn’t a number.visitor_token missing user_id: the payload has nouser_id, or it’s empty.
The format and the checks are on the HMAC visitor token page.
Messages aren’t sent (403)
Sending messages, uploading files and identify get 403 widget origin not allowed. The cause is the allowed domains list in SettingsWidget BuilderPrivacy: if it isn’t empty, requests are accepted only from the listed domains (example.com matches only that host, *.example.com any subdomain). Add your site’s domain and publish.
Something else?
Didn’t find your case? Write to us through the widget on support.forestsnet.com and we’ll add it to this page.

