Iframe protocol

postMessage protocol.

The chat widget renders inside an iframe and exchanges typed postMessage events with the host page. You don't need to use any of these directly when you're on the React SDK — but if you embed the widget elsewhere, want to forward your own session token, or need a runtime hook, this is the contract.

Every event is namespaced onpilot:*. The shape is always { type: string; data?: unknown }. The widget ignores anything that doesn't match this envelope.

Event catalog

All onpilot:* events

Direction is from the host page's perspective — “iframe → parent” means the widget posts the event up, “parent → iframe” means the host posts down.

EventDirectionPayloadWhen
onpilot:readyiframe → parentAfter the widget exchanges the JWT for a session and is ready to receive parent → iframe messages.
onpilot:messageiframe → parent{ id: string; content: string }A new assistant message has been rendered.
onpilot:closeiframe → parentUser clicked the close (X) button. The host should hide its container.
onpilot:resizeiframe → parent{ height: number }Inline embed only — fired when content height changes so the host can resize the wrapper.
onpilot:identity_tokenparent → iframe{ token: string }Forward a freshly-signed identity JWT. Send on every rotation; the iframe replaces its session.
onpilot:user_contextparent → iframe{ ...userMetadata }Push user-level metadata (plan, role, custom fields) the agent should know about. Persists across messages.
onpilot:page_contextparent → iframe{ url, title, entityType, entityId, metadata }Tell the agent what page / record the user is currently viewing. Pass {} to clear.
onpilot:contextparent → iframeCopilotContextCRM-friendly alias of onpilot:page_context. The OnPilotProvider sends both for backwards compatibility.
onpilot:theme_updateparent → iframe{ theme: 'light' | 'dark' | 'system' }Sync theme when the host changes light/dark mode.
onpilot:smart-suggestionsparent → iframe{ suggestions: string[] }Inject suggested prompts into the welcome screen.
onpilot:ui-flagsparent → iframe{ [flag: string]: boolean }Toggle UI features (e.g. hide footer, hide branding) at runtime.

Reaching the iframe

Talk to the iframe from outside the React SDK

The React components (CopilotBubble, CopilotSidebar, CopilotInline, CopilotPanel) all render the chat in an iframe with a stable title. If you're not using the React SDK, or you're inside a CRM where the SDK is mounted by a parent component you don't own, target the iframe by title.

js
// Reach the OnPilot chat iframe from anywhere on the page.
const iframe = document.querySelector('iframe[title="OnPilot Chat"]');

// Tell the agent which record the user is viewing right now.
// The agent receives this as page_context on every submit.
iframe?.contentWindow?.postMessage(
  {
    type: "onpilot:context",
    data: {
      url: window.location.href,
      title: document.title,
      recordType: "deal",
      recordId: "deal_8421",
      recordName: "Acme — Renewal Q3",
      recordData: { stage: "negotiation", amount: 48000 },
    },
  },
  "*",
);

iframe[title="OnPilot Chat"] is part of the public contract and won't change. If multiple agents are mounted on the same page, query for all of them and broadcast.

Pattern

Wait for ready before sending

The iframe only processes parent → iframe messages after it posts onpilot:ready. If you send earlier, your message is dropped — buffer until ready.

js
let ready = false;
const queue: Array<{ type: string; data?: unknown }> = [];

window.addEventListener("message", (e) => {
  if (e.data?.type === "onpilot:ready") {
    ready = true;
    queue.splice(0).forEach(send);
  }
});

function send(msg: { type: string; data?: unknown }) {
  const iframe = document.querySelector('iframe[title="OnPilot Chat"]') as HTMLIFrameElement | null;
  if (!ready || !iframe?.contentWindow) {
    queue.push(msg);
    return;
  }
  iframe.contentWindow.postMessage(msg, "*");
}

// Now safe to call before or after the iframe loads.
send({
  type: "onpilot:context",
  data: { url, title, recordType, recordId, recordName, recordData },
});

The React SDK does this buffering for you — the snippet above is for non-React hosts.

React

Same protocol, but typed

When you use @onpilot/react, the OnPilotProvider wraps the postMessage protocol. You typically only touch onMessage and onReady props, plus the context prop on CopilotBubble and friends — the provider handles token forwarding, ready buffering, and theme sync internally.

tsx
import { CopilotBubble } from "@onpilot/react";

<CopilotBubble
  identityTokenProvider={async () =>
    fetch("/api/onpilot/token").then((r) => r.text())
  }
  onReady={() => console.log("widget ready")}
  onMessage={(m) => console.log("assistant said", m.content)}
  context={{
    url: window.location.href,
    title: document.title,
    recordType: "deal",
    recordId: dealId,
    recordData: deal,
  }}
/>;