> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developers.synthesia.io/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developers.synthesia.io/docs/_mcp/server.

# Interactive Avatar: Widget security policies

The widget runs in a frame that loads from Synthesia's origin, and the script snippet loads one script from that same origin. On most sites, the snippet works as soon as you paste it. If your site sends security headers, though, those headers can block the script, the frame or the microphone, and the widget then stays blank or can't hear the visitor.

Read on if your site sends a `Content-Security-Policy`, `Permissions-Policy`, `Referrer-Policy` or `Cross-Origin-Embedder-Policy` header, or if your CMS rewrites the HTML you paste. Each section below explains what to change, what you can leave as it is, and what the widget can't support yet.

## Content Security Policy

A Content Security Policy tells the browser which origins your page may load scripts and frames from. If your page sends one, you need to add `https://interactive-avatars.synthesia-embeds.io` to two directives:

| Directive    | Needed for         | Why                                                     |
| ------------ | ------------------ | ------------------------------------------------------- |
| `script-src` | The script snippet | The script snippet loads `runtime.js` from this origin. |
| `frame-src`  | Both snippets      | The widget runs in a frame from this origin.            |

For example:

```text
Content-Security-Policy:
  script-src 'self' https://interactive-avatars.synthesia-embeds.io;
  frame-src 'self' https://interactive-avatars.synthesia-embeds.io
```

If your policy uses `script-src-elem`, add the origin there too. If your policy has no `frame-src`, browsers fall back to `child-src` and then to `default-src`, so add the origin to whichever one your policy has.

### Directives you don't need to change

The rest of your policy can stay as it is. The widget makes its own requests from inside its frame, where the frame's policy applies and yours doesn't. `runtime.js` itself needs nothing beyond `script-src`. So these directives don't affect the widget:

| Directive                          | Why you can leave it                                                                                                                                                                   |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `connect-src`                      | `runtime.js` makes no requests of its own. The requests for the session, the call and the bot check all run inside the frame.                                                          |
| `img-src`, `media-src`, `font-src` | Everything the widget shows is inside the frame.                                                                                                                                       |
| `style-src` (script snippet)       | `runtime.js` adds no stylesheet or style markup. It styles the frame through the DOM style API, which `style-src` doesn't restrict, so the widget works even under `style-src 'none'`. |

### Size the iframe without inline styles

The iframe snippet sets its size with an inline `style` attribute. If your `style-src` doesn't allow `'unsafe-inline'`, the browser ignores that attribute. The frame then falls back to a fixed 400 × 620 and can overflow a phone screen. To avoid this, set the size in your own stylesheet, using the class the snippet carries:

```css
.synthesia-ia-widget-frame {
  border: 0;
  width: 400px;
  max-width: 100%;
  height: 620px;
  max-height: 100dvh;
}
```

### Use a nonce

If your policy uses nonces with `'strict-dynamic'`, browsers ignore host names in `script-src`, so adding the widget origin isn't enough. Put your nonce on the script tag instead:

```html
<script
  async
  nonce="…"
  src="https://interactive-avatars.synthesia-embeds.io/v1/runtime.js"
  data-deployment-id="dep_…"
></script>
```

Under `'strict-dynamic'`, a script tag that a trusted script inserts, such as a tag added by your tag manager, needs no nonce. `runtime.js` doesn't load any other scripts, so there's nothing else to allow.

If you use the [inline stub](/docs/ia-widget-page-api#calling-the-api-before-the-script-loads), it needs your nonce or its hash in `script-src`, like any other inline script.

### Other headers

A few more headers affect how pages load scripts and frames. Here's how the widget behaves with each of them:

| Header                                       | How the widget behaves                                                                                                                                              |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `require-trusted-types-for 'script'`         | The widget works, because `runtime.js` writes no HTML and creates no scripts.                                                                                       |
| `X-Frame-Options`                            | The widget document doesn't send this header. It controls who may frame it with the CSP `frame-ancestors` directive, which it builds from your allowed origins.     |
| `Cross-Origin-Embedder-Policy: require-corp` | A page that sends this header can't frame the widget for now. The script itself still loads, because we serve it with `Cross-Origin-Resource-Policy: cross-origin`. |

If any of these policies blocks the frame, it stays empty. With the script snippet, your page also gets an [`internal` error](/docs/ia-widget-page-api#errors).

## Microphone and autoplay

The widget frame needs the microphone so visitors can talk to the avatar, and autoplay so they can hear it. Both snippets create the frame with `allow="microphone; autoplay"`. That attribute grants only these two permissions, and only to the widget origin. The browser still asks the visitor before it turns on the microphone, and the widget only asks once the visitor clicks to start a call.

> **Warning**
>
> Keep the `allow` attribute. If you remove it, or a sanitizer or CMS strips it, the browser blocks the microphone.

### Permissions-Policy

A frame can never get a permission that its page doesn't have, whatever the frame's `allow` attribute says. So if your page sends a `Permissions-Policy` header with a `microphone` or `autoplay` directive, that directive must list the widget origin. For example, both `microphone=()` and `microphone=(self)` block the widget. Add the origin like this:

```text
Permissions-Policy:
  microphone=(self "https://interactive-avatars.synthesia-embeds.io"),
  autoplay=(self "https://interactive-avatars.synthesia-embeds.io")
```

If your header has no `microphone` or `autoplay` directive, or you don't send the header at all, you don't need to change anything. The frame's `allow` attribute is enough.

When your page's policy blocks the microphone, what the visitor sees depends on the browser. Chrome and Edge let the widget detect the policy, so the widget tells the visitor that the microphone isn't allowed on this page. Firefox and Safari don't expose the policy, so there the widget asks the visitor to allow the microphone. With the script snippet, your page also gets an `internal` error.

## Referrer

The widget checks that it runs on an allowed site. It learns your page's origin from the referrer and compares it with the deployment's allowed origins. The frame carries `referrerpolicy="strict-origin"`, so the widget gets your origin but never the path of your page.

With the script snippet, you don't need to do anything. `runtime.js` always sets this attribute, so your page's own referrer policy doesn't affect the widget.

With the iframe snippet, keep the `referrerpolicy` attribute that the snippet comes with. Pages with a `no-referrer` or `same-origin` policy send no referrer to other origins. Without the attribute, the widget on such a page shows "can't load on this page" when the visitor starts a call.