## Adding an Avatar iFrame to a Web Page
Trulience avatars can be embedded in a web page using an <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/iframe" target="_blank">iFrame</a>:
```html
<iframe
    height="600px"
    title="Trulience avatar"
    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:

<iframe style="margin-bottom: 20px" height="600px" width="100%" allow="camera; microphone; fullscreen; accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" src="https://www.trulience.com/avatar/6038809566353378449?dialPageBackground=white&overlayButtonColor=#ff6200" frameborder="0" allowfullscreen></iframe>

### 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](/docs/trulience-platform/security) for full setup instructions.

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

```html
<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:

| Parameter             | Description                                                                                       |
|-----------------------|---------------------------------------------------------------------------------------------------|
| `connect`               | If set to true, the client will connect automatically when visiting the avatar link - bypassing the 'dial' screen   |
| `micOff`                | If set to true, the microphone will be muted by default                                           |
| `speakerOff`            | If set to true, the speaker will be muted by default                                              |
| `hideFS`                | If set to true, hides the full screen button                                                      |
| `hideChatInput`         | If set to true, hides the chat input box                                                          |
| `hideChatHistory`       | If set to true, hides the chat history                                                            |
| `hideLetsChatBtn`       | If set to true, hides the dial button                                                             |
| `hideMicButton`         | If set to true, hides the microphone button preventing the users from being able to mute/unmute   |
| `hideHangUpButton`      | If set to true, hides the hang up button                                                          |
| `hideSpeakerButton`     | If set to true, hides the speaker button                                                          |
| `dialButtonText`        | Text that appears on the dial button                                                              |
| `msgOnConnect`          | Message (string) sent to the avatar on connection                                                 |
| `screenAspectRatio`     | Sets the aspect ratio of the visible video area e.g. 16:9, 4:3, 1:1 etc.                          |
| `chatInputBoxWidth`     | Width of the chat input box: `full` (match the video area, the default) or `window` (span the full window width in landscape)                             |
| `showLogo`              | If set to true, the client will show either the default Trulience logo or the provided logoSrc    |
| `logoPosition`          | Can be right or left                                                                              |
| `logoSrc`               | The https location of the logo image to be displayed in top right or left of visible video window |
| `registerTrlEvents`     | List of events that should be notified to iframe's parent as and when they occur                  |
| `fullscreen`            | If set to true, the avatar will be displayed in fullscreen mode                                   |
| `disableDragging`       | If set to true, dragging to resize the avatar will be disabled                                    |
| `token`                 | Provide a JWT token to authenticate access to private avatars. See [Security](/docs/trulience-platform/security) for setup. |
| `userId`                | Associates the session with your own numeric user ID. Included as `user_id` in session webhook events. |
| `username`              | Associates the session with a display name. Included as `user_name` in session webhook events.    |
| `ttsLang`               | Sets the TTS language for avatars using the ElevenLabs voice provider (e.g. `es`, `fr`, `hi`). Ignored for other TTS providers. If a comma-separated list is given, only the first value is used. Equivalent to calling [`setTtsLanguage`](/docs/developer/javascript#trulience-class-methods) on the SDK. |
| `controlButtonPosition` | Can be center, right or left                                                                      |
| `hideToast`             | If 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:
```html
<iframe
    height="700px"
    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>
```

### Intro Video Buttons

Avatars with a pre-connect intro video show a play button over the video whenever it isn't playing. Clicking or tapping the video toggles play and pause. If the video has audio, a mute toggle (**Tap for sound** / **Mute**) is shown, including before playback. The video always starts muted unless the toggle has been tapped.

On desktop, the mute toggle fades out after a few seconds without mouse movement during playback and reappears when the mouse moves. On touch devices it stays visible.

Style the buttons with these parameters:

| Parameter                | Applies to                                  |
|--------------------------|---------------------------------------------|
| `introPlayButtonStyles`  | The round play button                       |
| `introPlayIconStyles`    | The play icon inside it (`color` sets the icon colour) |
| `introSoundButtonStyles` | The mute toggle                             |

Each value is an object of CSS properties in camelCase (e.g. `borderRadius`), applied as inline styles over the defaults. Only set the properties you want to change. In a query parameter, pass the object as URL-encoded JSON. A query parameter overrides the same key in the client JSON.

Optional nested keys:

| Key        | Applies when                               |
|------------|--------------------------------------------|
| `portrait` | The viewport is in portrait orientation    |
| `mobile`   | The device is a mobile device              |
| `hover`    | The mouse is over the button (buttons only) |

Overrides are applied in the order base, `portrait`, `mobile`, `hover`.

The play button is centred with `top: 50%; left: 50%; transform: translate(-50%, -50%)`. To move it, set `top`/`left`/`bottom` and `transform` yourself.

#### Example

Client JSON:
```json
{
  "introPlayButtonStyles": {
    "background": "rgba(26,115,232,0.8)",
    "width": "88px",
    "height": "88px",
    "border": "2px solid #fff",
    "hover": { "background": "rgba(26,115,232,1)" },
    "mobile": { "width": "64px", "height": "64px" }
  },
  "introPlayIconStyles": { "color": "#fff", "width": "44px", "height": "44px" },
  "introSoundButtonStyles": { "background": "#1a73e8", "borderRadius": "4px" }
}
```

Query parameters:
```html
<iframe
    src="https://www.trulience.com/avatar/<id>?introPlayButtonStyles=%7B%22background%22%3A%22rgba%2826%2C115%2C232%2C0.8%29%22%2C%22width%22%3A%2288px%22%2C%22height%22%3A%2288px%22%7D&introPlayIconStyles=%7B%22color%22%3A%22%23ffcc00%22%7D"
    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](/docs/developer/javascript). You can interact with the SDK through iFrame messaging.

There are two ways to do this, and both work at the same time:

- **Per-command messaging** (described below): send a `postMessage` frame such as `{ command: 'trl-chat', ... }`, and listen for events. Simple, and what most existing integrations use.
- **The [iFrame Bridge](#iframe-bridge)**: a single `call(target, method, args)` helper that reaches any SDK method or app action and returns a Promise. Prefer this for new code.

Either way, messages only take effect once the avatar has loaded and authenticated. Wait for the `auth-success` event before sending `start-call`, chat, or other commands (with the bridge, an early call rejects with a `not_ready` error you can retry).

### Listening to SDK Events

You can register which [SDK events](/docs/developer/javascript#available-events) you want to listen to by appending them to your iFrame's `src` attribute via a query param, for example:

```html
<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 Name | Description | Params|
|------------|-------------|-------|
| `trl-chat` | Fired when a chat message is received from the server.	| `{message, agentName, sttResponse, senderType}` |
| `trl-respond-avatar-photo-url` | Fired in response to the trl-request-avatar-photo-url request from the iframe's parent. | URL pointing to the photo of the avatar. |
| `action-button-click` | Fired when the user clicks one of the action buttons configured via `trl-set-action-buttons`. | `{ id }` (the opaque id of the clicked button). |

Each event reaches your `message` listener as `{ eventName, eventParams, iframeId }`, where `eventParams` holds the values in the Params column above.

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

#### Example

You can listen to iFrame events like this:

```javascript
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.eventName + " | Event parameters = " + eventData.eventParams);
    }
}, false);
```

### Sending Events

The iframe listens and reacts to the following events:

| Message Name                | Description                                                                                     | Params                                                                           |
|-----------------------------|-------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|
| `trl-request-avatar-photo-url`| Request for a link to the avatar photo that is set in the avatar config                         | N/A                                                                              |
| `trl-unregister-events`       | Send 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-chat`                    | Send a message for Trulience to process and make the avatar speak the response                  | Text to be processed by Trulience.                                               |
| `trl-mic-status`              | Send this message to mute/unmute the mic                                                        | `true` to unmute, `false` to mute the mic.                                       |
| `trl-set-speaker-status`      | Send this message to mute/unmute the speaker                                                    | `true` to unmute, `false` to mute the speaker.                                   |
| `start-call`                  | Start the session with the avatar. Optionally pass a JWT token for private avatars.             | `token` (optional): JWT token for authentication                                 |
| `end-call`                    | Message to terminate the ongoing session with the avatar                                        | N/A                                                                              |
| `trl-set-action-buttons`      | Configure the action buttons rendered in the avatar view. Send once, or again to update the set. See [Action buttons and content](#action-buttons-and-content). | `buttons`: array of `{ id, label?, iconUrl? }` (max 6). `id` is opaque and echoed back on click. |
| `trl-post-chat-media`         | Render an image or video into the chat.                                                         | `{ url, type: 'image' \| 'video', senderType?, options? }`. Video is MP4 only; `options` = `{ autoplay?, loop?, muted? }`. |
| `trl-show-toast`              | Show a toast notification in the avatar UI.                                                     | `{ message, type?: 'error' \| 'info' \| 'success' }`                             |

#### Example

You can define functions to send iFrame events like this:

```javascript
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):

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

End a call:

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

## Action buttons and content

The embedded avatar can render a set of **action buttons** that your page
configures at runtime. When the user clicks one, the iframe notifies your page;
what a button *means* is entirely up to you. In response, your page can push an
image or video into the chat, or show a toast. These are display primitives:
the avatar renders them, and your own code decides what they do. They belong to
the embedded avatar (what the iframe loads), not the headless
[JavaScript SDK](/docs/developer/javascript).

Every message below is a plain `postMessage` frame, the same mechanism used in
[Sending Events](#sending-events). Each also has a bridge equivalent
(`call('app', '...')`); see [iFrame Bridge](#iframe-bridge).

The examples use `'*'` as the target origin for brevity. In production, compute
and pin it (`const targetOrigin = new URL(iframe.src).origin`) and check
`event.origin` on inbound messages. See the [Security checklist](#security-checklist).

**1. Configure the buttons.** Send at any time; re-send to update the set. Up to
6 buttons are shown. `id` is opaque and round-tripped back to you on click;
`label` and `iconUrl` are display only.

```javascript
const iframe = document.getElementById('iframeId');
iframe.contentWindow.postMessage({
  command: 'trl-set-action-buttons',
  buttons: [
    { id: 'a', label: '200', iconUrl: 'https://example.com/a.png' },
    { id: 'b', label: '350', iconUrl: 'https://example.com/b.png' },
  ],
}, '*');
```

**2. Detect a click.** Subscribe to the `action-button-click` event, either by
adding it to `registerTrlEvents` on the iframe `src` or with `trl-register-events`,
then handle it:

```javascript
// subscribe once (or add action-button-click to registerTrlEvents in the src)
iframe.contentWindow.postMessage({
  command: 'trl-register-events', message: 'action-button-click',
}, '*');

window.addEventListener('message', (event) => {
  const { eventName, eventParams } = event.data || {};
  if (eventName === 'action-button-click') {
    const { id } = eventParams; // the id you configured in step 1
    // run your own logic, then respond (step 3)
  }
});
```

**3. Respond.** Once your own backend has decided the outcome, push content into
the chat on success, or show a toast on failure. Both are independent commands,
so you call whichever fits:

```javascript
// Image or video. Video must be a directly-playable file (MP4/H.264). HLS is
// not supported. Autoplay only fires when muted (browser policy).
iframe.contentWindow.postMessage({
  command: 'trl-post-chat-media',
  url: 'https://example.com/clip.mp4',
  type: 'video',            // 'image' | 'video'
  senderType: 'agent',      // whose chat bubble it renders as ('agent' | 'user')
  options: { autoplay: true, loop: false, muted: true }, // video only
}, '*');

// Toast
iframe.contentWindow.postMessage({
  command: 'trl-show-toast',
  message: 'Something to tell the user',
  type: 'error',            // 'error' | 'info' | 'success'
}, '*');
```

Note that the iframe delivers every event as both a modern `trl:event` frame and
a legacy `{ eventName, eventParams }` envelope, for backward compatibility.
Handle one shape, not both, or your click handler will run twice.

## 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](/docs/developer/javascript) (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](#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](#configuration-parameters) above, the bridge reads these from the iFrame `src`:

| Parameter | Purpose |
|-----------|---------|
| `parentOrigin` | **Recommended.** 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 `'*'`. |
| `bridgeDebug` | Set to `true` to log every bridge frame to the iframe's console. Useful during integration, but leave it off in production. |
| `allowedSdkMethods` | Optional 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. |
| `allowedTrlEvents` | Optional comma-separated list of event names your parent is allowed to subscribe to. If unset, there is no restriction. |

For example:

```html
<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:

```js
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](#basic-functions) and [Conversation history](#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](#error-codes)).

### Available methods

Any public method on the `Trulience` instance is callable via the `sdk` target, by name (dotted paths like `messages.set` work too). The ones you'll use most:

| Method | What it does |
| --- | --- |
| `sendMessage(text)` | Send a chat message (replaces the legacy `trl-chat`). |
| `setMicEnabled(on, userInteraction?)`, `toggleMic()`, `isMicEnabled()` | Mic control. |
| `setSpeakerEnabled(on)`, `toggleSpeaker()`, `isSpeakerEnabled()` | Speaker control. |
| `isConnected()`, `isMediaConnected()`, `getConnectionStatus()` | Connection state. |
| `processSSML(message, type?)`, `stopAvatarSpeech()` | Avatar speech control. |
| `getTranscripts()`, `getTranscriptsSummary()` | Read the transcript. |
| `messages.*` | Read, replace, or extend conversation history. See [Conversation history](#conversation-history). |

A few methods are blocked and return `{ code: 'denied_method' }`: `cleanUp`, `rtc`, the media-stream setters (`setMediaStream`, `setMediaStreamVideo`, `initRTCVideoTrack`, `setAgoraPublish`), and direct `on` / `off` / `emit` (use the `register-events` and `unregister-events` actions instead).

Some methods return values that can't be sent over `postMessage`, such as a `MediaStream` or `AudioContext`, or anything larger than 256 KB. These return `{ code: 'not_serializable' }` and only work from a parent page on the same origin as the iframe.

#### App-level actions (`app` target)

| Action | Args | Returns |
| --- | --- | --- |
| `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 }` |
| `set-action-buttons` | `[buttons: { id, label?, iconUrl? }[]]` | `void` |
| `post-chat-media` | `[{ url, type, senderType?, options? }]` | `void` |
| `show-toast` | `[{ message, type? }]` | `void` |

### Basic functions

#### Starting / ending the call

```js
// 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

```js
// 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

```js
// 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

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

#### App-level snapshot

```js
// 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

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

#### Changing client config at runtime

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

#### Transcripts

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

#### Speech control

```js
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.

```js
// 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:

| Name | Fires when |
| --- | --- |
| `auth-success` / `auth-fail` | After the iframe authenticates the session token. |
| `websocket-connect` / `websocket-close` / `websocket-error` | Underlying WebSocket lifecycle. |
| `websocket-message` | Avatar-side message received (`messageType`, `messageArray`, `senderType`, `agentName`). |
| `media-connected` | Audio and video are flowing. |
| `mic-update` | Mic enabled state changed. Payload is the new boolean. |
| `speaker-update` | Speaker enabled state changed. |
| `mic-access` | Mic permission status changed. `{ permissionGranted: boolean }`. |
| `trl-chat` | A chat turn was delivered (parent-friendly form of `websocket-message`). |
| `avatar-streaming` | Avatar publisher state changed. |
| `notification` | Generic notification from the SDK (toasts, errors). |
| `action-button-click` | The user clicked an action button configured via `set-action-buttons`. Payload `{ id }`. |

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.

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

#### `messages.get(options?)`

Returns the current conversation history as an array.

```js
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.

```js
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.

```js
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.

```js
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.

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

#### Putting it together

```js
// 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 }`.

| `code` | Meaning |
| --- | --- |
| `not_ready` | The SDK isn't initialized yet inside the iframe. Retry after `auth-success` fires. |
| `unknown_target` | The `target` field was not `'sdk'` or `'app'`. |
| `unknown_method` | The dotted path didn't resolve to a function (a typo, or the method doesn't exist on the current SDK build). |
| `denied_method` | The method is on the bridge's footgun denylist, or outside the embedder's `allowedSdkMethods` allowlist. |
| `invalid_args` | The arguments failed validation (only thrown by a few app actions). |
| `method_threw` | The SDK method ran but threw or rejected. The original error's message is preserved in `message`. |
| `not_serializable` | The 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](#sending-events)) still works unchanged, so you can adopt the bridge incrementally. Here is how the legacy frames map onto bridge calls:

| Legacy frame | New 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')` |
| `{ command: 'trl-set-action-buttons', buttons: [...] }` | `call('app', 'set-action-buttons', [[...]])` |
| `{ command: 'trl-post-chat-media', url, type, ... }` | `call('app', 'post-chat-media', [{ url, type, ... }])` |
| `{ command: 'trl-show-toast', message, type }` | `call('app', 'show-toast', [{ message, type }])` |

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

| Symptom | Likely cause |
| --- | --- |
| Calls silently never resolve | Iframe origin doesn't match `parentOrigin`. Add `bridgeDebug=true` and watch the iframe console for `drop: origin <yours>`. |
| `{ code: 'not_ready' }` on first call | You called before `auth-success` fired. Subscribe and wait, or retry with backoff. |
| `{ code: 'unknown_method' }` on `messages.get` | The current realtime provider doesn't expose `messages.*`. Check with `messages.isSupported` first. |
| `{ code: 'denied_method' }` on a public-looking method | Method 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 arriving | You 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.

## Add a floating avatar

You can also embed an avatar with a transparent background that floats on top of your existing website, like a chat widget, instead of sitting inline in the page.

### Find your Avatar ID

Every avatar has a unique ID. You need it to build the iframe `src` URL.

1. Log in to your [Trulience dashboard](https://www.trulience.com) and go to your avatars list.
2. Each avatar shows its **Avatar ID** directly below the avatar name — a long number like `750723204286267723`.
3. Copy that ID. You'll paste it into the iframe URL as: `https://www.trulience.com/avatar/YOUR_AVATAR_ID`

> **Note:** The avatar service always runs on `trulience.com`. Even if your site is hosted elsewhere, the iframe `src` always points to `www.trulience.com/avatar/…`

### Float it in the corner

Wrap the iframe in a container div. CSS `position: fixed` pins it to the bottom-left of the viewport so it stays visible as the user scrolls.

```html
<div id="trl-avatar">
  <iframe
    src="https://www.trulience.com/avatar/YOUR_AVATAR_ID"
    frameborder="0"
    allow="microphone; camera; fullscreen; autoplay; encrypted-media"
    allowfullscreen
    style="width:100%;height:100%;border:none;border-radius:20px">
  </iframe>
</div>
```

```css
#trl-avatar {
  position: fixed;
  bottom:    -6px;
  left:      0;
  z-index:   9999;
  min-height:200px;

  /* size set by JS below */
}
```

> **Warning:** `bottom: -6px` intentionally clips a few pixels at the bottom edge so the avatar frame bleeds into the page edge, giving a natural "sitting on the page" look. Change to `bottom: 0` if you prefer a clean gap.

### Make it responsive

This script sizes the avatar to 35% of the viewport, capped between 220px and 500px wide, and recalculates on resize. Without it the avatar will be a fixed pixel size.

```javascript
(function () {
  var el    = document.getElementById('trl-avatar');
  var MAX   = 500;   // px — largest the avatar will grow
  var MIN   = 220;   // px — smallest on narrow screens
  var RATIO = 4 / 3; // width ÷ height

  function resize() {
    var w = Math.max(MIN, Math.min(window.innerWidth * 0.35, MAX));
    el.style.width  = w + 'px';
    el.style.height = (w / RATIO) + 'px';
  }

  resize();
  window.addEventListener('resize', resize);
}());
```

> **Tip:** Change `0.35` (35% of viewport) to control how prominent the avatar feels. `0.25` is more subtle; `0.45` is larger. Change `RATIO` to match your avatar's `screenAspectRatio` URL parameter — `4/3` pairs with `screenAspectRatio=4:3`.

### Customising appearance and behaviour

Append the same query parameters described in [Configuration Parameters](#configuration-parameters) and [Adjusting Colour](#adjusting-colour) to the avatar URL to control the floating widget's behaviour and appearance — for example `screenAspectRatio`, `hideChatInput`, `hideChatHistory`, `dialPageBackground=transparent`, `disableDragging`, `controlButtonPosition`, and `overlayButtonColor`. You can also set `disablePanels=true` to hide side configuration panels and `controlButtonLayout=column` to stack the control buttons vertically instead of horizontally.

> **Tip:** Always URL-encode the `#` sign as `%23` in hex colours. For example, `#ff6200` becomes `%23ff6200` in the query string.

### Complete example

A full working HTML page. Replace `YOUR_AVATAR_ID` and drop it into your project.

```html
<!-- Add inside <head> -->
<style>
  #trl-avatar {
    position:   fixed;
    bottom:     -6px;
    left:       0;
    z-index:    9999;
    min-height: 200px;
  }
</style>

<!-- Add before </body> -->
<div id="trl-avatar">
  <iframe
    src="https://www.trulience.com/avatar/YOUR_AVATAR_ID?screenAspectRatio=4:3&hideChatInput=true&hideChatHistory=true&dialPageBackground=transparent&disableDragging=true&disablePanels=true&controlButtonLayout=column&controlButtonPosition=left&overlayButtonColor=%23ff6200"
    frameborder="0"
    allow="microphone; camera; fullscreen; autoplay; encrypted-media"
    allowfullscreen
    style="width:100%;height:100%;border:none;border-radius:20px">
  </iframe>
</div>

<script>
  (function () {
    var el    = document.getElementById('trl-avatar');
    var MAX   = 500, MIN = 220, RATIO = 4 / 3;
    function resize() {
      var w = Math.max(MIN, Math.min(window.innerWidth * 0.35, MAX));
      el.style.width  = w + 'px';
      el.style.height = (w / RATIO) + 'px';
    }
    resize();
    window.addEventListener('resize', resize);
  }());
</script>
```

Prefer a standalone, shareable version of this same guide? <a href="/floating_avatar" target="_blank" rel="noopener">Click here</a>.