Developer

iFrame

The simplest way to embed a Trulience avatar in your website

Adding an Avatar iFrame to a Web Page

Trulience avatars can be embedded in a web page using an iFrame:

<iframe 
    height="600px"
    src="https://www.trulience.com/avatar/<your-avatar-id>"
    allow="camera; microphone; fullscreen; accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
    frameborder="0" 
    allowfullscreen>
</iframe>

Here’s how this embed code looks in action:

Authentication

By default, newly created avatars are public. To restrict access to your avatar, you can make it private and require a JWT token for authentication.

To set up private avatars:

  1. Make your avatar private in the dashboard (Avatar Creator → Edit → ADVANCED tab → Privacy dropdown)
  2. Create an API key with the “Generate Tokens” permission
  3. Generate tokens from your backend using the /auth/generate-token endpoint

See the Security documentation for full setup instructions.

Once you have a token, append it to the iFrame’s src attribute:

<iframe src="https://www.trulience.com/avatar/<your-avatar-id>?token=<your-token>"></iframe>

If your avatar is public, you can omit the token from the URL.

Configuring the UI

You will notice that the avatar embed comes with some inbuilt UI. You can configure this UI by appending query parameters to the avatar URL or by editing your avatars’ client JSON configuration via the dashboard.

Configuration Parameters

The following parameters can be used to configure the behaviour of the iframe:

ParameterDescription
connectIf set to true, the client will connect automatically when visiting the avatar link - bypassing the ‘dial’ screen
micOffIf set to true, the microphone will be muted by default
speakerOffIf set to true, the speaker will be muted by default
hideFSIf set to true, hides the full screen button
hideChatInputIf set to true, hides the chat input box
hideChatHistoryIf set to true, hides the chat history
hideLetsChatBtnIf set to true, hides the dial button
hideMicButtonIf set to true, hides the microphone button preventing the users from being able to mute/unmute
hideHangUpButtonIf set to true, hides the hang up button
hideSpeakerButtonIf set to true, hides the speaker button
dialButtonTextText that appears on the dial button
msgOnConnectMessage (string) sent to the avatar on connection
screenAspectRatioSets the aspect ratio of the visible video area e.g. 16:9, 4:3, 1:1 etc.
chatInputBoxWidthCan be set to the width of the visible video area above, using window
showLogoIf set to true, the client will show either the default Trulience logo or the provided logoSrc
logoPositionCan be right or left
logoSrcThe https location of the logo image to be displayed in top right or left of visible video window
registerTrlEventsList of events that should be notified to iframe’s parent as and when they occur
fullscreenIf set to true, the avatar will be displayed in fullscreen mode
disableDraggingIf set to true, dragging to resize the avatar will be disabled
tokenProvide a JWT token to authenticate access to private avatars. See Security for setup.
controlButtonPositionCan be center, right or left
hideToastIf set to true, the client will not display toast alerts

Adjusting Colour

Here’s a list of colour attributes you can adjust using CSS colour strings, e.g. #ffffff or pink:

  • dialPageBackground
  • dialButtonTextColor
  • dialButtonBackground
  • chatScreenBGColor
  • userChatBubbleBGColor
  • avatarChatBubbleBGColor
  • userChatBubbleBorderColor
  • avatarChatBubbleBorderColor
  • userChatBubbleTextColor
  • avatarChatBubbleTextColor
  • inputBoxBGColor
  • inputBoxBorderColor
  • inputBoxTextColor
  • sendButtonBGColor
  • sendButtonArrowColor
  • sendButtonBorderColor
  • borderColorBetweenInputAndScreen
  • overlayButtonColor
  • loadingBarColor

Example

Here’s an example, demonstrating styling using query params:

<iframe 
    height="700px" 
    allow="camera; microphone; fullscreen" 
    src="https://www.trulience.com/avatar/<id>?chatScreenBGColor=#e7e7e7&userChatBubbleBGColor=#ff6200&avatarChatBubbleBGColor=#000000&userChatBubbleBorderColor=none&avatarChatBubbleBorderColor=none&userChatBubbleTextColor=white&avatarChatBubbleTextColor=white&inputBoxBGColor=#fff&inputBoxBorderColor=#fff&inputBoxTextColor=inherit&sendButtonBGColor=white&sendButtonArrowColor=#000000&sendButtonBorderColor=none&borderColorBetweenInputAndScreen=#f0f0f0" 
    frameborder="0"
    allow="camera; microphone; fullscreen; accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
    allowfullscreen>
</iframe>

iFrame messaging

Behind the scenes, the iFrame embed is loading the Trulience SDK. You can interact with the SDK through iFrame messaging.

Listening to SDK Events

You can register which SDK events you want to listen to by appending them to your iFrame’s src attribute via a query param, for example:

<iframe src="https://www.trulience.com/avatar/<id>?registerTrlEvents=auth-success,auth-fail,mic-update,speech-recognition-start,speech-recognition-end,speech-recognition-final-transcript" />

Extra iFrame Events

On top of the existing SDK events, the iframe also dispatches the following events:

Event NameDescriptionParams
trl-chatFired when a chat message is received from the server.{message, agentName, sttResponse, senderType}
trl-respond-avatar-photo-urlFired in response to the trl-request-avatar-photo-url request from the iframe’s parent.URL pointing to the photo of the avatar.

Please refer to our SDK documentation to find out more about events like auth-success.

Example

You can listen to iFrame events like this:

window.addEventListener("message", (event) => {
    
    // Verify event.origin as necessary.
    if (event.origin) {
        // Test the origin here and return if it does not match the expected value.
    }

    if (event.data !== null && event.data !== undefined) {
        let eventData = event.data;
        console.log("Event name = " + eventData.name + " | Event parameters = " + eventData.params);
    }
}, false);

Sending Events

The iframe listens and reacts to the following events:

Message NameDescriptionParams
trl-request-avatar-photo-urlRequest for a link to the avatar photo that is set in the avatar configN/A
trl-unregister-eventsSend this to unregister for notifications of various events registered in the iframe Source URL.Comma-separated list of event names to unregister, ideally matching those registered.
trl-chatSend a message for Trulience to process and make the avatar speak the responseText to be processed by Trulience.
trl-mic-statusSend this message to mute/unmute the mictrue to unmute, false to mute the mic.
trl-set-speaker-statusSend this message to mute/unmute the speakertrue to unmute, false to mute the speaker.
start-callStart the session with the avatar. Optionally pass a JWT token for private avatars.token (optional): JWT token for authentication
end-callMessage to terminate the ongoing session with the avatarN/A

Example

You can define functions to send iFrame events like this:

const sendChatMessage = (message) => {
    const dataToSend = { "command" : "trl-chat", "message" : message};
    const iframe = document.getElementById('iframeId');
    iframe.contentWindow.postMessage(dataToSend, "*");
}

Start a call with a JWT token (for private avatars):

const iframe = document.getElementById('iframeId');
iframe.contentWindow.postMessage({ command: 'start-call', token: jwt }, '*');

End a call:

const iframe = document.getElementById('iframeId');
iframe.contentWindow.postMessage({ command: 'end-call' }, '*');

iFrame Bridge

The iFrame Bridge lets you control the embedded avatar from the parent page through a single generic message protocol. Anything you can do on the JavaScript SDK (the Trulience instance) is reachable from the parent by name, returns a Promise, and surfaces errors with a structured code.

The bridge is additive. The legacy Sending Events commands above keep working unchanged, so you can use the bridge for new code and leave any existing legacy code in place.

Bridge URL parameters

On top of the configuration parameters above, the bridge reads these from the iFrame src:

ParameterPurpose
parentOriginRecommended. Comma-separated list of origins allowed to talk to the iframe. When set, the bridge rejects messages from any other origin, and replies are sent to the matched origin instead of '*'.
bridgeDebugSet to true to log every bridge frame to the iframe’s console. Useful during integration, but leave it off in production.
allowedSdkMethodsOptional comma-separated list of exact dotted paths your parent is allowed to call, to narrow the surface. If unset, all non-denied methods are reachable.
allowedTrlEventsOptional comma-separated list of event names your parent is allowed to subscribe to. If unset, there is no restriction.

For example:

<iframe src="https://www.trulience.com/avatar/<id>?parentOrigin=https://app.acme.com&bridgeDebug=true" />

The bridge helper

The bridge is a JSON-over-postMessage protocol, so there is no library to install. Copy this helper into your page:

const iframe       = document.getElementById('iframeId');
const iframeOrigin = new URL(iframe.src).origin;
const pending      = new Map();

function call(target, method, args = []) {
  return new Promise((resolve, reject) => {
    const id = crypto.randomUUID();
    pending.set(id, { resolve, reject });
    iframe.contentWindow.postMessage(
      { type: 'trl:call', id, target, method, args },
      iframeOrigin
    );
  });
}

window.addEventListener('message', (e) => {
  if (e.source !== iframe.contentWindow) return;
  if (e.origin !== iframeOrigin) return;
  const d = e.data;
  if (d?.type === 'trl:result') {
    const p = pending.get(d.id); if (!p) return;
    pending.delete(d.id);
    d.ok ? p.resolve(d.result) : p.reject(d.error);
  } else if (d?.type === 'trl:event') {
    onEvent(d.name, d.data);   // your event handler
  }
});

function onEvent(name, data) { /* see Subscribing to events below */ }

That gives you:

  • call('sdk', '<methodName>', [args]) to invoke an SDK method.
  • call('app', '<actionName>', [args]) to invoke an app-level action.
  • onEvent(name, data) to receive event pushes from the avatar.

Call surface

There are two targets:

  • sdk is any public method on the Trulience instance, reachable by name, including nested namespaces via dotted paths (for example messages.get). See Basic functions and Conversation history.
  • app is a small set of app-level actions that aren’t on the SDK itself: start-call, end-call, enter-vr, set-client-config, request-avatar-photo-url, register-events, unregister-events, and get-state.

Every call returns a Promise that either:

  • resolves with the method’s return value (both synchronous values and Promises are supported, since async methods are awaited inside the iframe before the result is sent), or
  • rejects with { code, message } (see Error codes).

Method status table

This is the full surface, mirrored from the Trulience public class plus the app-action registry.

In the Status column: ✓ Supported means callable via the bridge. ✗ Blocked means the method is on the bridge’s footgun denylist and returns { code: 'denied_method' }. ⚠ Marshalling-constrained means the method is technically callable but its argument or return value is not structured-cloneable (for example MediaStream or AudioContext), so it is usable only from in-page parents that pass JSON-safe data, otherwise it returns { code: 'not_serializable' }.

Identity & session state

MethodStatusNotes
getPlatform()✓ Supported
isRealtime()✓ Supported
isOauth()✓ Supported
isConnected()✓ Supported
isMediaConnected()✓ Supported
getConnectionStatus()✓ SupportedReturns a string.
getTGStatus()✓ Supported
getVPSStatus()✓ Supported
isDHWebglBased()✓ Supported
isDHUnrealEngineBased()✓ Supported
is3DAvatar()✓ Supported
setDHType(dhType)✓ Supported

Auth & user identity

MethodStatusNotes
setToken(token)✓ Supported
setUserId(uid)✓ Supported
setUserName(name)✓ Supported
connectGateway(credentials?)✓ SupportedReturns a Promise, awaited before reply.
disconnectGateway(reason?)✓ Supported

Chat / messaging

MethodStatusNotes
sendMessage(msg)✓ SupportedReplaces legacy trl-chat.
sendMessageToAvatar(msg)✓ Supported
sendMessageToVPS(msg)✓ Supported
sendUserTyping(isTyping)✓ Supported

Conversation history (messages.*)

See Conversation history for full details. Each method is reached via the dotted path.

MethodStatusNotes
messages.isSupported()✓ SupportedReturns false for speech-to-speech modes (OpenAI Realtime, Gemini Live).
messages.get(options?)✓ SupportedReturns array of {role, content} (OpenAI-compatible).
messages.set(messages, options?)✓ SupportedDestructive replace.
messages.append(messages, options?)✓ SupportedNon-destructive add.
messages.clear()✓ SupportedResets to dashboard defaults.
messages.triggerResponse()✓ Supported

Mic

MethodStatusNotes
toggleMic()✓ Supported
setMicEnabled(status, userInteraction?)✓ SupportedReplaces legacy trl-mic-status.
isMicEnabled()✓ Supported
setNeedMicAccess(needAccess)✓ Supported
setLocalMicrophoneDevice(device)⚠ Marshalling-constrainedPass a plain { deviceId } object; MediaDeviceInfo instances don’t survive postMessage.
requestPermission(type?)✓ Supported
isPermissionGranted(type)✓ Supported
getPermissionStatus(type)✓ Supported

Speaker / audio output

MethodStatusNotes
toggleSpeaker()✓ Supported
setSpeakerEnabled(status)✓ SupportedReplaces legacy trl-set-speaker-status.
isSpeakerEnabled()✓ Supported

TTS / STT / language

MethodStatusNotes
setTTSEnabled(enabled)✓ Supported
getTTSEnabled()✓ Supported
setTTSConfig(provider, voiceId, …)✓ Supported14 positional args; pass them in order in the args array.
getSttEnabled()✓ Supported
setSttSource(source)✓ Supported
getSttSource()✓ Supported
setSTTAddress(addr)✓ Supported
setSpeechRecogLang(lang)✓ Supported
setUserSelectedLanguage(lang)✓ Supported
getUserSelectedLanguage()✓ Supported

Avatar control & speech

MethodStatusNotes
setAvatarParams(params)✓ Supported
preloadAvatar(config?)✓ Supported
setAvatarState(state)✓ Supported
processSSML(message, type?)✓ Supported
stopAvatarSpeech()✓ Supported
stopOngoingSpeech()✓ Supported
setWaitForUnmute(value)✓ Supported
getWaitForUnmute()✓ Supported

Transcripts

MethodStatusNotes
getTranscripts()✓ SupportedCapped at 256 KB result size.
getTranscriptsSummary()✓ Supported

Image / video capture

MethodStatusNotes
stopVideoCaptionStream()✓ Supported
isVideoCaptionStreaming()✓ Supported
getImageCaptionConfig()✓ Supported

Media stream (advanced, most are blocked)

MethodStatusNotes
getAudioContext()⚠ Marshalling-constrainedReturns a non-cloneable AudioContext, so not_serializable. In-page only.
getCurrentMediaStream()⚠ Marshalling-constrainedReturns a non-cloneable MediaStream.
removeMediaStream(opts?)✓ Supported
setMediaStream(stream, opts?)✗ BlockedParent iframe can’t send a media stream.
setMediaStreamVideo(stream)✗ BlockedSame.
initRTCVideoTrack(track)✗ BlockedSame.
setupVADForMediaStream(stream)⚠ Marshalling-constrainedAccepts a MediaStream.
setAgoraPublish(cfg)✗ BlockedReconfigures the publisher channel mid-call.
rtc()✗ BlockedReturns the raw RTC handle.

Lifecycle & internal

MethodStatusNotes
fixAudioContext()✓ SupportedAuto-invoked by the bridge before every call, so it rarely needs explicit invocation.
cleanUp()✗ BlockedTears down the SDK; only the host iframe should call this.
on(name, fn) / off(name, fn) / emit(name, data)✗ BlockedUse the register-events / unregister-events app actions instead of direct on / off / emit access from a parent.

App-level actions (app target)

ActionArgsReturns
start-call[{ token? }]void
end-call[]void
enter-vr[]void
set-client-config[config]void
request-avatar-photo-url[]{ url: string \| null }
register-events[names: string[]]{ registered: string[] }
unregister-events[names: string[]]{ unregistered: string[] }
get-state[]{ isPlaying, avatarPhotoURL, micEnabled, speakerEnabled, connected, connectionStatus }

Basic functions

Starting / ending the call

// Start the call. Pass a fresh token if you want to swap it in here.
await call('app', 'start-call', [{ token: 'eyJ…' }]);

// End the call. Resets chat history and menu state.
await call('app', 'end-call');

Sending chat

// Replaces the legacy { command: 'trl-chat', message: '...' } frame.
await call('sdk', 'sendMessage', ['Hello avatar']);

// Send directly to the avatar (bypass VPS), or directly to VPS:
await call('sdk', 'sendMessageToAvatar', ['<trl-content … />']);
await call('sdk', 'sendMessageToVPS',    ['some control text']);

// Signal that the user is currently typing.
await call('sdk', 'sendUserTyping', [true]);

Mic / speaker

// Replaces the legacy { command: 'trl-mic-status', message: <bool> } frame.
await call('sdk', 'setMicEnabled',     [true, /* userInteraction */ true]);
await call('sdk', 'setSpeakerEnabled', [true]);

await call('sdk', 'toggleMic');
await call('sdk', 'toggleSpeaker');

const micOn      = await call('sdk', 'isMicEnabled');
const speakerOn  = await call('sdk', 'isSpeakerEnabled');

Connection state

const status    = await call('sdk', 'getConnectionStatus');   // string
const connected = await call('sdk', 'isConnected');           // boolean
const media     = await call('sdk', 'isMediaConnected');      // boolean

App-level snapshot

// One call returns a snapshot of the iframe's state, handy for
// reconciling UI on reconnect or after a page refresh.
const state = await call('app', 'get-state');
// {
//   isPlaying:        boolean,
//   avatarPhotoURL:   string | null,
//   micEnabled:       boolean | null,
//   speakerEnabled:   boolean | null,
//   connected:        boolean | null,
//   connectionStatus: string  | null
// }

Avatar photo URL

const { url } = await call('app', 'request-avatar-photo-url');

Changing client config at runtime

await call('app', 'set-client-config', [{
  hideMicButton: true,
  hideHangUpButton: false,
  // ...any forceConfig key
}]);

Transcripts

const all     = await call('sdk', 'getTranscripts');         // array
const summary = await call('sdk', 'getTranscriptsSummary');  // string

Speech control

await call('sdk', 'stopAvatarSpeech');
await call('sdk', 'stopOngoingSpeech');
await call('sdk', 'processSSML', ['<trl-anim type="aux" id="smileMedium" />', 'complete']);

Subscribing to events

The parent subscribes to events once, then receives trl:event frames whenever they fire inside the iframe.

// 1. Register the names you care about. Returns { registered: [...] }.
await call('app', 'register-events', [[
  'auth-success',
  'auth-fail',
  'media-connected',
  'websocket-message',
  'mic-update',
  'speaker-update',
  'trl-chat',
]]);

// 2. Handle them in your onEvent callback.
function onEvent(name, data) {
  switch (name) {
    case 'auth-success':      /* call setup done */ break;
    case 'media-connected':   /* video is now flowing */ break;
    case 'mic-update':        /* data is the new mic state */ break;
    case 'trl-chat':          /* data = { message, senderType, sttResponse, agentName } */ break;
    // ...
  }
}

// 3. To stop receiving an event, unsubscribe:
await call('app', 'unregister-events', [['mic-update']]);

Common event names you’ll subscribe to:

NameFires when
auth-success / auth-failAfter the iframe authenticates the session token.
websocket-connect / websocket-close / websocket-errorUnderlying WebSocket lifecycle.
websocket-messageAvatar-side message received (messageType, messageArray, senderType, agentName).
media-connectedAudio and video are flowing.
mic-updateMic enabled state changed. Payload is the new boolean.
speaker-updateSpeaker enabled state changed.
mic-accessMic permission status changed. { permissionGranted: boolean }.
trl-chatA chat turn was delivered (parent-friendly form of websocket-message).
avatar-streamingAvatar publisher state changed.
notificationGeneric notification from the SDK (toasts, errors).

Available events depend on which SDK features are enabled in the session. Set bridgeDebug=true and watch the iframe console to see what fires for your specific avatar configuration.

Conversation history

The messages.* namespace maps onto the realtime provider’s conversation-history API, letting you read, replace, or extend the running message list and re-prompt the avatar with it. A common use case is seeding the avatar with a summary of a conversation that happened on your own platform, so you don’t have to replay every turn.

All methods are reached via the sdk target with a dotted path. The bridge resolves messages on the SDK instance and then calls the final segment on the wrapper object.

messages.isSupported()

Returns true if the active realtime provider supports message history. Speech-to-speech providers (OpenAI Realtime, Gemini Live) return false. Call this once on session start to decide whether to expose the conversation-edit UI.

const ok = await call('sdk', 'messages.isSupported');
// → true | false

messages.get(options?)

Returns the current conversation history as an array.

const history = await call('sdk', 'messages.get', [{}]);
// → [ { role: 'user', content: '…' }, { role: 'assistant', content: '…' }, … ]

The exact shape of each message and the options accepted depend on the realtime provider. Pass an empty object first and inspect the result.

messages.set(messages, options?)

Replaces the entire conversation history with the supplied array. Use this when you want to seed the avatar with a fresh context (for example on session restart, or to restore a saved conversation). This is also how you hand the avatar a single summary message instead of a full back-and-forth.

await call('sdk', 'messages.set', [
  [
    { role: 'system', content: 'You are a helpful retail assistant.' },
    { role: 'system', content: 'Summary of prior chat: returning customer asked about order #1234, refund approved on 2026-05-20, now following up on delivery timing.' },
  ],
  {} // options
]);

After set, the next time the avatar responds it will treat the new list as the authoritative context.

messages.append(messages, options?)

Adds the supplied messages to the end of the existing list. Use this when you want to inject new context (for example a user message captured outside the avatar UI) without throwing away history.

await call('sdk', 'messages.append', [
  [ { role: 'user', content: 'Actually, can you check stock first?' } ],
  {}
]);

messages.clear()

Empties the conversation history. The avatar will start fresh on its next turn.

await call('sdk', 'messages.clear');

messages.triggerResponse()

Asks the avatar to produce a response without the user sending anything new. This is useful right after messages.set or messages.append when you want the avatar to react to the latest context.

await call('sdk', 'messages.set',         [history, {}]);
await call('sdk', 'messages.triggerResponse');

Putting it together

// Confirm support
if (!(await call('sdk', 'messages.isSupported'))) {
  console.warn('Realtime provider does not support messages history');
  return;
}

// Seed a conversation
await call('sdk', 'messages.set', [[
  { role: 'system', content: 'You are a friendly tour guide.' },
  { role: 'user',   content: 'Tell me something interesting about Paris.' },
], {}]);

// Make the avatar respond
await call('sdk', 'messages.triggerResponse');

// Later, push another user turn (for example captured from a non-avatar UI)
await call('sdk', 'messages.append', [
  [ { role: 'user', content: 'What about its cafés?' } ],
  {}
]);
await call('sdk', 'messages.triggerResponse');

// Read the running history (for example to persist it)
const snapshot = await call('sdk', 'messages.get', [{}]);
localStorage.setItem('conversation', JSON.stringify(snapshot));

// Reset
await call('sdk', 'messages.clear');

Error codes

Every rejected Promise resolves to an object of the shape { code: string, message: string }.

codeMeaning
not_readyThe SDK isn’t initialized yet inside the iframe. Retry after auth-success fires.
unknown_targetThe target field was not 'sdk' or 'app'.
unknown_methodThe dotted path didn’t resolve to a function (a typo, or the method doesn’t exist on the current SDK build).
denied_methodThe method is on the bridge’s footgun denylist, or outside the embedder’s allowedSdkMethods allowlist.
invalid_argsThe arguments failed validation (only thrown by a few app actions).
method_threwThe SDK method ran but threw or rejected. The original error’s message is preserved in message.
not_serializableThe return value couldn’t be sent over postMessage (functions, DOM nodes, and so on), or it exceeded the 256 KB result cap.

Legacy command equivalents

The previous one-message-per-command protocol (see Sending Events) still works unchanged, so you can adopt the bridge incrementally. Here is how the legacy frames map onto bridge calls:

Legacy frameNew equivalent
{ command: 'trl-chat', message: 'hi' }call('sdk', 'sendMessage', ['hi'])
{ command: 'trl-mic-status', message: true }call('sdk', 'setMicEnabled', [true, true])
{ command: 'trl-set-speaker-status', message: true }call('sdk', 'setSpeakerEnabled', [true])
{ command: 'trl-request-avatar-photo-url' }call('app', 'request-avatar-photo-url')
{ command: 'trl-set-client-config', payload: {…} }call('app', 'set-client-config', [{…}])
{ command: 'trl-register-events', message: 'a,b' }call('app', 'register-events', [['a','b']])
{ command: 'start-call', token: '...' }call('app', 'start-call', [{ token: '...' }])
{ command: 'end-call' }call('app', 'end-call')
{ command: 'enter-vr' }call('app', 'enter-vr')

Legacy events are also still delivered in the old shape ({ eventName, eventParams, iframeId }) alongside the new trl:event frame, so old parsers keep working.

Security checklist

Before going to production:

  1. Set parentOrigin to your exact origin (comma-separated if you have more than one). This switches outbound posts off '*' and rejects cross-origin senders.
  2. Verify event.origin === iframeOrigin on every inbound message in your parent-side handler (the helper above does this).
  3. Consider allowedSdkMethods and allowedTrlEvents to narrow what your parent can invoke or subscribe to.
  4. Never include user-controlled HTML in the parentOrigin value. Only ship origins you own.
  5. Don’t enable bridgeDebug=true in production. It logs every frame, including any sensitive arguments, to the iframe’s console.

Troubleshooting

SymptomLikely cause
Calls silently never resolveIframe origin doesn’t match parentOrigin. Add bridgeDebug=true and watch the iframe console for drop: origin <yours>.
{ code: 'not_ready' } on first callYou called before auth-success fired. Subscribe and wait, or retry with backoff.
{ code: 'unknown_method' } on messages.getThe current realtime provider doesn’t expose messages.*. Check with messages.isSupported first.
{ code: 'denied_method' } on a public-looking methodMethod is on the bridge denylist (cleanUp, rtc, on / off / emit, media-stream setters), or outside your allowedSdkMethods allowlist.
{ code: 'not_serializable' }The return value contains non-cloneable data (functions, DOM nodes) or exceeds 256 KB. Use a pagination option (for example messages.get({ limit: 50 })) for large lists.
Events not arrivingYou forgot register-events, or the name is misspelled (event names are case-sensitive).

For deeper debugging, open the iframe in DevTools and watch the console with bridgeDebug=true. Every inbound and outbound frame is logged with its origin.