Skip to main content
This guide walks you through integrating Corti Assistant into a React web application. You’ll learn the key concepts, architecture patterns, and implementation approach using the Web Component API.
Complete working example available: This guide references the basic React example in our examples repository. Clone it to see a fully functional implementation, then follow this guide to understand how it works and adapt it to your needs.
This guide uses ROPC (Resource Owner Password Credentials) authentication, matching the React basic example.

What you’ll learn

By following this guide, you’ll understand:
  • How to architect a secure client-server integration
  • Why ROPC authentication works well for embedded EHR scenarios
  • How to use the Web Component API with React
  • How to manage the interaction lifecycle programmatically
  • Key customization points and production considerations

Prerequisites

Before starting, ensure you have:
  • Node.js 18+ installed
  • Corti API access with valid credentials
  • OAuth client configured for ROPC (Resource Owner Password Credentials) flow
  • User account created in Corti Console for authentication
  • Basic familiarity with React and Express
Don’t have credentials yet? Visit Corti Console to create a user and OAuth client. Ensure your OAuth client has “Direct Access Grants” enabled for ROPC flow.

Architecture overview

The integration follows a client-server architecture: Backend (Express Server):
  • Handles OAuth authentication using ROPC flow via @corti/sdk
  • Provides configuration endpoint for environment-specific settings
  • Keeps sensitive credentials server-side
  • Runs on port 8013 by default
Frontend (React + Vite):
  • Fetches auth tokens and configuration from backend
  • Embeds Corti Assistant using @corti/embedded-web Web Component
  • Manages interaction lifecycle (create, navigate, handle events)
  • Uses React 19’s Suspense for async data loading
  • Runs on port 8015, with /api proxied to the backend
Why this architecture?
  • Security: Credentials stay on the server, never exposed to the browser
  • Separation of concerns: Auth logic separate from UI logic
  • Development speed: Simple to run and test locally

Key concepts

Authentication flow

  1. Backend authenticates with Corti using stored credentials (ROPC)
  2. Frontend requests tokens from backend /api/auth endpoint
  3. Web Component receives tokens and authenticates the embedded session
  4. No user interaction required - seamless experience
Why ROPC? Users are already logged into your application. Forcing them to re-authenticate with Corti via browser popup would be confusing and disruptive. ROPC allows seamless embedded authentication.

Web Component integration

The integration uses <CortiEmbeddedReact>, a React wrapper around the standard Web Component:
import {
  CortiEmbeddedReact,
  useCortiEmbeddedApi,
} from "@corti/embedded-web/react";

// In your component:
<CortiEmbeddedReact
  ref={cortiRef}
  baseURL={baseUrl}
  visibility="visible"
  onReady={handleReady}
  onEvent={handleEvent}
  onError={handleError}
/>;
Key benefits:
  • Type-safe: Full TypeScript support with IDE autocomplete
  • React-friendly: Hooks-based API (useCortiEmbeddedApi)
  • Event handling: Native React event props for Assistant callbacks
  • Lifecycle management: Automatic cleanup and ref management

Interaction lifecycle

Every clinical encounter in Corti Assistant is called an “interaction”:
  1. Create interaction - Define encounter metadata (type, identifier, timestamps)
  2. Navigate to session - Load the session view with recording interface
  3. User records - Clinician documents the encounter
  4. Handle events - React to document generation, errors, state changes
Why create interactions programmatically? This allows you to:
  • Link Corti sessions to your EHR’s encounter IDs
  • Pre-fill encounter metadata from your system
  • Control when and how sessions start
  • Track interactions in your database

Implementation guide

Project structure

your-react-app/
├── server.ts              # Backend: Auth + config endpoints
├── src/
│   ├── App.tsx            # Root: Suspense boundary + data fetching
│   ├── components/
│   │   └── EmbeddedAssistant.tsx  # Main integration component
│   ├── lib/
│   │   └── auth.ts        # API calls to backend
│   └── types.ts           # TypeScript definitions
├── .env                   # Credentials (never commit!)
└── package.json

Step 1: Backend setup

Purpose: Securely authenticate with Corti and provide tokens to the frontend. Key file: server.ts What it does:
  • /api/auth - Uses @corti/sdk to perform ROPC authentication
  • /api/config - Returns base URL based on environment (EU/US)
  • /api/health - Provides a simple backend health check
  • CORS configured for the local Vite dev server origin
  • Backend port defaults to 8013; frontend origin defaults to http://localhost:8015
Implementation approach:
// Uses Corti SDK for authentication
const auth = new CortiAuth({
  environment: process.env.CORTI_ENVIRONMENT,
  tenantName: process.env.CORTI_TENANT_NAME,
});

// ROPC flow - exchanges username/password for tokens
const tokenResponse = await auth.getRopcFlowToken({
  clientId: process.env.CORTI_CLIENT_ID,
  username: process.env.CORTI_USER_EMAIL,
  password: process.env.CORTI_USER_PASSWORD,
});

// Return credentials in the shape expected by api.auth()
res.json({
  access_token: tokenResponse.accessToken,
  refresh_token: tokenResponse.refreshToken,
  id_token: "",
  token_type: tokenResponse.tokenType || "Bearer",
  expires_in: tokenResponse.expiresIn,
  mode: "stateful",
});
See the full implementation: server.ts in example repo

Step 2: Frontend data fetching

Purpose: Fetch auth tokens and config before rendering the Assistant. Key file: src/lib/auth.ts What it does:
  • Promise-based API calls to backend
  • Simple in-memory caching to prevent duplicate requests
  • Separates data fetching logic from UI components
Why separate? Keeps components focused on rendering and keeps backend communication in one place. See the implementation: auth.ts in example repo

Step 3: App root with suspense

Purpose: Coordinate parallel data fetching and handle loading states. Key file: src/App.tsx Key pattern: React 19’s use() hook with Suspense for async data
function AppWithData({ configPromise, authPromise }) {
  const config = use(configPromise); // Suspends until resolved
  const authData = use(authPromise); // Fetched in parallel

  if (authData.error) {
    return <ErrorDisplay />;
  }

  return <EmbeddedAssistant baseUrl={config.baseUrl} authData={authData} />;
}
Why this pattern?
  • Parallel fetching: Config and auth load simultaneously
  • Declarative loading: Suspense fallback handles loading UI automatically
  • Error boundaries: Easy to add error handling at the boundary level
See the implementation: App.tsx in example repo

Step 4: Embedded Assistant component

Purpose: Render the Web Component and manage the interaction lifecycle. Key file: src/components/EmbeddedAssistant.tsx Core responsibilities:
  1. Render <CortiEmbeddedReact> with configuration
  2. Use useCortiEmbeddedApi hook to access API methods
  3. Handle onReady event to authenticate and configure the embedded app
  4. Set interaction-level options before creating the interaction
  5. Create the interaction and navigate to the session route
  6. Manage status display and error states
Critical pattern - preventing double initialization:
const hasInitialized = useRef(false);

const handleReady = async () => {
  if (hasInitialized.current) return; // Guard against React StrictMode
  hasInitialized.current = true;

  // Safe to authenticate and create interaction
};
Why needed? React 19’s StrictMode intentionally double-invokes effects in development. Without this guard, you’d create two interactions. Event handling strategy:
  • onReady - Authenticate, configure app UI, set interaction options, create interaction, navigate to session
  • onEvent - Receive Assistant events for logging or workflow integration
  • onError - Display errors to user
API sequence:
await api.auth(authData);

await api.configureApp({
  ui: {
    interactionTitle: true,
    aiChat: true,
    documentFeedback: true,
    navigation: true,
  },
});

await api.setInteractionOptions({
  mode: {
    fallback: "in-person",
    options: ["in-person", "virtual"],
  },
  documents: {
    actions: {
      sync: true,
    },
  },
});

const interaction = await api.createInteraction({
  assignedUserId: null,
  encounter: {
    identifier: `encounter-${Date.now()}`,
    status: "planned",
    type: "first_consultation",
    period: { startedAt: new Date().toISOString() },
  },
});

await api.navigate(`/session/${interaction.id}`);
See the implementation: EmbeddedAssistant.tsx in example repo

Running the example

Clone and run the complete example:
git clone https://github.com/corticph/corti-examples.git
cd corti-examples/embedded-assistant/react/basic-example
npm install
cp .env.example .env
# Edit .env with your credentials
npm run dev
Open http://localhost:8015 to see the integration in action. The backend runs at http://localhost:8013, and Vite proxies /api requests to that backend.

Customization points

Interaction creation

The example creates an interaction in EmbeddedAssistant.tsx after authentication and configuration:
const interaction = await api.createInteraction({
  assignedUserId: null,
  encounter: {
    identifier: `encounter-${Date.now()}`,
    status: "planned",
    type: "first_consultation",
    period: {
      startedAt: new Date().toISOString(),
    },
  },
});

Assistant configuration

The example calls api.configureApp() after authentication for app-level UI settings:
await api.configureApp({
  ui: {
    interactionTitle: true,
    aiChat: true,
    documentFeedback: true,
    navigation: true,
  },
});
The example calls api.setInteractionOptions() for interaction-level options:
await api.setInteractionOptions({
  mode: {
    fallback: "in-person",
    options: ["in-person", "virtual"],
  },
  documents: {
    actions: {
      sync: true,
    },
  },
});
See Configuration Reference for all options.

Event and error handling

The example wires onEvent and onError callbacks on <CortiEmbeddedReact>:
const handleEvent = () => {
  // Events are logged internally by the component
};

const handleError = (event: CustomEvent) => {
  setStatus({
    message: `Error: ${event.detail?.message || "Unknown error"}`,
    type: "error",
  });
};
See Events Reference for all available events.

Production considerations

Environment variables

Never commit credentials to version control:
# Use environment-specific .env files
.env.development  # Local dev credentials
.env.production   # Production credentials (injected via CI/CD)

# Add to .gitignore
echo ".env*" >> .gitignore
echo "!.env.example" >> .gitignore

CORS configuration

Update CORS for production domains:
app.use(
  cors({
    origin: [
      "https://your-production-domain.com",
      process.env.CLIENT_ORIGIN || (process.env.NODE_ENV === "development" ? "http://localhost:8015" : null),
    ].filter(Boolean),
    credentials: true,
  }),
);

Next steps

Build on the React basic example with the current Embedded API:
  • Document export - Handle document events and save generated content to your host application
  • Session management - Store created interaction IDs and navigate back with api.navigate("/session/<id>")
  • App configuration - Use api.configureApp() for UI, appearance, locale, and network settings
  • Interaction options - Use api.setInteractionOptions() for mode, document actions, spoken language, and templates
  • Event monitoring - Use onEvent and onError to track Assistant activity in your application

Troubleshooting

CORS errors

If you see CORS errors, verify Vite’s dev server origin matches the CLIENT_ORIGIN used by server.ts. The example uses strict port 8015 for Vite and port 8013 for the backend. Fix: Set CLIENT_ORIGIN to match your frontend URL:
CLIENT_ORIGIN=http://localhost:8015

Authentication fails

  • Verify credentials in .env match your Corti Console setup
  • Ensure OAuth client has “Direct Access Grants” enabled for ROPC
  • Check user account exists and is active in your Corti tenant
  • Verify CORTI_ENVIRONMENT matches your tenant region (eu/us)

Web Component not loading

  • Check browser console for import errors
  • Verify @corti/embedded-web package is installed correctly
  • Ensure baseURL matches your region (https://assistant.eu.corti.app)
  • Confirm microphone permissions are granted (required for recording)

Double initialization in Dev Mode

This is expected with React 19 StrictMode. The hasInitialized ref guard prevents issues. In production builds, StrictMode is disabled and this won’t occur.