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 for simplicity and practicality in embedded scenarios where users are already authenticated in the host application. For production deployments, consider your organization’s authentication requirements.

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 3000
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 5173 (Vite default)
Why this architecture?
  • Security: Credentials stay on the server, never exposed to the browser
  • Separation of concerns: Auth logic separate from UI logic
  • Flexibility: Easy to swap ROPC for other auth methods later
  • 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 instead of addEventListener
  • 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)
  • CORS configured for local Vite dev server
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,
});
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, makes it easy to add caching or error retry logic later. 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
Adapting for React 18? Use useEffect + useState for data fetching instead. The rest of the pattern stays the same. 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 create interaction
  4. 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;

  // Now 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, create interaction, navigate to session
  • onEvent - Log events (or handle specific ones like document.generated)
  • onError - Display errors to user
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:5173 to see the integration in action.

Customization Points

Change Authentication Method

To use PKCE instead of ROPC, modify server.ts to use:
const tokenResponse = await auth.getPkceFlowToken({
  clientId,
  redirectUri,
  code: authorizationCode,
});
See Authentication Guide for PKCE implementation details.

Customize Interaction Creation

Modify the createInteraction call in EmbeddedAssistant.tsx:
const interaction = await api.createInteraction({
  assignedUserId: currentUserId, // Assign to specific clinician
  encounter: {
    identifier: ehrEncounterId, // Your system's encounter ID
    status: "in-progress", // Match your encounter status
    type: "first_consultation", // Match your encounter type
    period: {
      startedAt: encounterStartTime,
      endedAt: encounterEndTime, // For completed encounters
    },
  },
});

Configure Assistant Appearance

Call api.configure() after authentication to customize:
await api.configure({
  features: {
    aiChat: false, // Hide AI chat feature
    navigation: true, // Show navigation sidebar
  },
  appearance: {
    primaryColor: "#00539F", // Your brand color
  },
  locale: {
    interfaceLanguage: "en",
    dictationLanguage: "en-US",
  },
});
See Configuration Reference for all options.

Handle Specific Events

Listen for specific Assistant events:
const handleEvent = (event: CustomEvent) => {
  const { name: eventType, payload } = event.detail;

  switch (eventType) {
    case "document.generated":
      // Save document to your database
      saveDocumentToEhr(payload);
      break;

    case "interaction.created":
      // Track interaction in analytics
      analytics.track("corti_interaction_started", payload);
      break;

    case "recording.stopped":
      // Update UI state
      setRecordingStatus("stopped");
      break;
  }
};
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

Error Handling

Add proper error boundaries and retry logic:
<ErrorBoundary fallback={<ErrorFallback />}>
  <Suspense fallback={<Loading />}>
    <EmbeddedAssistant />
  </Suspense>
</ErrorBoundary>

Token Refresh

Implement token refresh logic in your backend:
// Check token expiration
if (isTokenExpired(accessToken)) {
  // Refresh using refresh token
  const newTokens = await auth.refreshToken(refreshToken);
  // Update tokens in session
}

CORS Configuration

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

Next Steps

This is a foundational integration. Extend it with:
  • Document export - Capture document.generated events and save to your EHR
  • Session management - Load existing interactions using api.navigate("/session/<id>")
  • Advanced configuration - Customize features, appearance, and locale
  • Monitoring - Add logging and analytics around Assistant usage
  • Multi-user support - Handle authentication for multiple clinicians
Advanced integration patterns including document synchronization, session persistence, and multi-tenant setups will be available in Q2 2026. Check the examples repository for updates.

Troubleshooting

CORS Errors

If you see CORS errors, verify Vite’s dev server port matches the cors.origin in server.ts. Vite defaults to 5173 but may use 5174+ if that port is taken. Fix: Update the origin array in server.ts to match your Vite output:
origin: ["http://localhost:5174"]; // Match actual Vite port

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.