Troubleshooting
Widget / Troubleshooting

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 the ws parameter.
  • 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:

http
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 as data:, and previews in the message box use blob:. 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 include wss://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 code 4401. 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:

html
<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:

js
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_enabled isn’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:

bash
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: exp has passed; check the TTL and your server’s clock.
  • invalid visitor_token exp: exp isn’t a number.
  • visitor_token missing user_id: the payload has no user_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.

Was this page helpful?