Usher quickstart

Beta
Last updated  Sep 23, 2026

Get the orb running in a React Router app in six steps. For Next.js, follow the same steps and use the adapter from Framework adapters.

1. Install

Install the React bindings, the core, and the adapter for your router from the beta tag. Peer dependencies: react and react-dom 18+, plus react-router-dom 6.4+ (React Router 7 works) or next 13+.

terminal
# React Router appnpm install @voqal/usher-react@beta @voqal/usher-core@beta @voqal/usher-react-router@beta# Next.js app (swap the adapter)npm install @voqal/usher-react@beta @voqal/usher-core@beta @voqal/usher-next@beta# Pin the exact beta in production# (every package at 0.1.0-beta.0)

2. Get your key

Voqal issues each app a publishable key (pk_live_…). The key identifies your assistant, and Voqal hosts the voice session behind it, so you don’t run a server. It’s safe to ship in client code. To get one, email hello@voqal.ai with every origin your app is served from.

  • Test and live keys. Voqal gives you a pk_test_… key for development and a pk_live_… key for production. They have the same features. A test key is registered for your localhost origins, and its conversations are logged under a separate test prefix so they never mix with real users’ logs. Keep the key in an environment variable so each environment uses its own.
  • Origins are allow-listed per key. A key works only from the exact origins registered for it, such as https://app.example.com. List production, staging, and every preview domain. http://localhost and http://127.0.0.1 work on any port for development. From any other origin the hosted service answers 403 origin_not_allowed.
  • Keys are rate-limited. Past the limit, starting a session fails with rate_limited until the window resets.
  • Both errors reach voice.onError. See Hosted-key errors.
With a key, Voqal logs each conversation: what the user typed or said, what the assistant said, the tools it called, and audio clips of both sides. Logs are stored in the EU and kept indefinitely. Tell your users, and cover it in your privacy notice. You can turn audio or all logging off with the cloud prop; see What leaves the browser.

3. Lay out the files

Four files, with imports flowing one way: your router knows nothing about Usher, the map imports the router, the Usher layer imports the map, and the entry point renders both. Keep it that way and there are no circular imports.

project layout
src/  router.tsx                 # createBrowserRouter(...): imports your pages only  usher/destinations.ts      # the map: imports router.tsx  usher/usher-layer.tsx      # renders <Usher>: imports destinations.ts  main.tsx                   # renders <RouterProvider> and <UsherLayer>.env.local                   # VITE_VOQAL_KEY=pk_test_…

Using Next.js? The equivalent layout is on Framework adapters.

4. Let Usher find your pages

Your router stays as it is. Hand Usher the router and it discovers every route automatically, including dynamic ones like /cases/:caseId. Each page is titled from its path words (/settings/billing becomes “billing”), or from handle.usherLabel when a route sets one, as the cases and booking routes below do. That’s all Usher needs to start navigating.

src/router.tsx
import { createBrowserRouter } from "react-router-dom";import { Billing, BookConsultation, CaseDetail, Cases, Home, Layout } from "./pages";export const router = createBrowserRouter([  {    path: "/",    element: <Layout />,    children: [      { index: true, element: <Home /> },      { path: "cases", element: <Cases />, handle: { usherLabel: "My cases" } },      { path: "cases/:caseId", element: <CaseDetail /> },      { path: "consultations/book", element: <BookConsultation />, handle: { usherLabel: "Book a consultation" } },      { path: "settings/billing", element: <Billing /> },    ],  },]);
src/usher/destinations.ts
import { buildDestinationMap } from "@voqal/usher-core";import { reactRouterAdapter } from "@voqal/usher-react-router";import { router } from "../router";export const adapter = reactRouterAdapter(router);// Every route in your router becomes a destination. No list to maintain.export const DESTINATIONS = buildDestinationMap({ discovered: adapter.discover() });

Optional, recommended for your top pages: a manifest

Discovery knows a page’s path; it doesn’t know what the page is for or how your users ask for it. For the pages people ask for by name, add a manifest entry with a title, a one-line description, and the phrases they actually say. You can also restrict pages to roles and set a fallback page. The manifest wins over what was discovered, and every other route stays reachable.

src/usher/destinations.ts
import { buildDestinationMap, type DestinationManifestEntry } from "@voqal/usher-core";import { reactRouterAdapter } from "@voqal/usher-react-router";import { router } from "../router";// Optional: richer meaning for the pages users ask for by name. It wins over discovery.const MANIFEST: DestinationManifestEntry[] = [  {    destinationId: "cases",    routePattern: "/cases",    title: "My cases",    semanticDescription: "Every legal matter the user has open or closed.",    aliases: ["my cases", "case list", "my matters"],  },  {    destinationId: "consultations.book",    routePattern: "/consultations/book",    title: "Book a consultation",    semanticDescription: "Schedule a call with a lawyer: pick a practice area, a time, and a lawyer.",    aliases: ["book a consult", "talk to a lawyer", "schedule a call"],  },];export const adapter = reactRouterAdapter(router);export const DESTINATIONS = buildDestinationMap({  discovered: adapter.discover(),  manifest: MANIFEST,});

The Destinations guide covers every field, role scopes, dynamic ids, and the parent-page fallback.

5. Mount <Usher>

Render <Usher> once, beside your router, not inside a route. It draws a fixed orb in the bottom-right corner and keeps its session when routes change.

src/usher/usher-layer.tsx
import { useMemo } from "react";import { Usher } from "@voqal/usher-react";import { adapter, DESTINATIONS } from "./destinations";export function UsherLayer({ role }: { role: string }) {  const context = useMemo(() => ({ role }), [role]);  return (    <Usher      voqalKey={import.meta.env.VITE_VOQAL_KEY}      router={adapter}      destinations={DESTINATIONS}      context={context}    />  );}
src/main.tsx
import { StrictMode } from "react";import { createRoot } from "react-dom/client";import { RouterProvider } from "react-router-dom";import { router } from "./router";import { UsherLayer } from "./usher/usher-layer";createRoot(document.getElementById("root")!).render(  <StrictMode>    <RouterProvider router={router} />    <UsherLayer role="member" /> {/* the signed-in user's role */}  </StrictMode>,);
.env.local
# .env.local: not committed. Use a pk_test_ key locally and pk_live_ in production.VITE_VOQAL_KEY=pk_test_…
  • voqalKey turns on voice and chat on one hosted live session, with the browser microphone and speaker wired for you. No voice prop is needed.
  • If the variable is missing, voqalKey is undefined and Usher quietly runs in local text-only mode. Vite reads .env.local only at startup, so restart the dev server after creating or changing it. If the orb only opens a chat panel, check this first.
  • context.role scopes which destinations this user can hear about and reach. Pass the same role labels you use in roleScopes.

6. Try it

  • Tap the orb and allow the microphone. Say “take me to my cases”. It confirms out loud and your router moves.
  • Ask “how do I book a consult?”. It answers and offers to take you there.
  • Tap Aa and type the same thing. It’s the same conversation.
  • Ask for a page that isn’t in your map. It says so, and nothing moves.

Local text-only mode

Without a key, Usher runs entirely in the browser with a built-in rules-based reasoner. There’s no microphone and no model call, and typed turns go through the same destination map and validation chain. Use this mode to check your map and your router wiring before you add the key. Passing a voice prop without a key throws, so leave voice out in this mode.

src/main.tsx
// No key: a local, text-only Usher for wiring tests.<Usher router={adapter} destinations={DESTINATIONS} />
© 2026 VoqalVoqal SDK & engine documentation