Skip to main content
The PostMessage API enables secure cross-origin communication between your application and the embedded Corti Assistant. This method is recommended for iframe or WebView integrations.
This method is recommended for iFrame/WebView integration. Note, a web component is coming soon that will make this integration easier to use.

Overview

The PostMessage API uses the browser’s postMessage mechanism to enable secure communication between your application and the embedded Corti Assistant, even when they’re served from different origins. This makes it ideal for embedding Corti Assistant in iframes or WebViews.

Requirements

Before getting started, ensure you have:
  • Access to Corti Assistant: You’ll need credentials and access to one of the available regions
  • HTTPS: The embedded Assistant must be loaded over HTTPS (required for microphone access)
  • Microphone permissions: Your application must request and handle microphone permissions appropriately
  • OAuth2 client: You’ll need an OAuth2 client configured for user-based authentication
  • Modern browser or WebView: For web applications, use a modern browser. For native apps, use a modern WebView

Recommendations

  • Validate message origins to ensure security
  • Use specific target origins instead of '*' when possible
  • Implement proper error handling for all API calls
  • Handle authentication token refresh to maintain user sessions
  • Request microphone permissions before initializing the embedded Assistant

Available Regions

Features

  • Secure cross-origin communication
  • Works with any iframe or WebView implementation
  • Fully asynchronous with request/response pattern

Quick Start

Step 1: Set Up Authentication

The Embedded Assistant API only supports user-based authentication. You must authenticate as an end user, not as an application. Client credentials and other machine-to-machine authentication methods are not supported.
Before you can use the PostMessage API, you need to authenticate your users using OAuth2. The recommended flow is Authorization Code Flow with PKCE for secure, user-facing integrations. For detailed information on OAuth2 flows and authentication, see our OAuth Authentication Guide. Key points:
  • Use Authorization Code Flow with PKCE for embedded integrations
  • Obtain access_token and refresh_token for your users
  • Handle token refresh to maintain sessions
  • Never expose client secrets in client-side code

Step 2: Load the Embedded Assistant

Load the Corti Assistant in an iframe or WebView:
<!-- Load the embedded Corti iframe -->
<iframe id="corti-iframe" src="https://assistant.eu.corti.app/embedded" width="100%" height="600px"></iframe>

<script>
const iframe = document.getElementById('corti-iframe');
let isReady = false;

// Listen for the ready event
window.addEventListener('message', async (event) => {
  if (event.data?.type === 'CORTI_EMBEDDED_EVENT' && event.data.event === 'ready') {
    isReady = true;
    console.log('Corti embedded app is ready');

    // Authenticate the user (requires OAuth2 tokens)
    await authenticateUser();
  }
});

async function authenticateUser() {
  // Send authentication request with OAuth2 tokens
  iframe.contentWindow.postMessage({
    type: 'CORTI_EMBEDDED',
    version: 'v1',
    action: 'auth',
    requestId: 'auth-1',
    payload: {
      mode: 'stateful',
      access_token: 'your-access-token', // From OAuth2 flow
      refresh_token: 'your-refresh-token' // From OAuth2 flow
    }
  }, '*');
}
</script>

Message Format

All messages sent to the embedded app follow this structure:
{
  type: 'CORTI_EMBEDDED',
  version: 'v1',
  action: string,
  requestId?: string,
  payload?: object
}

Message Properties

  • type: Always 'CORTI_EMBEDDED'
  • version: API version (currently 'v1')
  • action: The action to perform (see API Reference for all actions)
  • requestId: Optional unique identifier for tracking responses
  • payload: Optional data specific to the action

Response Handling

Responses from the embedded app are sent via postMessage and can be identified by checking the message type:
window.addEventListener('message', (event) => {
  // Handle responses
  if (event.data?.type === 'CORTI_EMBEDDED_RESPONSE') {
    const { requestId, success, data, error } = event.data;
    // Handle response
  }
  
  // Handle events
  if (event.data?.type === 'CORTI_EMBEDDED_EVENT') {
    const { event: eventType, payload } = event.data;
    // Handle event
  }
});

Complete Integration Example

Here’s a complete example showing the recommended integration flow with proper request/response handling:
Example Embedded Integration
// State management
let iframe = null;
let isReady = false;
let currentInteractionId = null;
let pendingRequests = new Map();

// Initialize the integration
function initializeCortiEmbeddedIntegration(iframeElement) {
  iframe = iframeElement;
  isReady = false;
  currentInteractionId = null;

  setupEventListeners();
}

function setupEventListeners() {
  window.addEventListener('message', (event) => {
    // Handle responses
    if (event.data?.type === 'CORTI_EMBEDDED_RESPONSE') {
      handleResponse(event.data);
    }
    
    // Handle events
    if (event.data?.type === 'CORTI_EMBEDDED_EVENT') {
      handleEvent(event.data);
    }
  });
}

function handleResponse(responseData) {
  const { requestId, success, data, error } = responseData;
  const pendingRequest = pendingRequests.get(requestId);
  
  if (pendingRequest) {
    pendingRequests.delete(requestId);
    if (success) {
      pendingRequest.resolve(data);
    } else {
      pendingRequest.reject(new Error(error?.message || 'Request failed'));
    }
  }
}

function handleEvent(eventData) {
  switch (eventData.event) {
    case 'ready':
      isReady = true;
      startIntegrationFlow();
      break;
    case 'documentGenerated':
      onDocumentGenerated(eventData.payload.document);
      break;
    case 'recordingStarted':
      console.log('Recording started');
      break;
    case 'recordingStopped':
      console.log('Recording stopped');
      break;
    // ... handle other events
  }
}

function generateRequestId() {
  return `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}

async function startIntegrationFlow() {
  try {
    // 1. Authenticate
    await authenticate();

    // 2. Configure session
    await configureSession();

    // 3. Create interaction
    const interaction = await createInteraction();

    // 4. Add relevant facts
    await addFacts();

    // 5. Navigate to interaction UI
    await navigateToSession(interaction.id);

    console.log('Integration flow completed successfully');
  } catch (error) {
    console.error('Integration flow failed:', error);
  }
}

function sendMessage(action, payload = {}) {
  return new Promise((resolve, reject) => {
    const requestId = generateRequestId();
    pendingRequests.set(requestId, { resolve, reject });

    iframe.contentWindow.postMessage({
      type: 'CORTI_EMBEDDED',
      version: 'v1',
      action,
      requestId,
      payload
    }, '*');

    // Optional: Add timeout
    setTimeout(() => {
      if (pendingRequests.has(requestId)) {
        pendingRequests.delete(requestId);
        reject(new Error('Request timeout'));
      }
    }, 30000);
  });
}

async function authenticate() {
  // Requires OAuth2 tokens from user authentication
  return sendMessage('auth', {
    mode: 'stateful',
    access_token: 'your-access-token', // From OAuth2 flow
    refresh_token: 'your-refresh-token' // From OAuth2 flow
  });
}

async function configureSession() {
  return sendMessage('configureSession', {
    defaultLanguage: 'en',
    defaultOutputLanguage: 'en',
    defaultTemplateKey: 'corti-soap',
    defaultMode: 'virtual'
  });
}

async function createInteraction() {
  return sendMessage('createInteraction', {
    assignedUserId: null,
    encounter: {
      identifier: `encounter-${Date.now()}`,
      status: "planned",
      type: "first_consultation",
      period: {
        startedAt: new Date().toISOString(),
      },
      title: "Initial Consultation",
    },
  });
}

async function addFacts() {
  return sendMessage('addFacts', {
    facts: [
      { text: "Chest pain", group: "other" },
      { text: "Shortness of breath", group: "other" },
    ]
  });
}

async function navigateToSession(interactionId) {
  return sendMessage('navigate', {
    path: `/session/${interactionId}`
  });
}

function onDocumentGenerated(document) {
  console.log('Document generated:', document);
}

// Usage example:
const iframeElement = document.getElementById('corti-iframe');
initializeCortiEmbeddedIntegration(iframeElement);

Error Handling

Always handle errors when making requests:
Error Handling
try {
  const result = await sendMessage('auth', {
    mode: 'stateful',
    accessToken: 'your-access-token',
    refreshToken: 'your-refresh-token'
  });
  console.log('Authentication successful:', result);
} catch (error) {
  console.error('Authentication failed:', error.message);
  // Handle authentication failure
}

Security Considerations

When using postMessage, always:
  1. Validate message origin: Check event.origin to ensure messages come from trusted sources
  2. Use specific target origins: Replace '*' with the specific origin when possible
  3. Sanitize data: Never trust data from postMessage without validation
Security Best Practices
const ALLOWED_ORIGINS = [
  'https://assistant.eu.corti.app',
  'https://assistantmd.eu.corti.app',
  'https://assistant.us.corti.app'
];

window.addEventListener('message', (event) => {
  // Validate origin
  if (!ALLOWED_ORIGINS.includes(event.origin)) {
    console.warn('Message from untrusted origin:', event.origin);
    return;
  }

  // Process message
  if (event.data?.type === 'CORTI_EMBEDDED_EVENT') {
    // Handle event
  }
});

// Send messages with specific origin
iframe.contentWindow.postMessage({
  type: 'CORTI_EMBEDDED',
  version: 'v1',
  action: 'auth',
  payload: { /* ... */ }
}, 'https://assistant.eu.corti.app'); // Specific origin instead of '*'

Next Steps

Please contact us for help or questions.