Browse In-App Agent

Revrag Embed — Angular Integration Guide

The complete guide for adding the Revrag AI voice agent to an Angular 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-angular 2.0.0
Angular16 or newer — standalone components or NgModule
Router@angular/router, read automatically; apps without a router work too
SSRAngular Universal / @angular/ssr — the widget renders nothing on the server
TypeScriptTypes included
API keyGet it from https://app.revrag.ai

This guide documents 2.0.0. Coming from 1.0.7? One code change is required — pass appVersion to initialize() — and Angular 15 is no longer supported. Read the upgrade steps first.

1.0.7 predates most of this guide. It has:

  • no appVersion option
  • no Action Intelligence — the agent cannot see or act on your page
  • no call controls (EmbedCallService), no agent events and no EmbedLogout()
  • no /diagnostics entry

Staying on 1.0.7 (for example on Angular 15)? Follow the 1.0.7 guide instead.

Every Angular app integrates in the same three steps — standalone or NgModule, with or without the router:

StepWhat you doDetails
1. InitializeCall EmbedInitService.initialize() once, in your root component§2
2. MountWrap your app in one <revrag-embed-provider>§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, stylesheet, animations, peers and CSP
  2. Initialize — one service call, once, at the root
  3. Mount — one provider, and the SDK follows the user
  4. Decide where it appears — paths, groups, delays, and what the agent can see
  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 and its peers, add the stylesheet, enable Angular animations, and check the requirements.

Package

If your app does not already have @angular/animations and @angular/router, install them first, at the same version as your @angular/core:

npm ls @angular/core                                             # e.g. 19.2.19
npm install @angular/animations@19.2.19 @angular/router@19.2.19  # same version as @angular/core

Then install the SDK and its two runtime peers:

npm install @revrag-ai/embed-angular livekit-client lottie-web
# or
yarn add @revrag-ai/embed-angular livekit-client lottie-web
pnpm add @revrag-ai/embed-angular livekit-client lottie-web
  • Why these first: @angular/animations and @angular/router are peer dependencies. If one is missing, npm 7+ installs it for you at the newest version >=16.0.0 allows. That can be a newer major than the rest of your Angular packages, and the install fails with ERESOLVE. Pinning both to your @angular/core version first avoids it.
  • You need @angular/router installed even if your app has no router. The SDK imports it.
  • The core engine is bundled inside the package. Never install or import @revrag-ai/embed-core.

Stylesheet — required

Add it to the styles array of your build target in angular.json (or project.json in Nx):

"styles": [
  "src/styles.css",
  "node_modules/@revrag-ai/embed-angular/styles/widget.css"
]
  • It goes in projects.<your-app>.architect.build.options.styles.
  • Restart ng serve after changing angular.json. A running dev server does not pick up a new styles entry.
  • Without it, the button looks broken. It gets only a minimal fallback style (position and layout).

Animations — required

The widget uses Angular animations. Without the animations provider, the collapsed button still shows, but Angular throws NG05105: Unexpected synthetic property as soon as the widget opens (or shows its tooltip or agent panel). Only development builds throw; a production build shows no error, so a missing provider can go unnoticed there.

// src/app/app.config.ts — standalone apps
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideAnimations } from '@angular/platform-browser/animations';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes), provideAnimations()],
};

NgModule apps import BrowserAnimationsModule in AppModule instead — see 5B.

Peer dependencies

PackageRangeNotes
@angular/core, @angular/common, @angular/animations, @angular/router>=16.0.0The SDK uses signals, which arrived in Angular 16
rxjs^7.4.0Every Angular 16+ app already has it
livekit-client^2.0.0Voice calls
lottie-web^5.10.0The avatar animation

Your app also needs @angular/platform-browser (for the animations provider). Every Angular CLI app already has it.

Requirements

Angular16 or newer. Angular 15 and older do not build
BrowserAny current browser with WebRTC and getUserMedia
PageA secure context — HTTPS, or localhost in development. Browsers refuse the microphone anywhere else
StoragesessionStorage must be available. The widget reads its configuration from it
RenderingSafe with SSR: the widget renders nothing on the server and appears in the browser (5D)
TypeScriptWorks with "skipLibCheck": false since 2.0.0
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.
  • EmbedInitService.error$ emits the reason, and the button never appears.

Content-Security-Policy

If your app sends a CSP, merge these in (never replace your policy):

DirectiveAllowWhy
connect-srchttps://embed.revrag.ai (or your baseUrl host)Initialization, events, call token, screen map
connect-srcThe call server, over both wss: and https:Voice. Its host is the server_url in each call's token response (/embedded-agent/token); ask Revrag for it. On Revrag's cloud it is usually wss://*.livekit.cloud and https://*.livekit.cloud
connect-srcYour avatar animation's host, if the dashboard sets oneThe SDK fetches the animation file; if it cannot, it shows the bundled orb
img-srcdata:, plus your avatar image's host if the dashboard sets an image avatarThe call icons and the default avatar are bundled as data: images
style-srcNothing new if your app's Angular styles already pass your CSP (nonce or 'unsafe-inline'). 'unsafe-inline' only if widget.css is not loadedThe SDK's component styles are added like your own. A fallback <style> is injected only without widget.css
script-src'unsafe-eval' only if your avatar animation uses Lottie expressionsOtherwise not needed

For a default setup:

connect-src 'self' https://embed.revrag.ai wss://*.livekit.cloud https://*.livekit.cloud;
img-src     'self' data:;
  • Nothing else is fetched from third parties. Since 2.0.0 the call icons are bundled, and there is no corsproxy.io fallback.
  • An avatar animation that cannot be fetched (blocked by CSP or CORS, or missing) shows the bundled default orb instead.
  • Avatars hosted on Revrag's revrag-dev.s3.ap-south-1.amazonaws.com bucket are also tried at /s3-lottie/<path> on your own origin. On localhost that path is tried first, then the S3 URL directly; elsewhere the S3 URL is tried first, then that path. The bundled orb shows only if both fail. Add a proxy for /s3-lottie only if your custom avatar does not load.

What it pulls in

  • livekit-client (WebRTC) — loaded only when a call starts.
  • lottie-web — loaded when the avatar animation first renders.
  • The default avatar animation (about 400 KB) — loaded on demand since 2.0.0, so it no longer counts toward your initial bundle.
  • The Angular CLI warns Module 'lottie-web' … is not ESM. It is harmless; silence it with "allowedCommonJsDependencies": ["lottie-web"] in your build options.

2. Initialize

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

  • Call initialize() once, in your root AppComponent — the component that stays for the whole session.
  • Never call it in a route component or a lazy-loaded module.

Keep the key and version with your other environment settings:

// src/environments/environment.ts (environment.prod.ts: the same fields, with production: true)
export const environment = {
  production: false,
  revragApiKey: 'YOUR_API_KEY',
  appVersion: '4.2.0', // the version your dashboard config is provisioned for
};
// src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { EmbedInitService } from '@revrag-ai/embed-angular';
import { environment } from '../environments/environment';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet],
  template: `<router-outlet></router-outlet>`, // section 3 adds the provider here
})
export class AppComponent implements OnInit {
  constructor(private embedInit: EmbedInitService) {}

  ngOnInit(): void {
    // Every failure lands here, including a missing appVersion.
    this.embedInit.error$.subscribe((error) => {
      if (error) console.error('[Revrag] init failed:', error);
    });

    // Rejects only when appVersion is missing.
    this.embedInit
      .initialize(environment.revragApiKey, { appVersion: environment.appVersion })
      .catch(() => {}); // already logged by error$ above
  }
}
  • No src/environments folder? Run ng generate environments, or keep the values wherever your app keeps its config.

appVersion is required.

  • Without it, initialize() rejects with [RevRag] app_version is required…, error$ emits the same message, and the widget never appears.
  • TypeScript will not catch a missing appVersion — the option is typed as optional. Check it yourself.
  • This option is its only source. The provider has no [appVersion] input, and no <meta> tag or build variable is read.
  • 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 environment.ts.

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)
trackerCallback—Accepted for compatibility; the SDK never calls it. Use embedEvent.addCallback (§7)

What it returns

initialize() returns a Promise<void>:

  • It resolves when the handshake with the backend has finished — whether it succeeded or failed.
  • It rejects only when appVersion is missing. Every failure, that one included, arrives through error$.

EmbedInitService is providedIn: 'root'; inject it anywhere. It exposes the state as observables:

MemberTypeMeaning
isInitialized$Observable<boolean>The handshake with the backend succeeded
isLoading$Observable<boolean>The handshake is in flight
error$Observable<string | null>Why initialization failed — the place every failure surfaces
sessionData$Observable<SessionStorageData>The cached session data
isInitializedbooleanSynchronous snapshot of isInitialized$

Things worth knowing:

  • You do not need to wait for it. The provider shows the button by itself once initialization has stored its result.
  • The handshake gives up after 5 seconds, so on a very slow network it can fail.
    • After a failure, calling initialize() again retries.
    • After a success, calling it again with the same key makes no new request.
    • Calling it with a different key resets the SDK and initializes again.
  • Every version bump is a dashboard change.
    • 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 initialization has succeeded, tell the SDK who the user is. A small service keeps the timing right:

// src/app/revrag-identity.service.ts
import { Injectable } from '@angular/core';
import { filter, take } from 'rxjs';
import { EmbedInitService, embedEvent, EventKeys } from '@revrag-ai/embed-angular';

@Injectable({ providedIn: 'root' })
export class RevragIdentityService {
  constructor(private embedInit: EmbedInitService) {}

  /** Call once after login, with your own stable user id. */
  identify(user: { id: string; name?: string; email?: string }): void {
    this.embedInit.isInitialized$
      .pipe(filter((ready) => ready), take(1))
      .subscribe(() => {
        void embedEvent.event({
          eventKey: EventKeys.USER_DATA,
          data: { app_user_id: user.id, name: user.name, email: user.email },
        });
      });
  }
}
  • app_user_id is the important one — your stable id for this user, across sessions and devices. 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, once per login. 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.
  • The id is kept in the browser's localStorage, so it survives reloads until another USER_DATA or EmbedLogout() replaces it.

On logout

When your user signs out, call EmbedLogout():

// src/app/auth.service.ts
import { Injectable } from '@angular/core';
import { EmbedLogout } from '@revrag-ai/embed-angular';

@Injectable({ providedIn: 'root' })
export class AuthService {
  async signOut(): Promise<void> {
    await EmbedLogout(); // never rejects
    // …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, and drops click events still waiting to be sent for it.

Worth knowing:

  • Your API key, the widget's config and the session's initialization data 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 is only made on a page load; a full reload to your login page restores it.
  • Same function as React and React Native, which also export EmbedLogout().

EmbedInitService.reset() and clearSessionData() are not a logout.

  • reset() disconnects the SDK from the backend — events then fail with SDK not initialized until you call initialize() again.
  • clearSessionData() deletes the stored widget configuration, which hides the button until the page reloads.
  • Neither one forgets the user. Use EmbedLogout().

3. Mount

<revrag-embed-provider> is the only component you place. Put it once, around your app's content.

// src/app/app.component.ts — standalone
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { EmbedProviderComponent } from '@revrag-ai/embed-angular';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, EmbedProviderComponent],
  template: `
    <revrag-embed-provider>
      <router-outlet></router-outlet>
    </revrag-embed-provider>
  `,
})
export class AppComponent {
  // initialize() from section 2
}
  • It follows the router by itself. The provider injects Angular's Router and re-evaluates on every NavigationEnd — you do not need to pass the path.
  • The provider renders the button itself. Do not also place <revrag-embed-button> — that mounts a second, visible button with its own call.
  • Mount it once. Two providers mean two buttons, and the self-check reports it (V3).
  • Keep it in the root component. If the provider is destroyed, its button goes with it, and a live call ends.
  • Wrapping is optional. The provider just shows its content. If your CSS depends on your root's direct children, place <revrag-embed-provider></revrag-embed-provider> next to <router-outlet> instead. It works the same.
  • NgModule apps import EmbedModule instead of the component; the template is the same (5B).

Full input list

Every input, with its default. Most apps set none, or only the visibility ones.

Where the user is (§8)

InputDefaultMeaning
currentPath—string. Overrides the router's path. Set it only without a router, or to report synthetic paths. Pathname only
getScreenName—A function returning a name, or null. Names a screen the URL can't. Does not affect where the button appears

Where the button appears (§4)

InputDefaultMeaning
includeScreens[]string[]. Paths where the button may appear. Empty (and no groups) means everywhere
excludeScreens—string[]. Paths where the button never appears and the agent never looks. Landing on one ends a live call
showOnAllScreenstrueboolean. false: includeScreens also limits what the agent sees, and excludeScreens is ignored
matchMode'exact''exact' or 'startsWith'. How includeScreens match
embedButtonDelayMs0number. Entrance delay in ms on every path change
embedButtonVisibilityConfig—EmbedButtonVisibilityConfig. Journeys: groups, continuity, per-group delays

What the agent sees (§4)

InputDefaultMeaning
actionRootdocument.bodyAn ElementRef or an element. Narrows the part of the page the agent can see

Placement

InputDefaultMeaning
widgetPositioning'fixed''fixed' floats above the page. 'embedded' renders the widget in the normal page flow, after the provider's content, at full width
embedButtonPosition— (24px from the bottom and right){ bottom?, right? }, in 'fixed' mode. A number means px; a string is used as-is. A side you leave out is 24px
widgetSide—'left' or 'right'. Which side to align to, in 'embedded' mode only
widgetBottomOffset0number. Extra px added to the bottom offset, e.g. above a tab bar
widgetClassName''string. Has no effect today
  • A dashboard position replaces the placement inputs. If your Revrag dashboard config sets a position (widgetPosition and paddings), it is used instead of embedButtonPosition, widgetBottomOffset and widgetSide.
  • The user can drag the button to the other edge. The provider has no input to turn that off.
  • Changes to the visibility inputs (includeScreens, excludeScreens, matchMode, delays, groups) reach the button on the next navigation. currentPath and the placement inputs apply at once.
  • What the agent may see is updated at once, but only when includeScreens, excludeScreens or showOnAllScreens change. A changed embedButtonVisibilityConfig reaches the agent only with the next change to one of those three.
  • More on placement: which position wins and embedded position.

The provider does more than render the button.

  • It starts Action Intelligence — the part of the SDK that lets the agent see and act on your page — and reports every route to it.
  • It keeps the agent's highlight overlay mounted, even on paths where the button is hidden.
  • The button appears only after initialize() has succeeded in this browser tab. After a reload it appears straight away, from the config stored in sessionStorage, before the new handshake finishes.

What your dashboard controls

The widget's look and behaviour come from your Revrag dashboard config (widget_config), per app version. Since 2.0.0, fields the config leaves out use these defaults:

BehaviourDefault when the config leaves it outDashboard field
Nudge (tooltip or automatic call)NonecollapsedView.inactivityBehavior (show_popup for the tooltip, auto_trigger for an automatic call) and nudgeDelay (seconds, above 0); the tooltip also needs tooltipMessage
After a call connectsThe open card folds back to the button after 15 s; the call keeps running. Not while captions are oncollapsedView.collapsedTime (seconds)
Agent output panelCloses 15 s after its text has finishedcollapsedView.viewCloseDelay (ms)
Agent name / type"Revrag AI" / "Onboarding Agent"agentTextContent
Call button text"Start Call"agentTextContent.buttonText

The full list of changes is in the release notes.

When to use EmbedButtonComponent

Almost never. <revrag-embed-button> is the bare button, without the provider. On its own it has:

  • no route visibility (includeScreens, excludeScreens, groups, delays)
  • no highlight overlay, so the agent's highlights are invisible
  • no Action Intelligence mount, so the self-check reports no provider (V3)

It exists for the advanced case where you drive Action Intelligence yourself with ActionIntelligenceService.


4. Decide where it appears

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

  • You do not register screens; you decide about paths.
  • A path is the router's URL without ?query and #fragment (or whatever you pass as currentPath), matched exactly and case-sensitively.
// src/app/app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { EmbedProviderComponent, type EmbedButtonVisibilityConfig } from '@revrag-ai/embed-angular';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, EmbedProviderComponent],
  template: `
    <revrag-embed-provider
      [excludeScreens]="excluded"
      [includeScreens]="included"
      [embedButtonVisibilityConfig]="visibility"
    >
      <router-outlet></router-outlet>
    </revrag-embed-provider>
  `,
})
export class AppComponent {
  // Never show the button here, never let the agent see these paths, and END a live call
  // when the user lands on one. Matches the path and anything below it.
  excluded = ['/login', '/otp', '/settings'];

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

  visibility: EmbedButtonVisibilityConfig = {
    defaultDelayMs: 0,
    groups: [
      // A journey: the button stays put — no re-delay, and a live call keeps running.
      { 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' },
    ],
  };
}

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.

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).

All options

Button visibility

InputDefaultMeaning
includeScreens[]Paths where the button may appear. Empty means everywhere. Group screens count as included too
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

What the agent may see

InputDefaultMeaning
excludeScreens—Deny-list. The button hides, capture stops, no screen content is captured or sent, and an active call ends (click and screen-view analytics still report there). Matches the path or anything below it. Wins over includes and groups — unless showOnAllScreens is false, which ignores it
showOnAllScreenstruetrue: the agent may see every path except excludeScreens. false: includeScreens (plus group screens) is also the agent's allow-list — always by prefix, whatever matchMode says — and excludeScreens is ignored
actionRootdocument.bodyNarrows the agent to one region (below)

Pick one mode for the agent — do not mix them. A deny-list (excludeScreens), or an allow-list ([showOnAllScreens]="false" + includeScreens).

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' (until the button has appeared once in this group)
  • 'oncePerAppSession' — it counts only once the delay has finished: leave before it ends, and the delay runs again next time. The memory lasts until the page reloads or the provider is recreated.
  • 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 router's URL (§8) and write the lists against it.

What the agent can see

By default the agent can see document.body, except the SDK's own widget.

Narrow it to one region with actionRoot:

<revrag-embed-provider [actionRoot]="appArea">
  <main #appArea><router-outlet></router-outlet></main>
</revrag-embed-provider>
  • Dialogs and menus stay visible. Elements attached to document.body outside actionRoot are still seen when they are a <dialog>, have aria-modal="true", or have role="dialog", "alertdialog" or "menu" — Angular CDK and Material dialogs, for example.
  • Other overlays do not. Panels with other roles, such as the role="listbox" panels of mat-select and mat-autocomplete, are not seen when they sit outside a narrowed actionRoot. Keep the default root if the agent must use them.
  • While a modal <dialog> is open, the agent sees and acts on that dialog alone.

Mask sensitive fields with data-revrag-mask. The agent still sees the field — its label, its position, that it can be typed into — but its text and value are always blank. It can fill the field for the user, and can never read it back.

<input data-revrag-mask aria-label="Card number" />

<!-- Inherited: masking a container masks everything inside it -->
<section data-revrag-mask>…</section>

<!-- Conditional -->
<div [attr.data-revrag-mask]="isSensitive ? '' : null">…</div>
  • <input type="password"> is never read. No attribute needed.
  • There is no automatic detection. The SDK does not guess what looks like a card or account number. Mark everything sensitive yourself.
  • To keep a whole area away from the agent, leave it outside actionRoot, or keep it on a path in excludeScreens.
  • MASK_ATTR is exported as a constant ('data-revrag-mask') if you set the attribute from code.
  • Give icon-only buttons and unlabeled inputs an aria-label. The agent finds controls by name.

5. Your app's shape

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

Your appGo to
Standalone components (bootstrapApplication) — the Angular 17+ default5A
NgModule app (bootstrapModule)5B
Anything using the Angular Router — hash routing, lazy routes, route parameters5C
Server-side rendering (@angular/ssr, Angular Universal)5D
No router — tabs, steps or wizards in state5E
Several apps on one page, micro-frontends, iframes, or full page loads5F

5A — Standalone components

// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err));
// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideAnimations } from '@angular/platform-browser/animations';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes), provideAnimations()],
};
// src/app/app.component.ts — the entire integration
import { Component, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { EmbedInitService, EmbedProviderComponent } from '@revrag-ai/embed-angular';
import { environment } from '../environments/environment';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, EmbedProviderComponent],
  template: `
    <revrag-embed-provider [excludeScreens]="excluded">
      <router-outlet></router-outlet>
    </revrag-embed-provider>
  `,
})
export class AppComponent implements OnInit {
  excluded = ['/login', '/otp'];

  constructor(private embedInit: EmbedInitService) {}

  ngOnInit(): void {
    this.embedInit.error$.subscribe((error) => {
      if (error) console.error('[Revrag] init failed:', error);
    });
    this.embedInit
      .initialize(environment.revragApiKey, { appVersion: environment.appVersion })
      .catch(() => {}); // already reported through error$
  }
}

Plus the stylesheet in angular.json (§1) and the environment file (§2).

  • Your pages stay exactly what they are — no Revrag code in them.

5B — NgModule app

// src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { EmbedModule } from '@revrag-ai/embed-angular';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, BrowserAnimationsModule, AppRoutingModule, EmbedModule],
  bootstrap: [AppComponent],
})
export class AppModule {}
// src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { EmbedInitService } from '@revrag-ai/embed-angular';
import { environment } from '../environments/environment';

@Component({
  selector: 'app-root',
  standalone: false, // declared in AppModule
  template: `
    <revrag-embed-provider [excludeScreens]="excluded">
      <router-outlet></router-outlet>
    </revrag-embed-provider>
  `,
})
export class AppComponent implements OnInit {
  excluded = ['/login', '/otp'];

  constructor(private embedInit: EmbedInitService) {}

  ngOnInit(): void {
    this.embedInit.error$.subscribe((error) => {
      if (error) console.error('[Revrag] init failed:', error);
    });
    this.embedInit
      .initialize(environment.revragApiKey, { appVersion: environment.appVersion })
      .catch(() => {}); // already reported through error$
  }
}
  • Import EmbedModule once, in the module that declares the component holding the provider.
  • Keep standalone: false on AppComponent. Angular 19 and newer make components standalone by default, and a standalone component cannot be declared in an NgModule. It is valid on 16–18 too.
  • You can skip EmbedModule. EmbedProviderComponent is standalone, so you can list it in your module's imports directly.
  • EmbedModule also exports the widget's inner components (revrag-embed-widget and others) for apps written against 1.0.7. They are deprecated, not removed; use the provider.

5C — Angular Router

Nothing to wire up. The provider listens for the router's NavigationEnd and reports each path.

  • The path is the router URL after redirects, without ?query and #fragment.
  • Hash routing works too (withHashLocation(), or useHash: true in RouterModule.forRoot): the router URL is the part after #, e.g. /loans.
  • Lazy-loaded routes need nothing extra.
  • Put the provider in the root component, not in a routed component — a routed component is destroyed on navigation, taking the button and the call with it.

Route parameters: the router URL 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).

5D — Server-side rendering

The widget is browser-only. On the server, the provider renders no button, and route reporting and Action Intelligence do nothing.

Initialize only in the browser. ngOnInit also runs on the server, so guard the call:

// src/app/app.component.ts
import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { EmbedInitService, EmbedProviderComponent } from '@revrag-ai/embed-angular';
import { environment } from '../environments/environment';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, EmbedProviderComponent],
  template: `
    <revrag-embed-provider [excludeScreens]="excluded">
      <router-outlet></router-outlet>
    </revrag-embed-provider>
  `,
})
export class AppComponent implements OnInit {
  excluded = ['/login', '/otp'];

  constructor(
    private embedInit: EmbedInitService,
    @Inject(PLATFORM_ID) private platformId: object,
  ) {}

  ngOnInit(): void {
    if (!isPlatformBrowser(this.platformId)) return; // ngOnInit also runs on the server
    this.embedInit.error$.subscribe((error) => {
      if (error) console.error('[Revrag] init failed:', error);
    });
    this.embedInit
      .initialize(environment.revragApiKey, { appVersion: environment.appVersion })
      .catch(() => {}); // already reported through error$
  }
}
  • Without the guard, the server would call Revrag's backend on every render and register anonymous users that no browser ever uses.
  • Guard window inside any getScreenName you pass: typeof window !== 'undefined'.
  • Using provideClientHydration()? Test the widget on a hydrated page before you ship.

5E — No router

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

// src/app/onboarding-shell.component.ts
import { Component } from '@angular/core';
import { EmbedProviderComponent } from '@revrag-ai/embed-angular';

@Component({
  selector: 'app-onboarding-shell',
  standalone: true,
  imports: [EmbedProviderComponent],
  template: `
    <revrag-embed-provider [currentPath]="'/onboarding/' + step" [excludeScreens]="excluded">
      <!-- your step UI; update step as the user moves -->
      <button type="button" (click)="step = 'address'">Next</button>
    </revrag-embed-provider>
  `,
})
export class OnboardingShellComponent {
  step = 'details';
  excluded = ['/onboarding/pin'];
}
  • [currentPath] wins over the router whenever it is set.
  • Use stable, privacy-safe values: never a user id, an account number or a session token.
  • Without a router and without [currentPath], the provider reads window.location.pathname once, when it starts, and never again.

5F — Several apps, iframes, and multi-page apps

  • Several Angular apps or micro-frontends on one page: exactly one provider for the whole page. Render it in the shell that owns navigation; the other apps 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.

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

Inject EmbedCallService:

import { Component } from '@angular/core';
import { EmbedCallService } from '@revrag-ai/embed-angular';

@Component({
  selector: 'app-talk-button',
  standalone: true,
  template: `
    <button type="button" (click)="calls.startCall()">Talk to an expert</button>
    <button type="button" (click)="calls.endCall()" [disabled]="!calls.isCallActive()">End call</button>
  `,
})
export class TalkButtonComponent {
  constructor(public calls: EmbedCallService) {}
}
MethodReturnsBehaviour
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 (not while it is still connecting)
  • startCall() — call errors:
    • A denied microphone raises microphone_permission_denied.
    • A token failure (including a network error fetching it) raises no event — it logs Token generation failed to the console.
    • A failure to connect to the call server raises no event and logs nothing from the SDK.

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 with a nudgeDelay above 0, the widget expands and starts a call by itself that many seconds 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.
    • Since 2.0.0 there is no nudge and no automatic call unless the dashboard sets one up.

The widget card

import { Component, NgZone, OnDestroy, OnInit } from '@angular/core';
import {
  expandWidget,
  collapseWidget,
  isWidgetExpanded,
  subscribeWidgetExpanded,
} from '@revrag-ai/embed-angular';

@Component({
  selector: 'app-help-toggle',
  standalone: true,
  template: `<button type="button" (click)="toggle()">{{ open ? 'Close assistant' : 'Open assistant' }}</button>`,
})
export class HelpToggleComponent implements OnInit, OnDestroy {
  open = false;
  private unsubscribe?: () => void;

  constructor(private zone: NgZone) {}

  ngOnInit(): void {
    this.open = isWidgetExpanded(); // the listener is not called with the current value
    this.unsubscribe = subscribeWidgetExpanded((expanded) =>
      this.zone.run(() => (this.open = expanded)),
    );
  }

  toggle(): void {
    if (isWidgetExpanded()) collapseWidget(); // each returns true if it changed anything
    else expandWidget();
  }

  ngOnDestroy(): void {
    this.unsubscribe?.();
  }
}
  • These only change what is shown. They start no call and send no analytics.
  • Wrap your state change in NgZone.run. The listener can be called from outside Angular's zone.
  • 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

Who the user is goes in USER_DATA, after login (§2). For everything else:

// src/app/revrag-events.service.ts
import { Injectable } from '@angular/core';
import { embedEvent, EventKeys } from '@revrag-ai/embed-angular';

@Injectable({ providedIn: 'root' })
export class RevragEventsService {
  // Something happened in your product. event_name is required for ANALYTICS_DATA.
  async loanOfferViewed(amount: number): Promise<void> {
    const result = await embedEvent.event({
      eventKey: EventKeys.ANALYTICS_DATA,
      event_name: 'loan_offer_viewed',
      data: { amount },
      context: { screen: '/loans', flow: 'loan_application' },
    });
    if (!result.success) console.warn('[Revrag] event not sent:', result.error);
  }

  // Free-form context for the agent.
  async cartContext(cartValue: number): Promise<void> {
    await embedEvent.event({ eventKey: EventKeys.CUSTOM_EVENT, data: { cart_value: cartValue } });
  }
}
  • 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.
  • Without context.screen, the event carries the browser's current pathname.
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.
  • Use only these three keys. The other EventKeys values are for the SDK's own use.
  • embedEvent.customEvent(…) sends nothing to Revrag. It only notifies your own addCallback listeners.

embedEvent exposes event, customEvent, addCallback, removeCallback and getStoredAppUserId.

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-angular';

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

Click analytics — data-revrag-name

With enableTracker on (the default), every click on something clickable — a button, link, form control, or an element with a click handler — is also sent to Revrag as an analytics_data event. Nothing to wire up.

The event is named after the control. To choose the name yourself, add data-revrag-name:

<button data-revrag-name="apply_loan">Apply now</button>   <!-- event_name: apply_loan_pressed -->
  • The name is the first of: data-revrag-name, id, name, data-testid, aria-label, aria-labelledby, title, the first words of the visible text, an image's file name, then the role or tag.
  • The suffix says what happened: _pressed for a button or link, _opened / _closed for a dropdown trigger, _selected for an option, select or radio, _toggled for a checkbox.
  • Existing names are stable. A click earlier versions already reported under an id or name (a button, role="button", [onclick] or data-clickable element with an id or name) keeps its exact <id>_pressed name, with or without data-revrag-name. Its old name is also in legacy_event_name.
  • Generated ids are skipped for new names, such as Angular Material's mat-select-0 — so add data-revrag-name to Material controls you care about.
  • A handler delegated from a parent: add data-clickable to the element the user clicks.
  • Privacy: masked fields, password fields, and text that looks like an e-mail, phone, account or card number never become a name or a value. Clicks on the SDK's own widget are never reported.
  • Off switch: enableTracker: false, or the clickTracking flag in your Revrag configuration.

Agent events — embedOnAgent / embedOffAgent

AgentEventValueFires when
AGENT_VISIBLEagent_visibleThe provider shows the button on a path — on every navigation where it is shown, while a user id is stored (so it can be missing on the very first page)
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 (only if your dashboard sets one up)
AGENT_TAP_TO_OPEN / AGENT_TAP_TO_CLOSEagent_tap_to_open / agent_tap_to_closeThe user taps the button to expand or collapse 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 was removed — 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

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

// src/app/revrag-event-log.service.ts
import { Injectable, NgZone, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { AgentEvent, embedOnAgent, embedOffAgent, type AgentEventPayload } from '@revrag-ai/embed-angular';

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',
};

@Injectable({ providedIn: 'root' })
export class RevragEventLogService implements OnDestroy {
  /** Every agent event, inside Angular's zone — subscribe to forward them. */
  readonly events$ = new Subject<AgentEventPayload>();
  log = false;

  private readonly handle: string;

  constructor(zone: NgZone) {
    this.handle = embedOnAgent((event) => {
      if (this.log) console.info(`[revrag] ${LABELS[event.type] ?? event.type}`, event);
      zone.run(() => this.events$.next(event));
    });
  }

  ngOnDestroy(): void {
    embedOffAgent(this.handle); // one subscription, released with the app
  }
}
// In your root component's constructor:
constructor(eventLog: RevragEventLogService) {
  eventLog.log = !environment.production; // log in development only
}

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 it, and how getScreenName names screens.

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

SourceWhen it is used
1[currentPath]You pass a non-empty string — it always wins
2The Angular RouterThe router is present — the default for most apps. The URL after redirects, without ?query and #fragment, on every NavigationEnd
3window.location.pathnameNo router and no currentPath. Read once, when the provider starts

Rules worth knowing:

  • Paths are matched exactly and are case-sensitive. /Loans ≠ /loans, and /loans/ ≠ /loans.
  • Pass only the pathname as currentPath — strip ?… and #… yourself.
  • 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.

    • It takes no arguments. Read your own state and return a name, or null to fall back to the path.

    • Pass an arrow-function property, not a method, so this still works:

      // src/app/app.component.ts
      import { Component } from '@angular/core';
      import { Router, RouterOutlet } from '@angular/router';
      import { EmbedProviderComponent } from '@revrag-ai/embed-angular';
      import { WizardService } from './wizard.service'; // your own state
      
      @Component({
        selector: 'app-root',
        standalone: true,
        imports: [RouterOutlet, EmbedProviderComponent],
        template: `
          <revrag-embed-provider [getScreenName]="screenName">
            <router-outlet></router-outlet>
          </revrag-embed-provider>
        `,
      })
      export class AppComponent {
        constructor(private router: Router, private wizard: WizardService) {}
      
        // An arrow function keeps `this`. Return null to fall back to the path.
        screenName = (): string | null =>
          this.router.url.startsWith('/apply') ? `/apply/step-${this.wizard.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 and free of side effects.

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

  • Your path is the single source of truth. Once the provider reports a path from currentPath or the router, the SDK ignores its own route guesses (history changes, hashchange). That one path is what the agent, the screen map and Revrag's analytics see.

9. Verify it works

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

Turn on debug logs

  • In a normal Angular build, the SDK's debug logs are off — in development too.
  • Turn them on in the browser console (no reload needed):
window.__revragDebug = true;   // false turns them off again
  • A reload turns them off again. To see the logs from page load, set the flag in a <script> in index.html, before your app's bundle.

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

verifyIntegration() lives in a separate entry, @revrag-ai/embed-angular/diagnostics. Call it from a development-only component or service:

// src/app/revrag-self-check.component.ts — development only
import { Component } from '@angular/core';
import { verifyIntegration } from '@revrag-ai/embed-angular/diagnostics';

@Component({
  selector: 'app-revrag-self-check',
  standalone: true,
  template: `<button type="button" (click)="run()">Run Revrag self-check</button>`,
})
export class RevragSelfCheckComponent {
  async run(): Promise<void> {
    const result = await verifyIntegration(); // prints a report to the console
    console.log(result.ok, result.summary);
  }
}
  • Render it only in development — for example behind isDevMode() from @angular/core — and remove it before you ship.
  • Run it after you have logged in, visited two routes and clicked something.
  • Pass { offline: true } to skip the checks that make network requests (V7 and V10).
  • Pass { report: false } to skip the console report.
  • The last result is also stored on window.__REVRAG_VERIFY__, so you can read it from the console after a run.
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. It reads the browser's pathname plus #hash (not the router's path) and checks it only against excludeScreens (plus includeScreens when [showOnAllScreens]="false"). Run it on a screen where the button is visible: on a path hidden by includeScreens, a group or an entrance delay — or with hash routing or currentPath — it can report a false failure
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 capture throws, 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 are not failures and are not counted: V7 when no user id is stored; V9 when the page has no masked fields; V7 and V10 in offline mode.

A good first run

  1. Page load (with window.__revragDebug = true) → [RevRag] … lines; isInitialized$ emits true, error$ stays 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, a live call ends, and the agent stops seeing the page.

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? Subscribe to error$ and catch the initialize() promise.
    • [RevRag] 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 add the stylesheet to angular.json? Without it the button gets only a minimal fallback style and can look absent. Restart ng serve after the change.
  3. Does the path match? Turn on debug logs and read the [RevRag] scope lines, or log router.url. Matching is exact and case-sensitive; a trailing slash counts.
  4. Is the path excluded? excludeScreens beats everything (unless showOnAllScreens is false), 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 you define a group? Group screens turn includeScreens into an allow-list (§4).
  7. Is sessionStorage blocked? Strict privacy settings and sandboxed iframes block it; the widget cannot render without it.
  8. Is the provider in a routed component? Move it to the root component (5C).

NG05105: Unexpected synthetic property / the widget does not open

  • Cause: animations are not enabled.
  • Fix: add provideAnimations() (standalone) or BrowserAnimationsModule (NgModule) — §1.

"Can't bind to … since it isn't a known property of 'revrag-embed-provider'"

  • Cause: the component that uses the tag does not import the provider.
  • Fix: add EmbedProviderComponent to that standalone component's imports, or EmbedModule to its NgModule.

NgModule app: NG6008 "Component AppComponent is standalone, and cannot be declared in an NgModule"

  • Cause: on Angular 19 and newer, components are standalone unless they say otherwise.
  • Fix: add standalone: false to the component's decorator (5B).

The widget appears unstyled

  • Cause: the stylesheet is not in your build.
  • Fix: add node_modules/@revrag-ai/embed-angular/styles/widget.css to angular.json styles, then restart ng serve.

The build fails: signal is not exported by @angular/core

  • Cause: you are on Angular 15 or older.
  • Fix: upgrade Angular to 16 or newer, or stay on 1.0.7 (1.0.7 guide).

npm ERESOLVE on install

  • Cause: @angular/animations or @angular/router was missing, so npm picked a newer major than the rest of your Angular packages. Or your Angular is older than 16.
  • Fix: install @angular/animations and @angular/router at your @angular/core version first, then the SDK (§1).
  • More: the Dependency & Compatibility Guide.

TypeScript: Cannot find module '@revrag-ai/embed-core' (TS2307)

  • Cause: you are on 1.0.7, whose type files import the unpublished core package.
  • Fix: upgrade to 2.0.0. (On 1.0.7, skipLibCheck: true hides 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
    • the provider itself was destroyed (it sits in a routed component)
  • Fix: put the journey in one continuous group with no delay, and keep the provider in the root component.

Two buttons appear

  • Cause: two buttons are mounted — two providers, or a provider and a <revrag-embed-button>.
  • 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 (or called EmbedLogout()) 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.

The avatar shows the default orb, not ours

  • Cause: your avatar animation could not be fetched — your CSP connect-src or the host's CORS rules blocked it.
  • Fix: allow the avatar host in connect-src, or ask Revrag to serve it from a CORS-enabled origin (CSP).

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.

Your UI does not update from an SDK callback

  • Cause: the callback ran outside Angular's zone, so change detection does not see the change.
  • Fix: wrap the state change in NgZone.run(…), as in the widget card and the event logger.

Something else

  1. Turn on debug logs in index.html (a flag set in the console does not survive the reload).
  2. Reload.
  3. Reproduce.
  4. Send Revrag:
    • the console output from page load
    • the verifyIntegration() report
    • the SDK version and your Angular version
    • the browser and OS
    • the list of paths your app reports

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 it with your own message; for connection failures, treat "no agent_conversation_started within a few seconds of startCall()" as a failure
W4sessionStorage is required. Where it is blocked, the widget does not render—
W5The provider has no inputs to turn off dragging or set the auto-collapse time. In 'fixed' mode the button is always draggableSet the collapse time on the dashboard (collapsedView.collapsedTime)
W6trackerCallback and widgetClassName do nothingUse embedEvent.addCallback; style the page around the widget instead
W7No automatic PII detection; unmasked text and input values are captured and cached off-callMask sensitive fields (data-revrag-mask), keep personal data out of URLs, and disclose to your security reviewer
W8An avatar animation that cannot be fetched shows the bundled orb, not your avatarServe the animation from a CORS-enabled origin your CSP allows
W9Debug logs are off by default, in development toowindow.__revragDebug = true
W10Without a router and without currentPath, the path is read once and never updatesPass currentPath (5E)
W11Zoneless Angular is untestedKeep zone.js
  • 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

  • Angular 16 or newer
  • @angular/animations and @angular/router installed at your @angular/core version, then @revrag-ai/embed-angular, livekit-client and lottie-web
  • node_modules/@revrag-ai/embed-angular/styles/widget.css in angular.json styles
  • provideAnimations() or BrowserAnimationsModule
  • Served over HTTPS; allow="microphone" on any iframe that hosts the app

Initialize and identify

  • EmbedInitService.initialize(apiKey, { appVersion }) once, in the root component (browser only, with SSR)
  • 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$ logged and the initialize() promise caught
  • USER_DATA with your stable app_user_id sent after login, after isInitialized$ is true
  • Sign-out calls EmbedLogout() (§2)

Mount and visibility

  • Exactly one <revrag-embed-provider>, in the root component; no standalone <revrag-embed-button>
  • Router-less apps pass currentPath — pathname only, no query, hash or identifiers
  • Visibility lists 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
  • Nudge and auto-call settings on the dashboard are what your product wants

Privacy and CSP

  • Sensitive fields masked (data-revrag-mask), or kept outside actionRoot or on excluded paths
  • Icon-only buttons and unlabeled inputs have an aria-label — the agent finds controls by name
  • CSP allows the backend, the call server (wss: and https:) and your avatar host (§1)

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 the public API, package entries, working examples and the other guides.

Package@revrag-ai/embed-angular — entries ., /diagnostics, /styles/widget.css
AI coding assistantsAngular Copilot integration · the package also ships node_modules/@revrag-ai/embed-angular/revrag-integrate.md
Working exampleexamples/angular in the SDK repository — Angular 19, standalone, four visibility patterns
Versions & pitfallsDependency & Compatibility Guide
More patternsAdvanced provider usage
Release notesAngular SDK release notes
Older version1.0.7 guide
Other platformsReact · Android · React Native

Public API

Everything you need. Anything not listed here or under Deprecated is internal and may change without notice.

import {
  EmbedProviderComponent,     // <revrag-embed-provider> — mount once (section 3)
  EmbedButtonComponent,       // <revrag-embed-button> — only without the provider
  EmbedModule,                // for NgModule apps (section 5B)
  EmbedInitService,           // initialize, isInitialized$, isLoading$, error$ (section 2)
  EmbedCallService,           // startCall, endCall, isCallActive (section 6)
  EmbedLogout,                // on sign-out (section 2)
  ActionIntelligenceService,  // drive Action Intelligence without the provider (advanced)
  embedEvent, EventKeys,      // send events (section 7)
  expandWidget, collapseWidget, isWidgetExpanded, subscribeWidgetExpanded, // the card (section 6)
  AgentEvent, embedOnAgent, embedOffAgent,                                  // agent events (section 7)
  MASK_ATTR,                  // 'data-revrag-mask'
} from '@revrag-ai/embed-angular';

import type {
  EmbedButtonVisibilityConfig, EmbedButtonGroupConfig, EmbedButtonContinuity,
  EmbedButtonDelayPolicy, EmbedButtonPosition,
  SDKConfig, EventKey, CustomEventData, CustomEventPayload,
  AgentEventPayload, AgentEventHandler, ActionScopeInputs,
} from '@revrag-ai/embed-angular';

import { verifyIntegration, integrationSnapshot } from '@revrag-ai/embed-angular/diagnostics';
import type { VerifyResult, VerifyCheck, VerifyOptions } from '@revrag-ai/embed-angular/diagnostics';
  • embedEvent exposes only event, customEvent, addCallback, removeCallback and getStoredAppUserId.
  • integrationSnapshot() returns the runtime state without running any checks.
  • EmbedInitService also has reset(), clearSessionData(), getStorageInfo() and sessionData$. You do not need them for a normal integration — and see On logout before using the first two.

Deprecated, still working (exported for apps written against 1.0.7; to be removed in a later major version): EmbedWidgetComponent, AnimatedButtonComponent, ExpandedViewComponent, CallTextPanelComponent, TooltipComponent, WaveformVisualizerComponent, TypewriterComponent, EmbedLiveKitService, InteractionWatcherService, PlatformService, WidgetConfigService, resolveColorPalette, resolveAnimationData, resolveWidgetPosition, deriveWidgetConfig, and the types WidgetConfig, WidgetSide, ResolvedWidgetPosition, EventPayload. See the release notes.