Browse In-App Agent

You're viewing docs for @revrag-ai/embed-react 1.5.0, an older version. The latest is 2.0.0. View the latest docs

Revrag Embed — Web Integration Guide

The complete guide for adding the Revrag AI voice agent to a web app.

What you getOne floating button that follows your user across every route, and a voice agent that can see your page and act on it
Package@revrag-ai/embed-react 1.5.0
React17, 18 or 19
Next.jsApp Router and Pages Router
Preact10, via preact/compat
TypeScriptTypes included
API keyGet it from https://app.revrag.ai

This guide documents 1.5.0. Do not follow it against 1.4.4 or earlier.

Earlier versions (up to 1.4.4) predate most of it. They have:

  • no /react17 or /diagnostics entry
  • no appVersion option
  • no Action Intelligence, no call controls and no agent events
  • no EmbedLogout()

Every web app integrates in the same three steps — a Vite or CRA single-page app, a Next.js site on either router, React 17 or Preact:

StepWhat you doDetails
1. InitializeCall useInitialize once, at your app root§2
2. MountWrap your app in one EmbedProvider§3
3. IdentifySend your user (USER_DATA) after loginIdentify your user

Section 5 shows the exact setup for your app. Everything before it applies to every app.


Table of contents

  1. Install — package and stylesheet
  2. Initialize — one hook, once, at the root
  3. Mount — one provider, and the SDK follows the user
  4. Decide where it appears — paths, groups, delays, and the call licence
  5. Your app's shape — pick one, complete code
  6. Control it from your own code — calls and the widget card
  7. Events and analytics — what you send, what you can listen to
  8. How paths become screen names
  9. Verify it works
  10. Troubleshooting
  11. Known limitations
  12. Checklist

1. Install

Install the package, import the stylesheet once, and check the peer dependencies and requirements.

Package

npm install @revrag-ai/embed-react
# or
yarn add @revrag-ai/embed-react
pnpm add @revrag-ai/embed-react

Stylesheet — required

Import it once, at your app's entry point:

import '@revrag-ai/embed-react/style.css';
  • Without it, the button looks broken. It gets only a minimal fallback (position and layout).
  • The path is @revrag-ai/embed-react/style.css. The old @revrag-ai/embed-react/dist/style.css path does not resolve.

Peer dependencies

PackageRangeRequiredNotes
react, react-dom>=17.0.0YesReact 17 has its own entry on webpack hosts — see 5D
next>=14.0.0OptionalThe SDK never imports next
react-router-dom>=6.0.0OptionalThe SDK never imports it
  • next — on Next.js 12 or 13, npm refuses the install with ERESOLVE. Use npm install --legacy-peer-deps until the range is widened.
  • react-router-dom — React Router 5 hits the same ERESOLVE.

Requirements

React17, 18 or 19. Preact 10 through preact/compat
BrowserAny current browser with WebRTC and getUserMedia
PageA secure context — HTTPS, or localhost in development. Browsers refuse the microphone anywhere else
RenderingSafe to server-render: nothing touches window at import, and the button renders a hidden placeholder until it hydrates
TypeScriptmoduleResolution: "bundler" or "nodenext". With "node", the /react17 subpath needs a paths entry (5D)
API keyFrom Revrag, per environment

Your API key and environment are a pair.

  • The SDK talks to https://embed.revrag.ai unless you pass baseUrl.
  • A development key used against production (or the reverse) fails initialization.
  • useInitialize returns the reason in error, and the button never appears.

What it pulls in

  • livekit-client (WebRTC) — loaded only when a call starts.
  • lottie-web — bundled into a separate chunk for the avatar animation.
  • framer-motion — drives the widget's animations on the default entry; the React 17 entry replaces it with a small built-in stub.
  • Expect a noticeable first-load cost. Most of it is the default avatar animation, which ships in the main bundle today.

2. Initialize

Initialize once at the root, identify your user after login, and call EmbedLogout() when they sign out.

  • Call useInitialize once, in a component that stays mounted for the whole session — your app root, or the root layout.
  • Never call it inside a route or page component.
import { useInitialize } from '@revrag-ai/embed-react';

const API_KEY = 'YOUR_API_KEY';
const APP_VERSION = '4.2.0'; // the version your dashboard config is provisioned for

export function App() {
  const { isInitialized, error } = useInitialize(API_KEY, { appVersion: APP_VERSION });

  if (error) console.error('[Revrag] init failed:', error);

  return <Routes />; // your app; see section 3 for the provider
}

appVersion is required.

  • Without it, initialization fails with app_version is required… and the widget never appears.
  • The value must match a version your dashboard knows. Revrag provisions the widget's look and behaviour per platform and per app version.
  • Source it from your build: package.json, a build id, or an environment variable.

Options

OptionDefaultMeaning
apiKey (first argument)—Your tenant's key, for this environment
appVersion—Required. Selects the dashboard config for this version
baseUrlhttps://embed.revrag.aiThe Revrag backend. Omit it in production; pass the matching URL for any other environment
enableTrackertrueClick and route tracking, and an anonymous user id until you send a real one (see below)

What it returns

FieldTypeMeaning
isInitializedbooleanThe handshake with the backend succeeded
isLoadingbooleanThe handshake is in flight
errorstring | nullWhy initialization failed — the only place a failure surfaces

Three things worth knowing:

  • It runs once per API key.
    • Changing options after the first render is ignored.
    • A failed initialization is not retried until the key changes or the component remounts — so do not render the hook before you know appVersion.
    • The handshake gives up after 5 seconds, so on a very slow network it can fail for the rest of that mount.
  • Unmounting it stops tracking.
    • Leaving the component that called useInitialize disposes the SDK's click and route trackers; the button and calls keep working.
    • Keep it at or above the component that renders EmbedProvider.
  • Every version bump is a dashboard change.
    • On the web you deploy often; a deploy that bumps appVersion to a version the dashboard has no config for makes the widget disappear.
    • Put "dashboard config exists for this version" on your release checklist.

Identify your user

After login, and after isInitialized is true, tell the SDK who the user is:

import { useEffect } from 'react';
import { embedEvent, EventKeys } from '@revrag-ai/embed-react';

export function useRevragIdentity(isInitialized: boolean, user: { id: string; name?: string } | null) {
  useEffect(() => {
    if (!isInitialized || !user) return;
    void embedEvent.event({
      eventKey: EventKeys.USER_DATA,
      data: { app_user_id: user.id, name: user.name },
    });
  }, [isInitialized, user?.id]);
}
  • app_user_id is the important one — your stable id for this user. Everything the agent does is attributed to it.
  • Before login, calls still work. With enableTracker on (the default), the SDK registers an anonymous id (user_<timestamp>_<random>) during initialization. Your USER_DATA replaces it.
  • With enableTracker: false, there is no anonymous id: a call started before USER_DATA fails with app_user_id is required.
  • Send it after initialization. Sent earlier, the request fails with SDK not initialized. The id is still stored, but the backend never hears about this user until the next event.

On logout

When your user signs out, call EmbedLogout():

import { EmbedLogout } from '@revrag-ai/embed-react';

async function signOut() {
  await EmbedLogout();
  // …your own sign-out
}

It does four things, in order:

  1. Ends the call, including one that is still connecting.
  2. Stops the agent, cancelling anything it was doing on the page.
  3. Wipes this user's cached screens (IndexedDB revrag_embed_cache). Cached screens are tagged with the user id when they upload, so anything left behind would upload under the next user's id.
  4. Forgets the user id, from memory and from localStorage.

Worth knowing:

  • Your API key and the widget's config stay, so you do not initialize again.
  • Send USER_DATA for the next user as soon as they log in.
  • EmbedLogout() never rejects and is safe to call twice, or when no call is running.
  • Until the next USER_DATA, calls cannot start (app_user_id is required).
    • The anonymous id that initialization creates is only made on a page load.
    • A full reload to your login page restores it, if you want calls to work before login.
  • Same function as React Native, which also exports EmbedLogout().

3. Mount

EmbedProvider is the only component you render. Put it once, around your app.

  • Put it in a component that persists across navigation.
  • Put it inside your router, so it can read the current path.
import { EmbedProvider } from '@revrag-ai/embed-react';
import { useLocation } from 'react-router-dom';

export function RevragShell({ children }: { children: React.ReactNode }) {
  const { pathname } = useLocation();
  return <EmbedProvider currentPath={pathname}>{children}</EmbedProvider>;
}
  • The provider renders the button itself. Do not also render <EmbedButton /> — that mounts a second widget with a second call manager.
  • Mount it once. Two providers (two React roots, a micro-frontend that mounts its own) means two widgets that drift apart.
  • Always pass the path. Without currentPath or usePathHook, the SDK falls back to window.location.pathname:
    • The SDK only re-reads it on browser back/forward.
    • Client-side navigations are missed until something re-renders the provider.

Full prop list

Every prop, with its default:

<EmbedProvider
  // Where the user is — pass ONE of these (section 8)
  currentPath={pathname}                      // string; pathname only, no query or hash
  usePathHook={usePathname}                   // () => string, called as a hook on every render

  // Where the button appears (section 4) — define these OUTSIDE your component
  showOnAllScreens={true}                     // true: excludeScreens apply. false: excludeScreens are ignored
  includeScreens={[]}                         // paths where the button may appear; empty (and no groups) = everywhere
  excludeScreens={[]}                         // paths where it never appears; landing on one ends a call
  matchMode="exact"                           // 'exact' | 'startsWith', for includeScreens
  embedButtonDelayMs={0}                      // entrance delay on every path change
  embedButtonVisibilityConfig={undefined}     // journeys: groups, continuity, per-group delays

  // Action Intelligence
  getScreenName={undefined}                   // () => string | null | undefined — names a screen the URL can't
  actionRootRef={undefined}                   // RefObject<HTMLElement> — narrow what the agent sees
>
  {children}
</EmbedProvider>

The props type is not exported by name; use React.ComponentProps<typeof EmbedProvider> if you need it.

The provider does more than render the button.

  • It keeps the agent's highlight overlay mounted even where the button is hidden, and starts Action Intelligence for the session.
  • It wraps its own UI in an error boundary: if the widget ever fails to render, it hides itself and logs [RevRag] The voice widget crashed and was hidden; your app is unaffected.
  • Your own components are never inside that boundary.

4. Decide where it appears

Choose which paths show the button, which journeys keep a call alive, and any entrance delays.

  • You do not register screens; you decide about paths.
  • A path is location.pathname (or whatever you pass as currentPath), matched exactly and case-sensitively.
import type { EmbedButtonVisibilityConfig } from '@revrag-ai/embed-react';

// Module scope — never inline these in JSX (see the callout below).

// Never show the button on these, and END a live call when the user lands on one.
// Matches the path itself and anything below it: '/settings' also covers '/settings/pin'.
const EXCLUDED = ['/login', '/otp', '/settings'];

// Optional allow-list. Empty means "everywhere not excluded" — but only while you
// define no groups: group screens join this list (see the warning below).
const INCLUDED: string[] = [];

const VISIBILITY: EmbedButtonVisibilityConfig = {
  defaultDelayMs: 0,
  groups: [
    // A journey: the button stays put — no re-delay, and a live call keeps running —
    // while the user moves between these paths.
    { id: 'loan', screens: ['/loans', '/loans/apply', '/loans/review'], continuity: 'continuous' },

    // A per-screen group: re-evaluated on every path change.
    { id: 'help', screens: ['/help'], continuity: 'perScreen', delayMs: 3000, delayPolicy: 'oncePerAppSession' },
  ],
};

<EmbedProvider
  currentPath={pathname}
  excludeScreens={EXCLUDED}
  includeScreens={INCLUDED}
  embedButtonVisibilityConfig={VISIBILITY}
>

What this buys you, with no further code

User doesWidget does
Moves between paths in one continuous groupStays visible — no blink, no re-delay; a live call keeps running
Moves between allowed paths with no delayStays visible; a live call keeps running
Enters a path that has an entrance delayHides for the delay, then appears — a live call ends
Opens a path outside includeScreensHides — a live call ends
Opens a path in excludeScreensHides — a live call ends, and the agent stops seeing the page
Reloads the pageStarts fresh: the call ends; the SDK initializes again

Where the button disappears, the call ends.

  • On the web the call lives inside the button, so any hide ends it — an excluded path, a path outside includeScreens, or an entrance delay.
  • To keep a call alive across a journey, put those paths in one continuous group and give them no delay.
  • This is stricter than the mobile SDKs, where only exclusion ends a call; see §11.

Defining a group turns on the allow-list.

  • Every group's screens are added to includeScreens. So once you define any group, the button appears only on the paths in your groups and in includeScreens.
  • Everywhere else it is hidden, ending a live call. The example above shows the button only on the three loan paths and /help.
  • To keep it on other paths too, add them to includeScreens (with matchMode="startsWith", a section prefix like '/accounts' covers everything below it).

Define includeScreens and embedButtonVisibilityConfig outside your component.

  • The provider re-checks visibility whenever their identity changes.
  • Written inline in JSX, they are new on every render — and with a delay configured, every parent re-render hides the button again, ending a live call.
  • excludeScreens and showOnAllScreens are compared by content, so inline values are harmless there; but a change to them takes effect on the next path change, not immediately.

All options

EmbedProvider visibility props

PropDefaultMeaning
includeScreens[]Paths where the button may appear. Empty means everywhere (that is not excluded). Group screens count as included too
excludeScreens[]Paths where it never appears. Matches the path or anything below it. Wins over includes and groups — unless showOnAllScreens is false, which ignores it
showOnAllScreenstrueDespite the name, it does not force the button on. true: excludeScreens apply. false: excludeScreens are ignored
matchMode'exact'How includeScreens match: 'exact', or 'startsWith' (the path, or anything below it)
embedButtonDelayMs0Entrance delay in ms on every path change, unless a group or defaultDelayMs says otherwise
embedButtonVisibilityConfig—Groups (journeys) and a default delay
  • excludeScreens — landing on one ends a live call and stops the agent seeing the page.
  • showOnAllScreens — effect on the paths the agent may see:
    • true: the agent may see every path except excludeScreens.
    • false: includeScreens (plus group screens) also limits which paths the agent may see — always by prefix, whatever matchMode says.

EmbedButtonVisibilityConfig

FieldDefaultMeaning
defaultDelayMs—Delay for paths that no group sets one for. Wins over embedButtonDelayMs
groups[]Journeys

EmbedButtonGroupConfig

FieldDefaultMeaning
id—Any stable string
screens—The paths in this journey, matched exactly
continuity— (required)'continuous' keeps the button and a live call across the group; 'perScreen' re-evaluates on every path
delayMs—Entrance delay for this group. Wins over defaultDelayMs and embedButtonDelayMs
delayPolicy'perScreen''perScreen' (every path change), 'oncePerGroupEntry' (only when entering the group), 'oncePerAppSession' (once per group until the page reloads or the provider remounts)
  • screens — membership also matches the last path segment, so 'apply' matches /loans/apply — but the button only shows there if the full path is also included.
  • delayPolicy — for 'oncePerAppSession', it counts only once the delay has finished.

Delay precedence: group delayMs → defaultDelayMs → embedButtonDelayMs.

A path in includeScreens that your app never reports does not error — it silently hides the button there.

  • Log the path you pass (§8) and write the lists against it.

5. Your app's shape

Find your app below. Each is complete and copy-pasteable.

Your appGo to
Vite or CRA single-page app with React Router5A
Next.js, App Router5B
Next.js, Pages Router5C
React 17 (any bundler)5D
Preact5E
No router — tabs, steps or wizards in state5F
Several React roots, micro-frontends, iframes, or full page loads5G

5A — Single-page app with React Router

// src/main.tsx — the entire integration
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter, Route, Routes, useLocation } from 'react-router-dom';
import { EmbedProvider, useInitialize } from '@revrag-ai/embed-react';
import '@revrag-ai/embed-react/style.css';

const API_KEY = import.meta.env.VITE_REVRAG_API_KEY as string;
const APP_VERSION = import.meta.env.VITE_APP_VERSION as string;

// 1) Visibility config at module scope (section 4).
const EXCLUDED = ['/login', '/otp'];

// 2) The provider lives INSIDE the router, so it can read the path.
function RevragShell({ children }: { children: React.ReactNode }) {
  const { pathname } = useLocation();
  return (
    <EmbedProvider currentPath={pathname} excludeScreens={EXCLUDED}>
      {children}
    </EmbedProvider>
  );
}

// 3) Initialize once, in the component that never unmounts.
function App() {
  useInitialize(API_KEY, { appVersion: APP_VERSION });
  return (
    <BrowserRouter>
      <RevragShell>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/loans" element={<Loans />} />
          <Route path="/login" element={<Login />} />
        </Routes>
      </RevragShell>
    </BrowserRouter>
  );
}

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>
);
  • Your pages stay exactly what they are — no Revrag code in them.
  • HashRouter works too: pass useLocation().pathname, which is the part after #.

Dynamic segments: useLocation().pathname is the real path.

  • It is /orders/8812, not /orders/:id.
  • Allow a whole subtree with matchMode="startsWith" and includeScreens={['/orders']}.
  • Or exclude it with excludeScreens={['/orders']} (exclusion always covers everything below).

5B — Next.js, App Router

  • "use client" ships inside the package, so the components work from Server Components.
  • useInitialize and usePathname are hooks, so they still need one small client component:
// app/revrag-root.tsx
'use client';

import type { ReactNode } from 'react';
import { usePathname } from 'next/navigation';
import { EmbedProvider, useInitialize } from '@revrag-ai/embed-react';

const API_KEY = process.env.NEXT_PUBLIC_REVRAG_API_KEY as string;
const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION as string;
const EXCLUDED = ['/login'];

export function RevragRoot({ children }: { children: ReactNode }) {
  useInitialize(API_KEY, { appVersion: APP_VERSION });
  const pathname = usePathname();
  return (
    <EmbedProvider currentPath={pathname ?? undefined} excludeScreens={EXCLUDED}>
      {children}
    </EmbedProvider>
  );
}
// app/layout.tsx — a Server Component
import type { ReactNode } from 'react';
import '@revrag-ai/embed-react/style.css';
import { RevragRoot } from './revrag-root';

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <RevragRoot>{children}</RevragRoot>
      </body>
    </html>
  );
}
  • Use the root layout. It persists across navigations, so the provider — and a live call — survive route changes. A provider in a page component remounts on every navigation.
  • Expose the version at build time, e.g. in next.config: env: { NEXT_PUBLIC_APP_VERSION: require('./package.json').version }.
  • pathname is the real path (/orders/8812). For dynamic routes, see the note in 5A.

5C — Next.js, Pages Router

// pages/_app.tsx — the entire integration
import type { AppProps } from 'next/app';
import { useRouter } from 'next/router';
import { EmbedProvider, useInitialize } from '@revrag-ai/embed-react';
import '@revrag-ai/embed-react/style.css';

const EXCLUDED = ['/login'];

export default function MyApp({ Component, pageProps }: AppProps) {
  useInitialize(process.env.NEXT_PUBLIC_REVRAG_API_KEY as string, {
    appVersion: process.env.NEXT_PUBLIC_APP_VERSION as string,
  });
  const router = useRouter();

  return (
    // router.pathname is the route PATTERN — '/orders/[id]' — ideal for screen lists.
    <EmbedProvider currentPath={router.pathname} excludeScreens={EXCLUDED}>
      <Component {...pageProps} />
    </EmbedProvider>
  );
}
  • Use router.pathname, not router.asPath. asPath carries the query string and hash, so it never matches your lists exactly.
  • On Next.js 12 with React 17, import from @revrag-ai/embed-react/react17 (5D), and install with --legacy-peer-deps (§1).

5D — React 17

On webpack hosts — Create React App, Next.js 12, custom webpack — import everything from the React 17 entry:

import { EmbedProvider, useInitialize, embedEvent, EventKeys } from '@revrag-ai/embed-react/react17';
import '@revrag-ai/embed-react/style.css';
  • The API is identical; only the import path changes.
  • The React 17 entry swaps out the animation library (framer-motion), which webpack rejects on React 17, for a small built-in fallback; everything else is the same code.
  • On Vite, the default entry also works on React 17.

With TypeScript on moduleResolution: "node", map the subpath:

{
  "compilerOptions": {
    "paths": { "@revrag-ai/embed-react/react17": ["./node_modules/@revrag-ai/embed-react/index.d.ts"] }
  }
}

5E — Preact

  • Use the regular package through preact/compat.
  • With Vite, there is no extra config. @preact/preset-vite already aliases react, react-dom and react/jsx-runtime to preact/compat:
// vite.config.ts
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';

export default defineConfig({ plugins: [preact()] });
  • Then integrate exactly as in 5A. With Preact Router or preact-iso, pass the current path as currentPath.
  • On webpack, add the three aliases to resolve.alias yourself.
  • npm installs react/react-dom to satisfy the peer range; the alias means they are never bundled.

5F — No router

If your "screens" are state — tabs, wizard steps, a modal-driven flow — give each one a synthetic path:

<EmbedProvider currentPath={`/onboarding/${step}`} excludeScreens={['/onboarding/pin']}>
  <Onboarding step={step} />
</EmbedProvider>

Use stable, privacy-safe values: never a user id, an account number or a session token.

5G — Several roots, iframes, and multi-page apps

  • Several React roots or micro-frontends: exactly one EmbedProvider for the whole page. Render it in the shell that owns navigation; the other roots need no Revrag code.
  • The app runs inside an iframe: the iframe needs allow="microphone", and the agent sees only that iframe's document.
  • Your page embeds iframes: a cross-origin iframe (a payment form, a third-party widget) is one opaque box to the agent. It cannot read or fill it.
  • Multi-page app (full page loads): each page load initializes the SDK again, and a live call ends on every full navigation. Put useInitialize and the provider in a layout shared by every page.

6. Control it from your own code

Start and end calls, and open or close the widget card, from your own code.

Start and end calls

import { startCall, endCall, isCallActive } from '@revrag-ai/embed-react';

<button onClick={() => void startCall()}>Talk to an expert</button>
<button onClick={() => endCall()} disabled={!isCallActive()}>End call</button>
FunctionReturnsBehaviour
startCall()Promise<void>Connects a call. Never rejects, and does nothing if a call is already connecting or live
endCall()voidEnds the call. A no-op when there is none
isCallActive()booleantrue while a call is connected
  • startCall() — call errors:
    • A denied microphone raises microphone_permission_denied.
    • A network or token failure raises no event — it logs Token generation failed to the console.

Three things worth knowing:

  • Call startCall() from a click. The browser shows the microphone prompt then, and may block audio playback for calls started without a user gesture.
  • The button must be on screen. The call lives in the button: on a path where it is hidden, startCall() only warns [RevRag] Cannot startCall: SDK not initialized (no EmbedProvider mounted) and does nothing.
  • The dashboard can start calls too — make sure that is what your product wants.
    • If the dashboard sets collapsedView.inactivityBehavior to auto_trigger, the widget expands and starts a call by itself once the nudge delay (default 1.5 s) has passed after the button appears.
    • It does so whatever the user is doing, and again each time the button reappears.
    • It starts without a click, so the browser may block the microphone prompt or audio.

The widget card

import { expandWidget, collapseWidget, isWidgetExpanded, subscribeWidgetExpanded } from '@revrag-ai/embed-react';

expandWidget();     // open the card; returns true if it changed
collapseWidget();   // back to the floating button; returns true if it changed
isWidgetExpanded(); // boolean

useEffect(() => {
  setOpen(isWidgetExpanded());                              // the listener is not called with the current value
  return subscribeWidgetExpanded((expanded) => setOpen(expanded)); // returns the unsubscribe
}, []);
  • These only change what is shown. They start no call and send no analytics.
  • Before the dashboard config has loaded, calling expandWidget() or collapseWidget() makes the SDK ignore the dashboard's initial collapsed/expanded setting for that page load.

7. Events and analytics

Send your own context to Revrag, and listen to what the widget and the call are doing.

There are two separate systems:

  • Data events — embedEvent.event(…). You send context to Revrag (who the user is, what they did). They go to the backend.
  • Agent events — embedOnAgent(…). The SDK tells you what the widget and the call are doing. They stay in the browser (and are also reported to Revrag).

Sending — embedEvent.event

import { embedEvent, EventKeys } from '@revrag-ai/embed-react';

// Who the user is — after login (section 2).
await embedEvent.event({ eventKey: EventKeys.USER_DATA, data: { app_user_id: 'user-123', email: 'a@b.co' } });

// Something happened in your product. event_name is required for ANALYTICS_DATA.
await embedEvent.event({
  eventKey: EventKeys.ANALYTICS_DATA,
  event_name: 'loan_offer_viewed',
  data: { amount: 500000 },
  context: { screen: '/loans', flow: 'loan_application' },
});

// Free-form context for the agent.
await embedEvent.event({ eventKey: EventKeys.CUSTOM_EVENT, data: { cart_value: 1299 } });
  • As long as you pass data, every embedEvent.event(…) call resolves with { success, error? } and does not throw.
  • Without data, the promise rejects. Pass {} if you have nothing.
KeyWire valueUse it for
EventKeys.USER_DATAuser_dataWho the user is. Must carry app_user_id
EventKeys.ANALYTICS_DATAanalytics_dataNamed product events. Requires event_name
EventKeys.CUSTOM_EVENTcustom_eventFree-form context

Every key except USER_DATA needs a user id — your USER_DATA, or the anonymous one from initialization.

Watch every event in the browser

Register a listener to see the events you send and the ones the SDK raises. removeCallback matches on the same function instance:

import { embedEvent, type CustomEventPayload } from '@revrag-ai/embed-react';

const onEvent = (event: CustomEventPayload) => console.debug('[revrag event]', event.type, event);
embedEvent.addCallback(onEvent);
// later
embedEvent.removeCallback(onEvent);

Agent events — embedOnAgent / embedOffAgent

AgentEventValueFires when
AGENT_VISIBLEagent_visibleThe button becomes visible on the current path
AGENT_CONVERSATION_STARTEDagent_conversation_startedThe agent joins a connected call (a moment after isCallActive() turns true)
AGENT_CONVERSATION_ENDEDagent_conversation_endedA call the agent had joined ends, for any reason. A call that fails before the agent joins raises none
POPUP_MESSAGE_VISIBLEpopup_message_visibleThe inactivity nudge appears
AGENT_TAP_TO_OPEN / AGENT_TAP_TO_CLOSEagent_tap_to_open / agent_tap_to_closeThe user expands or collapses the widget
MICROPHONE_PERMISSION_ALLOWEDmicrophone_permission_allowedMicrophone access granted
MICROPHONE_PERMISSION_DENIEDmicrophone_permission_deniedMicrophone access denied — carries error_message and error_name
GEN_TOOL_TRIGGEREDgen_tool_triggeredReserved; not emitted on web yet
AGENT_CONNECTED / AGENT_DISCONNECTEDagent_connected / agent_disconnectedDeprecated, never emitted. Use the conversation events

Payload:

  • Every event has type, timestamp (ISO string) and session_id, plus its own fields.
  • The conversation events add call_id, reason and callDuration (seconds; 0 on start).

reason on an end is one of:

reasonMeaning
manual_disconnectHung up, or the user landed on an excluded path
destroyedThe button unmounted — see §4
room_disconnectedNetwork or server
participant_disconnectedThe agent left
  • Compare against the AgentEvent constants, never string literals.
  • Agent events are only raised while a user id is stored — your USER_DATA, or the anonymous one.

A drop-in event logger

Paste this once to see every agent event with a readable label. It is the fastest way to watch the widget working, and the hook where you forward events to your own analytics:

// useRevragEventLog.ts
import { useEffect, useRef } from 'react';
import { AgentEvent, embedOnAgent, embedOffAgent, type AgentEventPayload } from '@revrag-ai/embed-react';

const LABELS: Partial<Record<string, string>> = {
  [AgentEvent.AGENT_VISIBLE]: 'button visible',
  [AgentEvent.AGENT_TAP_TO_OPEN]: 'user opened the widget',
  [AgentEvent.AGENT_TAP_TO_CLOSE]: 'user closed the widget',
  [AgentEvent.POPUP_MESSAGE_VISIBLE]: 'nudge shown',
  [AgentEvent.MICROPHONE_PERMISSION_ALLOWED]: 'microphone allowed',
  [AgentEvent.MICROPHONE_PERMISSION_DENIED]: 'microphone DENIED',
  [AgentEvent.AGENT_CONVERSATION_STARTED]: 'call started',
  [AgentEvent.AGENT_CONVERSATION_ENDED]: 'call ended',
};

export function useRevragEventLog(options: { log?: boolean; onEvent?: (e: AgentEventPayload) => void } = {}) {
  const latest = useRef(options);
  latest.current = options;

  useEffect(() => {
    const handle = embedOnAgent((event) => {
      if (latest.current.log) console.info(`[revrag] ${LABELS[event.type] ?? event.type}`, event);
      latest.current.onEvent?.(event);
    });
    return () => embedOffAgent(handle); // one subscription, released on unmount
  }, []);
}
// In your root component, development only:
useRevragEventLog({ log: true });

Gate logging to development.

  • Payloads carry app_user_id and call ids.
  • Do not forward them wholesale to error trackers or third-party analytics.

8. How paths become screen names

Learn where the SDK reads the path from, the rules for the path you pass, and how getScreenName names screens.

The SDK identifies "where the user is" by a path:

SourceWhen it is used
1currentPathYou pass it — recommended
2usePathHook()You pass a hook (e.g. usePathname); called on every render, so pass a stable function and never switch between passing it and not
3window.location.pathnameNothing passed. Only re-read on browser back/forward — client-side navigations are missed

Rules worth knowing:

  • Paths are matched exactly and are case-sensitive. /Loans ≠ /loans, and /loans/ ≠ /loans.
  • Pass only the pathname as currentPath — the button's lists match it exactly, so strip ?… and #… if your router includes them.
  • Never put identifiers in a path you control (/onboarding/${userId}): paths become the agent's screen map and travel to Revrag.
  • getScreenName names the screen the user is on, for when the URL alone can't — a wizard whose steps share one path, or screens chosen by a query parameter.
    • Without it, the screen is the path plus the hash.

    • It takes no arguments: read your own state (or location) and return a name, or null to fall back to the URL:

      getScreenName={() => (pathname === '/apply' ? `/apply/step-${step}` : null)}
    • Its value is the screen's identity, not a display label. The agent's screen map is keyed on it, so two screens must never return the same name, and one screen must not return different names.

    • It runs on every capture, so keep it cheap.

    • It does not affect where the button appears, which always uses the path.

  • Your path is the single source of truth.
    • Once you pass currentPath (or usePathHook), it is the only route the agent, the screen map and Revrag's analytics see. The SDK's own route inference (pushState, replaceState, back/forward, hashchange, Next.js route events, with enableTracker on) is ignored.
    • Without either prop, that inference is the source instead — it reports the path plus hash, and on Next.js the full URL with query, so keep tokens and personal data out of query strings and hashes.

9. Verify it works

Turn on debug logs, run the self-check, and walk through a good first run.

Turn on debug logs

  • The SDK's debug logs are often off, even in development. They are on only when your bundle defines process.env.NODE_ENV as something other than production at runtime — which many browser builds do not.
  • Turn them on in the browser console (no reload needed):
window.__revragDebug = true;   // false turns them off again

What the logs look like:

  • Every line starts with [RevRag] and an area — call, runtime, scope, capture, cache, sync, widget.
  • Messages to and from the agent print as [RevRag ▸ …] (outgoing) and [RevRag ◂ …] (incoming), with one JSON line per message under [RevRag wire].
  • Failures log to the console even with debug off — a failed API request, Token generation failed, the startCall warning.

Run the self-check

import { verifyIntegration } from '@revrag-ai/embed-react/diagnostics';

const result = await verifyIntegration();          // prints a report to the console
console.log(result.ok, result.summary);
  • Run it in development after you have logged in and visited two routes.
  • Pass { offline: true } to skip the checks that make network requests.
IdCheck
V1SDK initialized
V2API key accepted — reads the stored initialization result, so it also fails when sessionStorage is blocked
V3Provider mounted exactly once
V4Widget rendered, or hidden with a reason
V5User identity set
V6Route changes detected (needs two visited routes; reads the screen cache, so it fails when the dashboard turns localCache off, or right after EmbedLogout())
V7Events accepted — sends a real custom_event named revrag_verify_integration
V8Capture healthy — fails only if nothing actionable is captured or the SDK's own UI leaks in; unlabeled controls and truncated containers are notes
V9Masking honored
V10The Revrag backend is reachable (it does not check the call server or the avatar host)
  • Each failing check carries a remediation telling you what to change.
  • Skipped checks: V7 when no user id is stored; V9 when the page has no masked fields.
  • Pass { report: false } to skip the console report.

A good first run

  1. Page load (with window.__revragDebug = true) → [RevRag] … initialization lines; useInitialize returns isInitialized: true, error: null.
  2. Log in → your USER_DATA is sent.
  3. Navigate to an allowed path → the button appears; agent_visible fires.
  4. Tap the button and talk → the microphone prompt (first time) → microphone_permission_allowed → agent_conversation_started.
  5. Navigate inside a continuous group → the call keeps running.
  6. Hang up → agent_conversation_ended with reason: 'manual_disconnect'.
  7. Navigate to an excluded path → the button is gone.

Invalid patterns: a call before initialization; agent_visible never firing; two buttons.


10. Troubleshooting

Find your symptom below, then work through its causes and fixes.

The button never appears

Check these in order — the first three account for most cases.

  1. Did initialization succeed? Log error from useInitialize.
    • app_version is required… → pass appVersion.
    • Any other error → check the API key and baseUrl belong to the same environment, and that the dashboard has a config for this appVersion.
  2. Did you import the stylesheet? Without @revrag-ai/embed-react/style.css the button gets only a minimal fallback style and can look absent.
  3. Does the path match? Log the currentPath you pass. Matching is exact and case-sensitive; a trailing slash counts.
  4. Is the path excluded? excludeScreens beats everything, and covers every path below it.
  5. Is an entrance delay running? A 3000 ms delay looks like "not working" for three seconds.
  6. Did useInitialize run before the provider? The button renders only once initialization has stored its result. (Unmounting useInitialize later only stops tracking; the button stays.)
  7. Is sessionStorage blocked? Strict privacy settings and sandboxed iframes block it; the widget cannot render without it.

The call ends when the user navigates

  • Cause: the button was hidden — and on the web, a hidden button ends the call (§4). Either:
    • the next path is outside includeScreens or excluded
    • the next path has an entrance delay
    • your visibility config is written inline and re-applies its delay on re-render
  • Fix: put the journey in one continuous group, give it no delay, and move the config to module scope.

Two buttons appear

  • Cause: two widgets are mounted — two EmbedProviders (two React roots, a micro-frontend), or a provider and an <EmbedButton />.
  • Fix: keep exactly one provider and no standalone button.

The call does not start

  • No microphone prompt: the page is not a secure context (HTTPS), or it runs in an iframe without allow="microphone".
  • The user denied the microphone: listen for microphone_permission_denied and tell them how to re-enable it in the site settings.
  • app_user_id is required: you set enableTracker: false and have not sent USER_DATA yet.
  • Nothing in particular: turn on debug logs and read the [RevRag] call lines; check your CSP allows the call server over wss: and https:.

startCall() logs "no EmbedProvider mounted"

  • Cause: the button is not mounted on this path (hidden by visibility), so there is nothing to start a call from.
  • In development: with React StrictMode, this can also happen on a path where the button is visible — production is unaffected (§11).

Next.js: "createContext only works in Client Components" / hook errors

  • Cause: you are on 1.4.4, whose files carry no "use client". Fix: upgrade.
  • Cause: you called useInitialize / usePathname from a Server Component. Fix: move them into a client component (5B).

npm ERESOLVE on install

  • Cause: you are on Next.js 12–13 or React Router 5, which the peer range does not include yet.
  • Fix: install with --legacy-peer-deps (or an overrides entry). The SDK imports neither package, so this is safe.
  • More: every out-of-range workaround is in the Dependency & Compatibility Guide.

React 17: errors about useSyncExternalStore or the animation library

  • Cause: you are on a webpack host using the default entry.
  • Fix: import from @revrag-ai/embed-react/react17 (5D).

TypeScript: "Cannot find module '@revrag-ai/embed-react/react17'"

  • Cause: your moduleResolution is "node".
  • Fix: switch to "bundler", or add a paths entry (5D).

Agent events never fire

  • Cause: they are only raised while a user id is stored.
  • Fix: with enableTracker: false, send USER_DATA first.
  • Also check that you compare event.type against AgentEvent constants.

Something else

  1. Turn on debug logs.
  2. Reload.
  3. Reproduce.
  4. Send Revrag:
    • the console output from page load
    • the verifyIntegration() report
    • the SDK version
    • the browser and OS
    • the list of paths you pass as currentPath

11. Known limitations

Each is a current behaviour, its effect, and what to do.

IdLimitationWhat to do
W1Hiding the button ends a call. The call lives in the button, so an entrance delay or a path outside includeScreens ends it, not only excludeScreenscontinuous groups without delays for journeys
W2After EmbedLogout(), calls need a new user id. The anonymous id is only created on a page loadSend USER_DATA after the next login, or reload to your login page (§2)
W3Call errors are not shown in the widget, and only one reaches your code. A denied microphone raises microphone_permission_deniedHandle microphone_permission_denied with your own message; for connection failures, treat "no agent_conversation_started within a few seconds of startCall()" as a failure
W4A failed initialization is not retried until the API key changes or the component remountsRender useInitialize only once you know appVersion
W5sessionStorage is required. Where it is blocked, the widget does not render—
W6startCall() and isCallActive() in development StrictMode may report no provider. Production builds are unaffected—
W7Without currentPath or usePathHook, the button misses client-side navigations, and the agent falls back to the SDK's inferred routesAlways pass one
W8First-load size: the default avatar animation ships in the main bundle—
W9Peer ranges exclude Next.js 12–13 and React Router 5--legacy-peer-deps
W10No automatic PII detection; unmasked text and input values are captured and cached off-call, and click and route tracking ignore maskingMask sensitive fields (data-revrag-mask), keep personal data out of URLs, and disclose to your security reviewer
W11The avatar fetch falls back to corsproxy.io (Revrag's own S3 avatars fall back to /s3-lottie/… on your site instead)Allow it in CSP, or ask Revrag to serve your avatar from a CORS-enabled origin
W12Debug logs are often off in development too (they depend on a runtime process.env.NODE_ENV)window.__revragDebug = true
  • W3 — a failed connection (token, network, a room that drops before the agent joins) raises no event at all — the button simply returns to idle.

12. Checklist

Check off each item for your integration.

Install and serve

  • @revrag-ai/embed-react installed (--legacy-peer-deps on Next.js 12–13 or React Router 5)
  • @revrag-ai/embed-react/style.css imported once at the entry point
  • Served over HTTPS; allow="microphone" on any iframe that hosts the app

Initialize and identify

  • useInitialize(apiKey, { appVersion }) once, in a component that never unmounts, at or above the provider
  • API key and baseUrl from the same environment; production omits baseUrl
  • Dashboard has a widget config (and avatar) for this exact appVersion — re-checked on every release that bumps it
  • error from useInitialize logged
  • USER_DATA with your stable app_user_id sent after login, after isInitialized
  • Sign-out calls EmbedLogout() (§2)

Mount and visibility

  • Exactly one EmbedProvider, in a persistent layout, inside the router; no standalone <EmbedButton />
  • currentPath (or usePathHook) passed — pathname only, no query, hash or identifiers
  • Visibility lists and config defined at module scope, written against the paths your app actually reports
  • Login, OTP, PIN and payment paths in excludeScreens
  • Journeys that must keep a call alive are one continuous group, with no entrance delay

Privacy and CSP

  • Sensitive fields masked (data-revrag-mask) or excluded
  • CSP allows the backend, the call server (wss: and https:), the avatar host and corsproxy.io

Verify and test

  • verifyIntegration() passes in development
  • A real call tested on desktop Chrome and iOS Safari, on the production build and CSP

Reference

Look up package entries, working examples, the backend contract and the other platform guides.

Package@revrag-ai/embed-react — entries ., /react17, /diagnostics, /style.css (also /styles/widget.css)
Working examplesexamples/nextjs · examples/react · examples/react17-smoke · examples/next17-smoke · examples/preact-poc
Backend contractdocs/BE_DATA_CONTRACT.md · docs/WEB_TREE_PAYLOADS.md
Other platformsAndroid · React Native
Versions & pitfallsDependency & Compatibility Guide
Release notesReact SDK release notes
  • Working examples — copy imports from this guide, not from them. Most build against the SDK's source through bundler aliases — including …/dist/style.css, which does not resolve from the published package.
    • examples/nextjs — Next.js, App Router
    • examples/react — React 19 + React Router
    • examples/react17-smoke — React 17
    • examples/next17-smoke — Next.js 12, Pages Router
    • examples/preact-poc — Preact, with Action Intelligence fixtures