Troubleshooting
The ten things that go wrong, in roughly the order people hit them. Each section names the symptom, the most common cause, and the fix.
The bubble doesn't appear
Symptom: You added the script tag, published, opened the page in incognito, and there's no bubble.
Check first: Browser console (F12 → Console). If you see a red error mentioning usewilow.com, jump to the matching section below — CORS, 401, etc.
If the console is clean:
- Script tag not actually on the page. View source (
Ctrl+U/⌘+⌥+U) and search forloader.js. If it's not there, your platform's caching layer didn't pick up the change. Re-publish, hard-refresh. - Two widgets on the page. Some platforms inject a previous draft of the script. View source and search for
loader.js— if you see it twice, only one will boot. Remove the older one. - Maintenance mode is on. Open admin → Widget → Maintenance and check whether you flipped that toggle yourself recently. The widget shows a maintenance notice instead of the bubble when paused.
CORS error in the console
Symptom: Console shows something like:
Access to fetch at 'https://usewilow.com/api/widget/config'
from origin 'https://example.com' has been blocked by CORS policyCause (99% of the time): the origin isn't on your API key's allowed list.
Fix: Admin → API keys → ⋯ → Edit origins. Add the exact origin shown in the error message. Watch for:
- Scheme matters.
http://andhttps://are different. List the one your site actually uses. - Subdomain matters.
example.comandwww.example.comare different. Add both if both serve the widget. - Port matters when non-default.
http://localhost:3000is not the same ashttp://localhost. - No wildcards.
https://*.example.comis invalid; list each subdomain.
After saving, hard-refresh the page (Cmd+Shift+R / Ctrl+Shift+R). Allowed-origins changes are immediate server-side; only the browser's request needs to retry.
401 Invalid API key
Symptom: Network tab shows /widget/config returning 401 with body {"error": "Invalid API key"}.
Causes, in order:
- Typo in
data-api-key. Most common. Look for stray quotes, line breaks, or a half-pasted value. - Key was revoked. Check Admin → API keys; if the row is missing or marked inactive, the loader tag still has the old value. Mint a new one and update the script tag.
- Wrong account. If you have multiple accounts under the same login, the key you minted in account A won't authenticate against account B.
The widget never tries again with a different value, so refreshing the page won't help — fix the script tag and re-deploy.
Streaming reply never arrives
Symptom: You send a message, the typing indicator shows, but no text appears. The conversation hangs.
First check: Network tab → look at the /messages request. If it's still pending after 30+ seconds, the SSE stream is stuck.
Causes:
- Corporate proxy / WAF buffering SSE. Some enterprise proxies hold streamed responses until the whole stream completes, then dump it in one go. There's no fix on our side; either the visitor's network whitelists
usewilow.comor they switch networks. This rarely affects retail visitors — almost always a B2B SaaS issue. - Browser extension intercepting fetch. Aggressive ad blockers occasionally kill the EventStream MIME type. Test in incognito with extensions off.
- You hit your workspace's conversation cap. Status
400 conversation limit exceededin the response body. Talk to us if you need a higher cap, or wait for next billing period.
If the request returned 5xx, that's on us — file a bug with the request id from the response headers.
Visitor JWT not landing on tool calls
Symptom: You configured a tool with "Requires the visitor to be signed in" and the bot says it can't help anonymous visitors, even though you're sure the visitor is signed in.
Walkthrough:
- Open devtools on the visitor page → Application → Cookies. Confirm the JWT cookie name you configured under Widget → Privacy → Visitor JWT cookie name is actually present, not HttpOnly. HttpOnly cookies are invisible to JavaScript by design — the loader can't read them. Either remove
HttpOnly(security tradeoff) or usedata-user-jwt/window.wilow.identify()instead. - If the cookie is there but the bot still treats the visitor as anonymous, check the cookie name exactly — the loader does case-sensitive equality.
auth_token≠Auth_Token. - Open the Playground (admin → Chat playground), reproduce the question. The side panel shows whether the JWT was forwarded to the tool webhook. If it shows "no JWT", the loader didn't see one.
See Authenticated visitor tools for the three identification shapes (script attribute, window.wilow.identify(), cookie).
Webhook signature mismatch
Symptom: Your lead/ticket webhook receiver is logging signature mismatch (or you're returning 401 from your verifier).
Almost always one of these:
- Body was parsed before verification. You signed against
req.bodyafterexpress.json()had already turned the bytes into an object and re-serialized them. The bytes you hashed don't match the bytes we signed. Fix: useexpress.raw({ type: "application/json" })on the webhook route — see Webhook security for framework specifics. - Wrong secret. You rotated the secret in Admin and haven't redeployed your verifier. Or you're using a staging secret against prod.
- Encoding mismatch. The signature is
sha256=<hex>. If your code is comparing against just<hex>(nosha256=prefix) or against base64, it'll never match.
The X-Wilow-Delivery-Id header is unique per send — log it on every receipt so you can correlate with our delivery logs if you escalate.
Custom CSS doesn't apply
Symptom: You saved CSS in the editor, but the live widget on your site still looks default.
In order of likelihood:
- Page is cached. The widget config has a 60-second freshness window — visitors loading right after you save may still see the previous CSS. Wait a minute, hard-refresh.
- Selector targeting the wrong thing. The widget renders inside an iframe. CSS in the editor goes into the iframe; you're styling Wilow's internal classes (e.g.
.wilow-bubble,.wilow-message). External page CSS can't reach inside the iframe — that's a security boundary, not a bug. See Custom CSS for the class names. - CSS rejected by the validator. Saving with invalid CSS surfaces an inline error in the editor; if you missed it, your previous CSS is what's still serving. Re-open the editor and look for the error.
- Feature turned off. If custom CSS was disabled on your workspace, your CSS is preserved server-side but not applied. Talk to us to re-enable.
Lead webhook never fires
Symptom: Visitors leave their email, the lead shows up in Leads, but your endpoint never gets called.
Walkthrough:
- URL configured? Admin → Widget → Lead webhook. If the field is empty, no delivery happens (the lead still records in our DB).
- URL reachable from our side? Try
curl -X POST <your-url> -d '{}'from a public network. If you get a connection error, your firewall or VPN is blocking inbound from us. We don't currently advertise a static egress IP range; the URL must accept arbitrary inbound. - Feature not enabled. If the lead webhook isn't on for your workspace, the URL field is read-only and we don't deliver. Talk to us to enable it.
- Failures on your endpoint aren't retried. A 5xx response from your endpoint is logged on our side but doesn't trigger a retry — by design, to avoid hammering a struggling endpoint. If reliability matters, front your CRM with a queue you control.
SSO login fails or the IdP shows an error
Symptom: You set up SSO, click Sign in with SSO, and either bounce back with an error or the IdP itself shows a "AppNotAssigned" / "InvalidAssertion" type message.
Walk it in order:
Redirect URI / ACS URL mismatch. On the IdP side, the registered redirect (OIDC) or ACS (SAML) URL must match what the Wilow SSO page is showing exactly — same scheme, same host, same path, no trailing slash if Wilow doesn't show one. Copy-paste from the SSO page, don't retype.
User's email domain isn't in your allowed list. The Wilow SSO config has an Allowed email domains field. If the IdP-asserted email's domain isn't in there, the callback rejects them. Check Settings → SSO → Allowed email domains.
Signing certificate expired (SAML only). Some IdPs rotate certs on a schedule and forget to publish the new one. Re-paste the current PEM from your IdP's metadata into the Wilow SSO config.
Issuer URL wrong (OIDC only). The Issuer URL must be the
issclaim value your IdP actually emits — sometimes there's a trailing/v2.0or an environment-specific tenant id. Check the OpenID metadata endpoint (<issuer>/.well-known/openid-configuration); whatever the metadata says, use exactly.User not assigned to the app (Microsoft Entra / Okta / Auth0). Some IdPs require explicit user assignment to an app. If your IdP is in that group, the IT admin needs to add the Wilow application to the user (or assign by group).
If nothing above fits, screenshot the IdP-side error and send to support — we can usually identify the misconfiguration from the wording.
I hit my usage cap and the bot stopped answering
Symptom: Visitors report the widget shows a "we're not taking new chats right now" message and you didn't pause it.
Cause: Your message usage hit the configured limit. Two flavours:
- Budget-stop is set to "always stop" and you've hit the plan cap. The widget stops to avoid overage charges. Either increase your hard cap (see usage limits) or flip budget-stop to keep serving and accept overage charges within the hard cap.
- You hit your overage hard cap. Same UX, slightly different cause — you've already accumulated overage charges up to the maximum you said you'd pay. Increase the cap on usage limits, or wait for the next billing period.
Check usage to see where you are. Trial accounts behave the same way at the trial cap.
Still broken?
If none of the above fits, email [email protected] with:
- Your account's Tenant ID (Admin → Account → Account, shown as a read-only field)
- The URL where it's broken
- Browser + OS
- A screenshot or copy-paste of any console / network error
- The approximate time you saw the issue (UTC)
That's enough for us to pull logs and trace it.