> 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 page API

The script snippet gives your page a small JavaScript API, `window.SynthesiaInteractiveAvatar`. You can use it to open and close the widget from your own buttons, to react when a call starts, ends or fails, and to link each call to your own records. For example, you can send those events to your analytics, or show a fallback contact form when the avatar can't take a call.

You can also control the `<synthesia-interactive-avatar>` element directly. It has its own `open()`, `close()` and `init()` methods, and it fires each event as a DOM event. The iframe snippet has no page API, so you need the script snippet for anything on this page.

## Methods

All methods live on `window.SynthesiaInteractiveAvatar`. Most of them take an optional deployment ID. Pass one to address a single widget, or leave it out to address every widget on the page.

| Method                 | What it does                                                                                                |
| ---------------------- | ----------------------------------------------------------------------------------------------------------- |
| `open(deploymentId?)`  | Expands the bubble.                                                                                         |
| `close(deploymentId?)` | Minimizes the bubble.                                                                                       |
| `init(deploymentId?)`  | Loads a widget that is waiting for consent. See [Consent deferral](/docs/ia-widget-privacy#consent-deferral).    |
| `on(name, handler)`    | Calls `handler` each time the event fires. It returns a function that you call to remove the handler again. |

The element has its own `open()`, `close()` and `init()` methods, which act on that element's widget only.

`open()` and `close()` only work once the widget is ready. If you want the bubble to open as soon as possible, call `open()` from a `ready` handler. This example uses the function form of the API, so it also works [before the script has loaded](#calling-the-api-before-the-script-loads):

```js
SynthesiaInteractiveAvatar("on", "ready", function (detail) {
  SynthesiaInteractiveAvatar.open(detail.deploymentId);
});
```

## Events

Events tell your page what the widget and the visitor are doing. Listen to them with `on()`, or with `addEventListener` on the element, where each event has a name that starts with `synthesia-ia:`.

Every handler receives an object with the widget's `deploymentId` and its current `state`. When the state is `error`, the object also has an `error` field. The two call events also carry a [`sessionId`](#link-a-call-to-your-own-records). On the element, you find the same object in the event's `detail`.

| Event             | DOM event on the element       | Fires when                                                                                                  |
| ----------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| `ready`           | `synthesia-ia:ready`           | The widget appears on the page for the first time.                                                          |
| `opened`          | `synthesia-ia:opened`          | The bubble expands. This includes a bubble that starts expanded.                                            |
| `closed`          | `synthesia-ia:closed`          | The bubble minimizes.                                                                                       |
| `session_started` | `synthesia-ia:session-started` | The visitor's call has started. It fires once per call.                                                     |
| `session_ended`   | `synthesia-ia:session-ended`   | A call that started is over, because it ended, dropped or failed. It fires once for each call that started. |
| `error`           | `synthesia-ia:error`           | The widget enters the `error` state.                                                                        |

`state` tells you where the call is. It's `idle` before a call or when the visitor is back at the start, `connecting` while a call starts, and `in_call` while it runs. It's `ending` while a call the visitor ended is closing, `ended` once the call is over, and `error` when something went wrong. The changes to `connecting`, to `ending` and back to `idle` have no event of their own. You see them in `state` on the next event.

## Errors

When something goes wrong, for example the widget can't load or a call can't start or go on, the widget enters the `error` state and fires an `error` event. The event's `error` field holds a `code` and a `message`. Branch on `code`, which stays stable. Use `message` only for your logs: it's English text for developers, and we may reword it.

| Code                        | What happened                                                                                                    |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `handshake_invalid`         | Synthesia refused the widget's security check. A retry usually works.                                            |
| `captcha_failed`            | The bot check failed.                                                                                            |
| `origin_not_allowed`        | Your page's origin isn't in the deployment's allowed origins.                                                    |
| `not_found`                 | The deployment doesn't exist, or it isn't published.                                                             |
| `configuration_invalid`     | The published configuration can't run a session.                                                                 |
| `rate_limited`              | Your workspace sent too many session requests in a short time.                                                   |
| `no_credit`                 | Your workspace has no credit left.                                                                               |
| `concurrency_limit_reached` | Your workspace has reached its limit on calls at the same time, or the visitor waited too long for a free place. |
| `daily_limit_reached`       | A daily call limit has been reached.                                                                             |
| `spend_limit_reached`       | Your workspace has reached its spend limit.                                                                      |
| `session_unavailable`       | The service couldn't start a call.                                                                               |
| `internal`                  | Something went wrong in the visitor's browser or on your page, or something unexpected failed. See below.        |

### The `internal` code

`internal` covers problems on the visitor's side and on your page, plus anything unexpected. Its `message`, meant for your logs, says which of these happened:

* The visitor denied the microphone, your page blocked it, or the browser found none.
* The connection check before the call failed.
* Your page sent no referrer, so the widget couldn't check your origin.
* The widget frame didn't load. In that case your page gets a single `error` event about 15 seconds after the page loads, or after you call `init()` if you use consent deferral. The message is "The widget frame did not become ready."
* Something else failed unexpectedly.

A blocked origin or an unpublished deployment usually stops the frame from loading at all. That's why you usually see `internal` for these two cases, and not `origin_not_allowed` or `not_found`.

We may add new codes over time. Treat any code you don't recognize as a generic failure.

## Calling the API before the script loads

Because the script loads with `async`, it doesn't hold up your page, but `SynthesiaInteractiveAvatar` may not exist yet when your own code runs. To call the API early, add this stub above the script tag. The stub queues your calls, and the script runs them once it has loaded:

```html
<script>
  window.SynthesiaInteractiveAvatar =
    window.SynthesiaInteractiveAvatar ||
    function () {
      (SynthesiaInteractiveAvatar.q = SynthesiaInteractiveAvatar.q || []).push(
        arguments
      );
    };
  SynthesiaInteractiveAvatar("on", "session_started", function (detail) {});
</script>
```

Until the script has loaded, call the API as a function and pass the method name first, for example `SynthesiaInteractiveAvatar("open")`. In this form, `on` doesn't return a function that removes the handler.

If your site has a Content Security Policy, the stub needs your nonce or its hash, like any other inline script. See [Use a nonce](/docs/ia-widget-security-policies#use-a-nonce).

## Link a call to your own records

`session_started` and `session_ended` also carry a `sessionId`. It's the call's public ID, in the form `ses_…`, and it's the same on both events for one call. Store it next to your own records, such as an order or a support ticket, so you can tell later which conversation belongs to which customer:

```js
SynthesiaInteractiveAvatar.on("session_started", function (detail) {
  saveSessionId(detail.sessionId);
});
```

`sessionId` can be missing if the widget didn't report one, so treat it as optional.