Skip to navigation

Interactive Avatar: Widget security policies

Allow the widget in your Content Security Policy, Permissions-Policy and referrer settings.

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:

DirectiveNeeded forWhy
script-srcThe script snippetThe script snippet loads runtime.js from this origin.
frame-srcBoth snippetsThe widget runs in a frame from this origin.

For example:

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:

DirectiveWhy you can leave it
connect-srcruntime.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-srcEverything 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:

.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:

<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, 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:

HeaderHow the widget behaves
require-trusted-types-for 'script'The widget works, because runtime.js writes no HTML and creates no scripts.
X-Frame-OptionsThe 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-corpA 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.

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.

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:

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.