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, refresh_token, and id_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 === 'embedded.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: {
      access_token: 'your-access-token', // From OAuth2 flow
      refresh_token: 'your-refresh-token', // From OAuth2 flow
      id_token: "your-id-token", // From OAuth2 flow
      token_type: "Bearer",
    }
  }, '*');
}
</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 "embedded.ready":
      isReady = true;
      startIntegrationFlow();
      break;
    case "document.generated":
      onDocumentGenerated(eventData.payload.document);
      break;
    case "recording.started":
      console.log("Recording started");
      break;
    case "recording.stopped":
      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", {
    access_token: "your-access-token", // From OAuth2 flow
    refresh_token: "your-refresh-token", // From OAuth2 flow
    id_token: "your-id-token", // From OAuth2 flow
    token_type: "Bearer",
  });
}

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

Events

Corti Assistant dispatches events to notify your application of state changes and important updates. When using the PostMessage API, these events are wrapped in the CORTI_EMBEDDED_EVENT message type.

Event Format Translation

Core events documented in the Events Reference are wrapped for postMessage delivery: Core Event Structure:
{
  "event": "recording.started",
  "confidential": false,
  "payload": {
    "mode": "dictation",
    "language": "en",
    "interactionId": "int_123"
  }
}
PostMessage Wrapper:
{
  "type": "CORTI_EMBEDDED_EVENT",
  "event": "recording.started",
  "confidential": false,
  "payload": {
    "mode": "dictation",
    "language": "en",
    "interactionId": "int_123"
  }
}

Listening for Events

Set up a message listener to receive events from the embedded Assistant:
Listening for Events
const ALLOWED_ORIGINS = [
  "https://assistant.eu.corti.app",
  "https://assistantmd.eu.corti.app",
  "https://assistant.us.corti.app",
];

window.addEventListener("message", (event) => {
  // Validate origin for security
  if (!ALLOWED_ORIGINS.includes(event.origin)) {
    return;
  }

  // Check for Corti events
  if (event.data?.type === "CORTI_EMBEDDED_EVENT") {
    const { event: eventName, confidential, payload } = event.data;

    // Handle different event types
    switch (eventName) {
      case "recording.started":
        console.log("Recording started:", payload);
        break;
      case "recording.paused":
        console.log("Recording paused:", payload);
        break;
      case "document.generated":
        console.log("Document generated:", payload);
        handleDocumentGenerated(payload);
        break;
      case "error.triggered":
        console.error("Error occurred:", payload);
        break;
      default:
        console.log("Unknown event:", eventName, payload);
    }
  }
});

function handleDocumentGenerated(payload) {
  const { documentId, documentName, interactionId } = payload;
  // Update your UI, sync to backend, etc.
}

Available Events

For a complete list of events and their payload structures, see the Events Overview. Common events include:
  • recording.started - Recording has started
  • recording.paused - Recording has paused
  • document.generated - Document has been generated
  • document.updated - Document has been edited
  • document.synced - Document synced to external system
  • error.triggered - An error occurred

Legacy Events

The embedded Assistant also dispatches legacy events using camelCase names (e.g., recordingStarted, documentGenerated). These are deprecated and will be removed in a future version.

Error Handling

Always handle errors when making requests:
Error Handling
try {
  const result = await sendMessage("auth", {
    accessToken: "your-access-token",
    refreshToken: "your-refresh-token",
    id_token: "your-id-token",
    token_type: "Bearer",
  });
  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.