Skip to content

postMessage protocol

Status: Current | Last reviewed: 2026-10-07

This page is the contract between the host page and the embedded surface-app iframe. Every message in either direction has a source, a type, and a payload. Both sides must filter messages by source and verify event.origin before trusting any payload.

The implementation is in the repo at apps/surface-app/lib/embed/bridge.ts. The auto-generated type Reference page covers every exported function and type.

Copilot stream events

copilot-event is emitted from the iframe with {embedId, conversationId, event} for every received stream event, including events consumed by card/state rendering. Catalog actions also emit the existing external-event before local handling. These are notifications, not permission to execute a host action. Keep origin, source, embed and record checks on every consumer.

The Embedded SDK exposes sendCopilotEvent({name, data, sourceId, delivery}). It sends an acknowledged copilot-command with command: "event" and the event object. delivery defaults to "stream"; "both" also permits the ordinary card path. The TypeScript SDK offers the same method and subscribeToEvents(callback), which returns an unsubscribe function. Ordinary sendUIAction retains "card" delivery by default.

The acknowledgement proves that the API accepted the event. Each worker's input_mode and event_names decide whether it processes that event; it does not prove a model response or external write. Raw events retain their original shape and type. Application events do not become caller transcript turns.

Envelope

Every message looks like:

{
  "source": "trueserve-surface-app" | "trueserve-dashboard" | "trueserve-gallery" | "trueserve-sfdc" | "trueserve-ai-manager",
  "type": "<event-name>",
  "payload": { ... }
}

The source field tells you where the message originated:

Source Origin
trueserve-surface-app The iframe (any message you receive from surface-app)
trueserve-dashboard A host running the TrueServe internal dashboard
trueserve-gallery The agent gallery preview tool
trueserve-sfdc A Salesforce Lightning host
trueserve-ai-manager The AI Manager test/preview host

If you write a brand-new host, use one of these source names that fits or coordinate a new one — surface-app's allowlist (in bridgeListenForParentMessages) is the gate.

iframe → host

These are the events surface-app emits to the host. Listen with window.addEventListener("message", ...) and check event.data.source === "trueserve-surface-app".

Event Payload When it fires
ready { embedId, conversationId } First message after the iframe boots and the bridge initializes
connection { embedId, isConnected, conversationId } Connection state changes — agent backend reachable / disconnected
error { embedId, message, conversationId? } Recoverable error worth surfacing to the host
conversation-event { embedId, conversationId, event: SurfaceAppEvent } Every turn, action, or A2UI render event during the conversation
copilot-event { embedId, conversationId, event } Complete received stream, including transcripts and events consumed by card/state rendering
external-event { embedId, eventName, sourceId, label?, payload } An A2UI action emitted by a profile-backed catalog component; the host decides whether to handle it

Transcript events retain the conversation API vocabulary: interim_transcript, final_transcript, and refined_transcript, with text and speaker under event.data. They are not renamed to turn. Other stream names include tool_call, UI_RENDER, UI_STATE, and STATUS; use copilot-event when you need the complete feed. See the SSE contract.

The hosted TrueServeEmbed JavaScript SDK maps conversation-event to embed.on("event", callback) and retains embed.on("copilot-event", callback) for the full feed. The outer payload.conversationId is the subscribed UUID; the inner transcript event's numeric conversationId is not an API lookup key. See the live Genesys example.

host → iframe

These are messages the host posts to the iframe. surface-app accepts them only from origins in the embed JWT's allowed_origins claim, and only with one of the four host source names.

Event Payload What it does
context-update { pageContext, componentContext } Tells the agent what the user is currently looking at — page type, label, summary, optional component context
set-view-mode { mode } Changes the iframe's layout: narrow, wide, etc.
set-capabilities { capabilities, hostFamily?, hostVersion? } Host advertises which host_action actions it can perform
set-assist-snapshot { record, context?, objectApiName?, recordId? } Supplies record-backed data used to auto-bind Agent Assist catalog component props
set-event-display { showEvents } Shows or hides tool-call cards and pipeline events in an embedded transcript without reloading the iframe

context-update

type ContextUpdatePayload = {
    pageContext: {
        pageType: string;       // "case-detail", "opportunity-list", ...
        pageLabel: string;      // human-readable
        entityId?: string;      // optional — the record id
        summary: string;        // short natural-language description
    } | null;
    componentContext: {
        componentId: string;
        componentLabel: string;
        summary: string;
    } | null;
};

The summary fields are concatenated and prepended to the agent's system prompt at the next turn. Keep them short (a sentence or two each).

set-view-mode

The mode value is one of the ViewMode union values defined in the surface-app SDK module. Use narrow for a sidebar layout, wide for a docked-bottom layout. surface-app re-renders to match.

set-capabilities

type SetCapabilitiesPayload = {
    capabilities: string[];    // e.g. ["salesforce.emailComposer.open", "dashboard.modal.open"]
    hostFamily?: string;       // "sfdc-lightning", "internal-dashboard", ...
    hostVersion?: string;      // free-form
};

Capability names are namespaced strings. The agent uses this set to decide whether to suggest a host_action-typed tool — without the right capability, the suggestion is hidden from the user. See Host capabilities.

set-event-display

type EventDisplayPayload = {
    showEvents: boolean;
};

AI Manager uses this for the phone-emulator Transcript tab. The iframe can collect pipeline events from page load, while the host toggles whether event markers and tool-call cards appear in the timeline.

When a surface iframe uses an embed token, it may briefly load an auth exchange or redirect document before the final conversation page. Hosts should post this message to the surface-app origin and treat an early postMessage origin mismatch as transient; the iframe's final load event should resend the current display state.

Filtering rules — both sides

The bridge enforces three rules; mirror them in your host code:

  1. Origin check — drop any message whose event.origin is not in the embed's allowed_origins. Without this, a sibling iframe on a compromised host can inject events.
  2. Source check — only honor messages whose source field is the expected name. Unrelated postMessages from third-party scripts are ignored.
  3. embedId match — when multiple iframes share a page, payloads carry an embedId. The bridge drops any message with a non-matching embedId. Hosts targeting a specific iframe must include the matching embedId.

See also

copilot-command

The Embedded SDK exposes updateFormSchema(definition, target?, conversationId?), confirmFormField(name, value, target?, conversationId?), controlForm(control, target?, conversationId?), completeForm(target?, conversationId?), and startNextFormInstance(target?, conversationId?). Each is an acknowledged promise. The TypeScript client's form methods take conversationId? before target?.

Host → iframe, with the same parent-window, allowed-origin and embed-ID checks as other bridge commands:

{
  source: 'trueserve-host',
  type: 'copilot-command',
  payload: {
    embedId, requestId,
    command: 'form_context',
    conversationId, // optional, defaults to the connected conversation
    variables: {
      form_target: { copilot_id: 'forms', form_id: 'intake' },
      active_form: { form_name: 'Delivery', fields: [{ name: 'address', required: true }] }
    }
  }
}

Only active_form, confirmed_fields, form_control, and form_target are accepted by this command. The surface serializes objects through setSessionVariables and its authenticated BFF route. form_control accepts pause, resume, reset, complete, and new_instance. confirmed_fields is an array of {field_name, field_value}. Targets name the configured copilot and form, not a card surface or the pushed form's display label.

Presentation commands use the same envelope: command: 'dismiss' with surfaceId, or command: 'dismissal_policy' with policy: 'manual' | 'on_not_applicable' and optional surfaceId for a per-card override. SDK methods are dismissCopilot(surfaceId) and setCopilotDismissalPolicy(policy, surfaceId?).

Iframe → host: {source: 'trueserve-surface-app', type: 'copilot-command-result', payload: {requestId, ok, error?}}. The SDK correlates the request ID and rejects a failed request, a 15-second timeout, or destruction while pending. The acknowledgement means the API accepted the variables, not that a model run completed. A missing/incorrect target or a disallowed completed-instance transition is ignored by the runtime. Send after connection and transcription startup.

Applicability arrives at the TypeScript client as a UI_RENDER with message_type: 'copilotStatus', surface_id, and payload {applicable:false, reason, preserve_values?}. The SDK emits copilot_status and consumes this message before the A2UI renderer. It only emits a local deleteSurface under the automatic policy, and never does so for preserve_values:true. Manual dismissal emits that same local renderer event without changing form state or deleting persisted events.