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:
- Origin check — drop any message whose
event.originis not in the embed'sallowed_origins. Without this, a sibling iframe on a compromised host can inject events. - Source check — only honor messages whose
sourcefield is the expected name. Unrelated postMessages from third-party scripts are ignored. - embedId match — when multiple iframes share a page, payloads carry an
embedId. The bridge drops any message with a non-matchingembedId. Hosts targeting a specific iframe must include the matchingembedId.
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.