Troubleshooting

Beta
Last updated  Sep 23, 2026

Most problems come down to three things: the map, the adapter, or the voice setup. Start with the first checks, then find your symptom below.

First checks

  • Log every decision with onAction={(action) => console.log(action)}. A refused action has a reason and a detail that usually tell you exactly what’s wrong.
  • Pass voice.onDiagnostic and voice.onError and watch the console while you tap the orb.
  • Test your map without voice: remove the key and the voice prop, and type into the panel. The local mode uses the same map and validation chain.
  • Drive a turn from code with ref.current.sendText("take me to billing") and inspect the AgentAction it returns.

The orb only opens a chat panel

  • The key is undefined, so Usher is in local text-only mode (the panel status reads “Text mode”). Check that VITE_VOQAL_KEY or NEXT_PUBLIC_VOQAL_KEY is set in .env.local, then restart the dev server. Vite and next dev read that file only at startup.
  • In a deployed Next.js app, NEXT_PUBLIC_ values are inlined at build time. Set the variable in the environment and rebuild.

The orb doesn't appear

  • It’s fixed to the bottom-right at z-index 40. Check whether one of your overlays covers it.
  • In Next.js, make sure it renders from a client component. See Render it in the browser only.
  • Make sure <Usher> is mounted on the page you’re looking at. Mount it once in your signed-in layout, not inside one route.

Voice won't start

  • origin_not_allowed (403): the page’s origin isn’t on your key’s allow-list. Send Voqal the exact origin, including scheme and port, for every environment: production, staging, and preview domains. http://localhost and http://127.0.0.1 work on any port.
  • rate_limited (429): too many sessions for the key this minute. Wait and tap again.
  • invalid_key: the key is unknown or disabled, or it doesn’t start with pk_live_ or pk_test_.
  • Permission denied: onError gets a NotAllowedError. Reset the site’s microphone permission in the browser. Chat still works through Aa.
  • Not HTTPS: browsers only allow the microphone on HTTPS or localhost.
  • Blocked socket: a CSP without the right connect-src host, or without blob: in script-src, stops the session or the microphone. See CSP.

It talks but nothing moves

  • It may have offered rather than navigated. That’s the design for “how do I” questions. Say “yes”, or say “take me to…”.
  • You may already be on that page. Usher doesn’t navigate to the current page.
  • Look for a refused action. not_in_allowlist means the id isn’t in the browser’s map.
  • navigation_failed means your adapter’s navigate threw. The detail holds your router’s error.
  • You added a page to destinations during a live session. An open session keeps the map it started with. End it and start a new one. See When prop changes apply.

It picks the wrong page

  • Give each important page a specific title and a one-line semanticDescription. That’s what the voice model reads.
  • Remove generic one-word aliases shared by sibling pages. Add the phrases users actually say.
  • Discovered-only routes get titles from their path (/settings/team becomes “team”). Add a manifest entry for anything users ask for by name.
  • The local ranker only matches ASCII letters and digits. Aliases in other scripts are ignored there, though the voice model still reads titles and descriptions in any language.

It keeps asking which one

  • For dynamic pages: a param_unresolved refusal means the id couldn’t be found. Pass context.selectedEntity on detail pages, or add resolveEntity.
  • For similar pages: two destinations score close together. Make their aliases and descriptions clearly different.

It doesn't use my knowledge base

  • Look for a console.warn saying entries didn’t fit. A live session carries at most 8,000 characters of whole entries, in the order you list them. Put the most important entries first, and keep each one to a sentence or two.
  • Knowledge loaded after the session started reaches the next session, not the open one.
  • In local text mode, an entry has to share at least 30% of the question’s words. Add triggerPhrases for how users ask.

It thinks I'm on another page

  • Your adapter’s currentLocation() is stale. In Next.js, keep the location in a ref and pass subscribe, as in the App Router example.
  • Without onLocationChange events, a live session isn’t told when the user moves by hand.

The conversation resets or disconnects

  • A changed assistantId or organizationId starts a new conversation. That’s deliberate.
  • Signing out with runtime().logout() ends the session and clears the panel, as intended.
  • × on the orb and End in the chat panel end the session and clear the transcript. A tap on the orb only pauses, and – in the panel only minimizes it.
  • UsherSessionLostError means the live connection dropped and three reconnects failed, or the server closed it. The next tap starts a fresh session.
  • After a network drop, or the hosted service’s roughly 10-minute session cutoff, Usher reconnects with a fresh credential. The reconnected model doesn’t have the earlier conversation, so it may ask again about something already said. The transcript on screen is unaffected.
  • A full page reload starts a new session. The conversation lives in memory for this beta.

Next.js

  • “useRouter only works in Client Components”: add "use client" to the file that renders <Usher>.

Getting help

During the beta, email hello@voqal.ai with the package versions, your router, the AgentAction from onAction, and the diagnostics from onDiagnostic. Leave out tokens and user data.

© 2026 VoqalVoqal SDK & engine documentation