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

# Getting started

This guide embeds an AI Agent on a web page with the Messaging Web SDK. Pick one of three paths: a module script, the npm package, or an existing Chat SDK script tag that keeps working unchanged.

You need your AI Agent's handle. If you are not sure of your handle, ask your Ada team.

For new installations, the module script and the npm package are the recommended paths. The `window.adaEmbed` and `window.adaSettings` globals remain fully supported for existing installs, but are deprecated for new installations. See [Security best practices](/messaging/web/security-best-practices) for the reasoning and for guidance on scoping your integration code.

## Embed with a module script

Import the hosted SDK entry, create the `adaEmbed` interface, and start the AI Agent.

```html
<script type="module">
  import { createAdaEmbedInterface } from "https://messaging-assets.ada.support/sdk.js";

  const adaEmbed = createAdaEmbedInterface();

  await adaEmbed.start({
    handle: "<your-handle>",
  });
</script>
```

You should now see a chat button in the bottom right corner of the page. Click the button to open and close the chat drawer.

The `adaEmbed` reference stays private to the module. Do not assign it to `window`; see [Keep the client in a private scope](/messaging/web/security-best-practices#keep-the-client-in-a-private-scope).

Notes:

* `handle` is required. Most websites need only `handle`.
* Set `cluster` or `domain` only if Ada gives you a non-default regional configuration.
* Pass `parentElement` to render the conversation inline inside your own container. Omit it for the floating launcher and drawer. In `parentElement` mode, `toggle()` is not supported.

> **Check**
>
> The SDK supports a rich set of settings, actions, and events. For example, you can set the AI Agent language, pass end-user context through `metaFields`, or start an identified session with `identityToken`. The [SDK API reference](/messaging/web/sdk-api-reference) covers all of them.

## Install from npm

Use [`@ada-cx/messaging-sdk`](https://www.npmjs.com/package/@ada-cx/messaging-sdk) when your site is built with a bundler.

```sh
npm install @ada-cx/messaging-sdk
```

> **Note**
>
> [`@ada-cx/messaging-sdk`](https://www.npmjs.com/package/@ada-cx/messaging-sdk) is live on npm. Install the latest version.

The package carries TypeScript types, test mocks, and a thin loader. It never contains the SDK runtime. The loader imports the runtime from Ada's CDN at page load, so every page receives the current rollout version. The npm version never pins the runtime. This is the same model as `@stripe/stripe-js`.

### Quick start

Call `loadAdaMessaging` with your handle. It loads the runtime, creates the interface, starts the AI Agent, and returns the interface.

```ts
import { loadAdaMessaging } from "@ada-cx/messaging-sdk";

// Module scope: your code holds the only reference.
const ada = await loadAdaMessaging({ handle: "<your-handle>" });
```

`loadAdaMessaging` never assigns the interface to `window` or any other global. Keep the returned reference in module scope, where only your own code can reach it. See [Security best practices](/messaging/web/security-best-practices#keep-the-client-in-a-private-scope).

Use `loadMessagingSdk` when you want the runtime module without starting the AI Agent, for example to preload it before the page needs the widget:

```ts
import { loadMessagingSdk } from "@ada-cx/messaging-sdk";

const sdkModule = await loadMessagingSdk();
```

`loadMessagingSdk` memoizes the in-flight import, so concurrent calls share one load. Both loaders reject a failed load with a `MessagingSdkLoadError` and permit a retry:

```ts
import { MessagingSdkLoadError } from "@ada-cx/messaging-sdk";

try {
  await loadAdaMessaging({ handle: "<your-handle>" });
} catch (error) {
  if (error instanceof MessagingSdkLoadError) {
    console.error(error.code); // "invalid_cdn_base" | "invalid_build_sha" | "unstamped_build_sha" | "sdk_import_failed" | "sdk_module_invalid"
  }
}
```

### One interface per page

Only one messaging interface can run on a page, and `loadAdaMessaging` enforces this for you. Repeated calls return the same memoized promise and resolve to the same interface. The first call's settings win, and later calls never start a second widget. The call is therefore safe in code that runs more than once, such as React Strict Mode development effects. You do not need to cache the promise yourself. A failed load or start clears the memo, so a later call can retry.

Control the widget through the returned interface. `stop()` tears the widget down, and a later `start(settings)` on the same interface starts it again. A `loadAdaMessaging()` call after `stop()` returns the same stopped interface. It does not restart the widget. If web chat is off, `loadAdaMessaging()` keeps the same interface without mounting a widget; call [`start()`](/messaging/web/sdk-api-reference#start) to retry.

If you create interfaces yourself with `createAdaEmbedInterface`, only one can start. A second interface's `start()` rejects with the `AdaEmbedError` code [`start_owner_conflict`](/messaging/web/sdk-api-reference#start).

### Use the types

Import any public type directly. Type imports add zero runtime code.

```ts
import type {
  AdaMessagingInterface,
  AdaMessagingSettings,
  AdaMessagingStartOptions,
  PublicMessage,
  ResetParams,
} from "@ada-cx/messaging-sdk";
```

`AdaMessagingInterface` is the interface `loadAdaMessaging` resolves. The `AdaEmbedInterface` name remains available as its legacy alias.

Runtime symbols such as `AdaMessagingClient` and `createAdaEmbedInterface` are exported as types only. Get their implementations from `loadMessagingSdk()`. `createAdaEmbedInterface` is the legacy `window.adaEmbed` compat facade; new integrations use `loadAdaMessaging`.

### Test without the CDN

The `./testing` entry provides an in-memory mock of the messaging interface, so your unit tests never touch the network.

```ts
import { createMockAdaMessaging } from "@ada-cx/messaging-sdk/testing";

const ada = createMockAdaMessaging();

await ada.sendMessage("hello");
expect(ada.calls).toContainEqual({
  method: "sendMessage",
  args: ["hello"],
});
```

The mock records every call in `calls`, and `$emit(key, data)` simulates SDK events for your subscribers. Pass overrides to replace any method with your own spy.

### Bundler note

The loader imports the runtime with a fully dynamic URL. The import is pre-annotated with `/* webpackIgnore: true */` and `/* @vite-ignore */`, so webpack and Vite leave it for the browser to resolve.

> **Warning**
>
> Do not copy the CDN import into your own code without those annotations. Without them, the bundler tries to resolve the `https:` URL at build time and fails, or rewrites the import so it never reaches Ada's CDN. If your bundler still rewrites dynamic imports, configure it to ignore imports of `https:` URLs.

## Migrate from the Chat SDK web script

If your site already loads Ada with the Chat SDK script tag, keep it. No code change is required.

```html
<script
    id="__ada"
    data-handle="<your-handle>"
    src="https://static.ada.support/embed2.js">
</script>
```

The loader keeps the `window.adaEmbed` global, and Ada switches the runtime behind it per handle. `window.adaSettings` remains fully supported, and legacy callbacks such as `adaReadyCallback`, `chatterTokenCallback`, and `conversationEndCallback` keep working.

The globals stay supported for this path indefinitely. They are deprecated only for new installations, which should use the [module script](#embed-with-a-module-script) or the [npm package](#install-from-npm). See [Security best practices](/messaging/web/security-best-practices#prefer-module-imports-for-new-installations).

If you replace the legacy script tag with the npm loader, update the page's CSP.
Add `https://messaging-assets.ada.support` to `script-src`, `connect-src`, and `frame-src`.
Alternatively, reuse your existing `static.ada.support` allowance with `cdnBase: "https://static.ada.support/messaging/"`.
Allow the selected host in all three directives.

For new code, use the module or npm loader and event subscriptions.
Existing `embed2.js` pages keep their script tag and supported customizations without code changes.

If your page uses these customizations, review the remaining differences:

| Legacy customization                                                 | Messaging behavior                                                                   |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `createProactive` bubble                                             | Not supported. Use `triggerProactive` for configured proactive messages.             |
| `ada:end_conversation`                                               | Delivers a narrower conversation payload. Do not rely on undocumented legacy fields. |
| `setLanguage` and `triggerAnswer`                                    | Return promises. Await completion when later code depends on them.                   |
| `preload`, `reset({ activeModality })`, and start-time `embedStyles` | Ignored with a warning.                                                              |
| `ada-x-storage-frame`                                                | No matching host element.                                                            |

For new code, prefer event subscriptions over legacy callbacks:

```ts
await window.adaEmbed.subscribeEvent("ada:end_conversation", (event) => {
  console.log("Conversation ended", event);
});
```

> **Note**
>
> The transcript-bearing events `ada:message:sent` and `ada:message:received` are exact-subscription only, and they require the [`enableProgrammaticControl`](/messaging/web/sdk-api-reference#enableprogrammaticcontrol) setting. They are not delivered through `subscribeAll()`.

## Migrate from the embed2 npm package

If your build installs `@ada-support/embed2`, swap the dependency. This is the one
integration path that needs a code change.

```sh
npm uninstall @ada-support/embed2
npm install @ada-cx/messaging-sdk
```

The old package bundles the Ada runtime into your build. The new package does not. It
carries types, test mocks, and a loader that imports the runtime from Ada's CDN at page
load. Your users get fixes without a rebuild on your side.

### Update the import

`@ada-support/embed2` exports a ready-made interface as its default export. You then
call `start`.

```ts
// Before
import adaEmbed from "@ada-support/embed2";

await adaEmbed.start({ handle: "<your-handle>" });
```

`@ada-cx/messaging-sdk` exports `loadAdaMessaging`. One call loads the runtime, starts
the AI Agent, and returns the interface.

```ts
// After
import { loadAdaMessaging } from "@ada-cx/messaging-sdk";

const ada = await loadAdaMessaging({ handle: "<your-handle>" });
```

Hold the returned interface in module scope. Call your existing actions on it. See the
[SDK API Reference](/messaging/web/sdk-api-reference) for the current surface.

### Add the CSP directives

The runtime now loads from Ada's CDN, so your Content Security Policy must allow it.
Add the asset-host directives and your AI Agent's API host, listed in [Content Security Policy](#content-security-policy) before you deploy. A missing directive blocks the
widget silently.

### What carries over

Your AI Agent configuration carries over unchanged. You do not reconfigure the Agent.

`window.adaSettings` keeps working if your page sets it. Ada honors it for this path.
Settings you pass to `loadAdaMessaging` take precedence.

## Frame layout

Every web install uses the same DOM IDs and default frame geometry, including module scripts, npm, and `embed2.js`.
Messaging creates `#ada-entry` as its container. The frames are direct children, including when you set `parentElement`.
The launcher, chat, and bubble use `ada-button-frame`, `ada-chat-frame`, and `ada-intro-frame` as their IDs, classes, and names.
The optional mask uses `ada-drawer-mask-frame` as its ID. It is a sibling div after the launcher.
Existing page elements with these IDs remain unchanged.
Rules that hide or transform `#ada-entry div:first-child` do not affect the frames.

The launcher frame sits at the viewport edge with a 24px transparent band around the button.
The desktop drawer has 24px bottom and side gaps. Its default limits are 375px wide and 720px high.
The bubble frame uses a 52px bottom band and a 24px side band, with an 8px gap above the button.
Mobile drawers use the visual viewport height and stay anchored to the top left without a bottom inset.
See [Frame layout](/messaging/web/sdk-api-reference#frame-layout) for CSS dimensions and supported page overrides.

## Validate a specific runtime version

The runtime normally resolves from Ada's rollout at page load. To validate a specific build on a staging page, append the `ada-messaging-version` query parameter to the page URL:

```
https://staging.example.com/checkout?ada-messaging-version=<40-character-sha>
```

Use a 40-character hex git SHA of a deployed build. Ada serves each build under
an immutable SHA root. The SHA your page loaded is therefore visible in the resolved
runtime URL: `<asset-host>/<sha>/sdk/index.js`. Read it from the network panel or from
the loaded module URL.

Use it for regression testing. Record the SHA a page loaded while it worked. Pin that
SHA later to compare behavior against the current rollout.

The pin bypasses the rollout while the parameter is present on the page URL. An invalid
or unavailable value falls back to the stable rollout.

The SDK reads this parameter from the page URL. Your page cannot disable it. A crafted
link can therefore load an older deployed build for any visitor, which can predate a
security fix. The SDK validates the identifier's format and attempts to load it from Ada's asset host.
Treat the parameter as a debugging affordance rather than a production control.

To point the npm loader at a different asset host, pass `cdnBase`:

```ts
await loadAdaMessaging(
  { handle: "<your-handle>" },
  { cdnBase: "<asset-host>" },
);
```

The preferred production root is `https://messaging-assets.ada.support/`.
For production pages whose CSP already allows `static.ada.support`, use `cdnBase: "https://static.ada.support/messaging/"`.
Without `cdnBase`, the npm loader automatically retries once from the static root when the default import fails, including CSP and network failures.
The retry preserves `pinBuildSha` and its `<sha>/sdk/index.js` path.
If the default load fails, a second copy of the SDK on the page can still load from the static root.

An explicit `cdnBase` disables automatic fallback.
Module-invalid and validation errors retain their original codes and messages.
If both imports fail, the loader rejects with `sdk_import_failed` and names both URLs.
After a successful fallback, the loader logs one warning per page, without debug logging.
The warning includes the original error as its second console argument.
A `TypeError` from the default import produces the fetch-failure warning:

```text
[Ada] Loaded the Messaging SDK from https://static.ada.support/messaging/ because the default root https://messaging-assets.ada.support/ was blocked or unavailable. Add https://messaging-assets.ada.support to script-src, connect-src and frame-src, or pass cdnBase: "https://static.ada.support/messaging/".
```

For a CSP block, update your host's policy as shown in the fetch-failure warning.

Other errors produce the evaluation-failure warning:

```text
[Ada] Loaded the Messaging SDK from https://static.ada.support/messaging/ because the module at https://messaging-assets.ada.support/sdk.js failed to evaluate (Error: boom). This is an Ada-side problem, not your host's CSP. Report it to Ada.
```

For an evaluation-failure warning, report the original error to Ada.
Network failures also produce the fetch-failure warning.
A module that throws `TypeError` during evaluation also produces the fetch-failure warning, so the classification does not prove a CSP block.

Successful loads from the requested root produce no fallback warning.
For staging validation, Ada can provide another asset root.
The `cdnBase` value must name an Ada-operated host.
Ada does not support mirroring the runtime or serving it from your own host.
See [Why the SDK loads the current build](/messaging/web/security-best-practices#why-the-sdk-loads-the-current-build).

### Pin the CDN build

> **Warning**
>
> Pinning is not recommended for production. A pinned runtime misses Ada's fixes and rollouts. A pinned runtime can also predate later server contract changes and stop working.

The npm package exports `CDN_BUILD_SHA`. The value is the git commit SHA of the Ada commit your installed npm version was published from. Ada deploys each commit's runtime under an immutable SHA root on the CDN, so the value names the CDN build associated with your npm version.

Pass the SHA as the `pinBuildSha` loader option. The loader then skips the rollout and imports that exact build:

```ts
import { CDN_BUILD_SHA, isCdnBuildShaStamped, loadAdaMessaging } from "@ada-cx/messaging-sdk";

if (isCdnBuildShaStamped()) {
  await loadAdaMessaging({ handle: "<your-handle>" }, { pinBuildSha: CDN_BUILD_SHA });
}
```

Use a pin only to debug an issue, to validate a staged build, or to reproduce a report against a known runtime. The option accepts any full 40-character hex git SHA of a deployed Ada build. Only installed npm packages carry a real `CDN_BUILD_SHA`. In a local checkout the value is a 40-zero placeholder, `isCdnBuildShaStamped()` returns `false`, and the loader rejects the placeholder with the `unstamped_build_sha` error code. In the first minutes after an npm release, a pin can fail with `sdk_import_failed` until the matching CDN deploy completes. `pinBuildSha` composes with `cdnBase` for staged validation.

`DEFAULT_MESSAGING_CDN_BASE` and `FALLBACK_MESSAGING_CDN_BASE` export the two production roots.
When both imports fail, `error.fallbackCause` holds the second failure.

## Content Security Policy

If your site enforces a CSP, allow the Ada asset host in three directives. Also allow
your AI Agent's API host in `connect-src`:

| Directive                    | Value             | Why                                                                                         |
| ---------------------------- | ----------------- | ------------------------------------------------------------------------------------------- |
| `script-src`                 | asset host        | Loads `sdk.js`, which then imports the versioned SDK module                                 |
| `connect-src`                | asset host        | `sdk.js` fetches the rollout manifest to resolve the runtime version                        |
| `connect-src`                | AI Agent API host | The SDK clears the unread badge and reads notification status from your AI Agent's API host |
| `frame-src` (or `child-src`) | asset host        | The SDK mounts its iframes from the asset host                                              |

Current builds position the frames with property writes rather than a style attribute,
so a strict `style-src` does not stop the widget rendering. Builds from before this
change set a style attribute instead, which a strict `style-src` blocks, leaving the
frames mispositioned. That matters only if you pin an older build with the options
below.

A `frame-src` limited to the asset host does block one thing: the SDK's own outage panel,
which it shows if the chat never becomes usable. The panel is a `data:` document, so your
policy blocks it and your end user sees an empty frame. Set
[`showFallbackOnTimeout: false`](/messaging/web/sdk-api-reference#showfallbackontimeout) and
render your own outage
state instead. An exact `subscribeEvent("ada:chat_frame_timeout", ...)` subscription also
suppresses the panel. A `subscribeAll()` listener does not: it receives the event but
still gets the panel. Neither option widens your policy.

Printing an attachment adds nothing to this list. When an end user prints an image, the
widget opens a separate page that Ada hosts. The image and the print styles load under
that page's own policy, not yours.

The asset host depends on how the SDK loads:

* Module script or default npm loader: `https://messaging-assets.ada.support`
* npm loader fallback or explicit static `cdnBase`: `https://static.ada.support`
* Chat SDK script tag (`embed2.js`): `https://static.ada.support`

The recommended allow-list uses `https://messaging-assets.ada.support` in all three directives.
During a migration, allow both production hosts or set `cdnBase: "https://static.ada.support/messaging/"` to reuse the static root.
When CSP blocks the default bootstrap, manifest, or runtime, the npm fallback can load the SDK from the static root.
This fallback requires your policy to allow `https://static.ada.support` in all three directives.

```
Content-Security-Policy:
  script-src  'self' https://messaging-assets.ada.support;
  connect-src 'self' https://messaging-assets.ada.support https://<your-ai-agent-api-host>;
  frame-src   'self' https://messaging-assets.ada.support;
```

Your AI Agent's API host depends on your cluster, not on your handle alone. On the
default cluster it is `https://<your-handle>.ada.support`. On another cluster it takes
that cluster's domain, such as `https://<your-handle>.eu.ada.support`. If you set the
`endpoint` setting, allow that origin instead. Confirm the value in your browser's
network panel before you enforce the policy.

This policy allows the SDK module. It does not allow an inline `<script>` block, so the
inline example in [Embed with a module script](#embed-with-a-module-script) is blocked
under it. Move that bootstrap into a module file your page serves. Or give the inline block a
nonce or hash that your policy lists.

The conversation runtime's own API and WebSocket traffic runs inside a cross-origin
iframe with its own CSP, so your page's policy does not govern it. The SDK in your page
does make a small number of its own API calls, which your policy does govern. They fail
silently, with no error on your page. Without your AI Agent's API host in `connect-src`
the SDK cannot read notification status. A returning end user's live agent session is
then not resumed, and no unread badge appears.

> **Note**
>
> If your policy has no `frame-src`, framing falls back to `child-src` and then `default-src`. A restrictive `default-src` with neither directive blocks the widget without a `frame-src` violation to point at.

Roll out CSP changes with `Content-Security-Policy-Report-Only` first. It surfaces every violation without breaking the page.

### Debug a blocked widget

The browser console names the violated directive and the blocked URL.
The npm loader always warns once after a successful static fallback.
Successful loads from the requested root produce no fallback warning.
To enable additional SDK diagnostics, set `window.adaSettings.debug = true` or run `localStorage.setItem("ada.debug", "1")`.