Troubleshooting
Solutions for the most common widget integration issues: CSP blocks, CORS errors, the bubble not appearing, and recording failures.
On this page
- Known limitations
- The bubble does not appear on my page
- Content Security Policy
- Not CSP: the microphone Permissions-Policy
- A data-voicegram-trigger button does nothing
- The modal opens with default theme, then re-styles a moment later
- The modal opens but my domain is disabled
- The verification email does not arrive
- Recording fails on iOS Safari
- CORS errors in the console
- Still stuck
- Next steps
If the widget is not behaving as expected, work through the checks below in order. Most issues are a stale browser cache, a mismatched public key, or a missing meta tag.
Known limitations
Some behaviors are by design. Confirm none of these match your situation before you dig deeper:
- Light DOM only. Triggers and the bubble both work in the light DOM. Closed Shadow Roots and (effectively) open Shadow Roots block trigger detection. Do not put
data-voicegram-triggerinside encapsulated component shadow roots. - Domain must match. The widget's public key is tied to a registered domain in the Voicegram dashboard. The widget hides itself on pages whose hostname does not match.
- Browser cache up to 24 hours. The widget bundle on
cdn.voicegram.iois cached for up to 24 hours. After a widget update ships, returning visitors may continue to use the previous bundle for up to a day until their cache expires. New visitors get the new bundle immediately. - Single instance per page. Loading the widget script twice in the same page is unsupported. Each init call overwrites the previous instance.
- iframes. The widget is scoped per document. A trigger in the parent page only fires if the widget is loaded in the parent. A trigger inside an iframe needs the widget loaded in that iframe.
- Mobile native. WebView-based apps work, pure native does not. See Mobile and native apps.
The bubble does not appear on my page
Symptoms. You added the script tag, but no floating bubble shows up in the bottom-right corner.
Work through these in order:
- Hard-refresh. Browser cache may be serving an older script. Cmd+Shift+R (macOS) or Ctrl+Shift+F5 (Windows), or load the page in an Incognito window.
- Check the public key matches the domain. Open DevTools, switch to the Network tab, reload the page, and look for a request to
/api/config?publicKey=...&domain=.... If the response is{ success: false, ... }, the key and domain do not match. Re-check the public key in the dashboard against the hostname of the page you are loading. - Check
<meta name="voicegram-bubble">. Ifcontent="off"is set on the page, the bubble is hidden by design. See Customization. - Check the dashboard. If the domain is marked inactive in your Voicegram dashboard, the widget hides automatically.
- Check
window.__voicegram__exists. In the DevTools console, typewindow.__voicegram__. If it isundefined, the script did not load. Look for network errors on the script URL, or for a Content Security Policy violation in the console.
Content Security Policy
If your site sends a Content-Security-Policy header, add the following. Most sites have no CSP at all, in which case nothing here applies and the widget works with only the script tag.
script-src https://cdn.voicegram.io
connect-src https://www.voicegram.io https://*.livekit.cloud wss://*.livekit.cloud
media-src blob:
style-src 'unsafe-inline'
What each one is for:
script-srchttps://cdn.voicegram.io. The widget bundle and its lazily-loaded recording chunk.connect-srchttps://www.voicegram.io. Session creation, verification, and telemetry. Note thewww.It is required: CSP host matching is exact, sohttps://voicegram.ioalone does not matchwww.voicegram.ioand every request will be blocked.connect-srchttps://*.livekit.cloud wss://*.livekit.cloud. The realtime recording connection. Use the wildcard, not the specific hostname. The realtime service fails over to regional hostnames, so a pinned host works until the first failover and then breaks intermittently. Both thehttps://andwss://entries are needed: the connection starts with regular HTTPS requests before upgrading to a WebSocket.media-srcblob:. Playing back the recording locally before sending.style-src'unsafe-inline'. The widget styles itself from an inline stylesheet inside its Shadow DOM.
Not required: img-src (all icons are inline SVG), font-src (system fonts only), frame-src (no iframes), worker-src, and 'unsafe-eval' (the widget contains no workers, no WebAssembly, and no eval).
How to confirm this is your problem. Open DevTools and look in the Console for a line beginning [Voicegram] Recording blocked by this page's Content Security Policy. The widget detects the block directly and prints exactly what to add. If you see a red "Refused to connect" CSP error naming a livekit.cloud address, that is the same thing.
Not CSP: the microphone Permissions-Policy
This is a separate header from CSP and a separate failure. If your site sends:
Permissions-Policy: microphone=()
then () is an empty allowlist meaning nobody, not even your own scripts. The browser denies the microphone before it ever shows a permission prompt, so no visitor can fix this and resetting site permissions in the browser changes nothing. Change the microphone entry to:
Permissions-Policy: microphone=(self)
How to confirm — use Chrome. The DevTools console shows [Violation] Permissions policy violation: microphone is not allowed in this document, and the widget prints a line beginning [Voicegram] Recording blocked by this page's Permissions-Policy header naming the fix.
Both lines are Chromium-only. Safari and Firefox do not expose the policy introspection the widget uses to tell a header block apart from a visitor declining the microphone prompt, so in those browsers the widget shows the ordinary "allow microphone access" message even when the real cause is this header. Check the response headers directly if you cannot use Chrome.
Note this fires before any CSP problem, because the microphone is requested before the recording connection opens. A site with both misconfigurations will hit this one first, and fixing the CSP alone will not appear to help.
If the widget is loaded inside an iframe, that iframe also needs allow="microphone".
A data-voicegram-trigger button does nothing
Symptoms. You added the attribute to a button, but clicking does not open the modal.
- Confirm
window.__voicegram__exists. If the widget script did not load, the document-level click listener does not exist. See the bubble-does-not-appear section above. - Confirm the button's hostname matches. Cross-iframe triggers do not work. The widget must be loaded in the same document as the trigger.
- Confirm the button is in the light DOM. Triggers inside Shadow Roots do not bubble correctly. See Customization › Light DOM only.
- Confirm the button is not
disabled. The browser blocks click events on disabled controls before the widget sees them. - Cmd-click and middle-click are intentionally ignored. Plain left-click only. This is so
<a href data-voicegram-trigger>can still open in a new tab on Cmd-click.
The modal opens with default theme, then re-styles a moment later
Symptoms. A brief flash of the default Voicegram colors before your branded theme paints.
This should not happen on current widget versions. If you see this:
- Hard-refresh to drop the stale browser-cached bundle.
- If it persists, contact us. This is a bug.
The modal opens but my domain is disabled
Symptoms. A visitor opens the widget on a domain you have marked inactive in the dashboard.
This should not happen on current widget versions. If you see this:
- Hard-refresh to drop the cached older bundle.
- If it persists, contact us.
The verification email does not arrive
Symptoms. The visitor submits their email, but no 6-digit code arrives.
This is usually a backend or email-deliverability issue, not a widget issue.
- Check the address spelling. Typos are the most common cause.
- Check the spam folder. Verification emails go to spam occasionally on first contact with a sender.
- Check the Voicegram dashboard error logs. Common causes surfaced there: the email address has DNS or disposable-domain validation issues, or your custom-domain email sending is misconfigured.
- Try a different email. Some corporate mail filters silently drop transactional emails from new senders.
Recording fails on iOS Safari
Symptoms. The widget opens, the visitor enters the verification code, but the recording UI never appears or recording fails immediately.
- iOS 14.3 or later is required. Earlier versions can't record in Safari, so ask the visitor to update iOS.
- Microphone permission must be granted. When prompted, the visitor must tap Allow. If they previously denied permission, they have to clear the per-site permission in Settings › Safari › Camera & Microphone before the prompt re-appears.
- Some captive-portal Wi-Fi networks block live recording. Public Wi-Fi at airports, hotels, and coffee shops occasionally blocks the live audio connection. Try cellular or a different network to rule this out.
CORS errors in the console
Symptoms. Console shows Access-Control-Allow-Origin errors against voicegram.io or cdn.voicegram.io.
These should not appear on properly registered domains. If they do:
- Confirm your page's hostname is registered in the dashboard. Add the domain under Dashboard › Widgets.
- Confirm you are using the public key (
pk_*) and not a secret key (sk_*). Secret keys are server-side only and do not authenticate widget traffic. - If both check out and you still see CORS errors, contact us with the full console error.
Still stuck
If none of the above resolves your issue:
- Capture a screenshot of the console errors (DevTools › Console).
- Capture the network response for
/api/config?publicKey=...&domain=...(DevTools › Network). - Note the widget bundle URL including any query parameters.
- Contact us with those three artifacts.
Next steps
- Quickstart. The one-line install plus the four-step smoke test.
- Customization. Custom triggers and per-page bubble suppression.
- Mobile and native apps. Mobile-specific install gotchas.