Security best practices

The Messaging Web SDK runs inside your page, so it inherits your page’s trust boundary. This page collects the practices that keep an integration safe: how to load the SDK, how to scope your references, which settings widen exposure, and how to handle identity tokens.

Prefer module imports for new installations

The window.adaEmbed and window.adaSettings globals remain fully supported for existing installs. That includes pages that load the SDK through the legacy static.ada.support loader path. Those pages need no code change.

For new installations, the globals are deprecated. New installations should import the SDK module from https://messaging-assets.ada.support/sdk.js, or install the @ada-cx/messaging-sdk npm loader.

The reason is simple: a global is reachable by every script on the page. Analytics tags, ad pixels, session replay tools, and any compromised third-party script can read and call window.adaEmbed. A reference held in module scope is reachable only by your own code.

The npm loader’s loadAdaMessaging never assigns the interface to window.adaEmbed or any other global. The returned reference is the only handle. Keep it in module scope, and do not create globals of your own.

Keep the client in a private scope

Never assign the client, or references derived from it, to window or any other global object. Derived references include the interface itself, transcripts returned by getMessages(), event payloads, and subscription ids.

A <script type="module"> block already gives you a private scope. A top-level const in a module is not visible to other scripts:

1<script type="module">
2 import { createAdaEmbedInterface } from "https://messaging-assets.ada.support/sdk.js";
3
4 const adaEmbed = createAdaEmbedInterface();
5 await adaEmbed.start({ handle: "<your-handle>" });
6
7 await adaEmbed.subscribeEvent("ada:end_conversation", (event) => {
8 // handle the event with your own code
9 });
10</script>

Classic scripts

Top-level var and function declarations in a classic <script> become properties of window. Wrap your integration code in an immediately invoked function expression so your state stays inside the closure:

1<script>
2 (function () {
3 let subscriptionId;
4
5 function onConversationEnd(event) {
6 // handle the event with your own code
7 }
8
9 window.adaSettings = {
10 handle: "<your-handle>",
11 onAdaEmbedLoaded: async () => {
12 subscriptionId = await window.adaEmbed.subscribeEvent(
13 "ada:end_conversation",
14 onConversationEnd,
15 );
16 },
17 };
18 })();
19</script>

subscriptionId and onConversationEnd are private to the closure. Only the window.adaSettings contract itself remains global, because the legacy loader requires it.

Anti-pattern

Do not copy this pattern. It parks the client and a transcript on globals, where every script on the page can reach them:

1<!-- Anti-pattern: do not use -->
2<script type="module">
3 import { createAdaEmbedInterface } from "https://messaging-assets.ada.support/sdk.js";
4
5 const adaEmbed = createAdaEmbedInterface();
6 window.adaEmbed = adaEmbed; // any script can now drive the client
7 await adaEmbed.start({ handle: "<your-handle>", enableProgrammaticControl: true });
8
9 window.lastMessages = await adaEmbed.getMessages(); // transcript exposed to any script
10</script>

Limit transcript exposure

Two settings widen what page scripts can see. Both default to off, and both are locked on the first configuration.

  • enableProgrammaticControl unlocks sendMessage, getMessages, getConversation, setComposerText, and setDelegate, plus the ada:message:sent and ada:message:received events. Those events carry full message bodies, including any personal information end users type.
  • headless runs the session with no visual indicator. On a compromised page, an attacker can run conversation activity under the visiting end user’s token without any user-facing signal.

Every script in your host page inherits your page’s trust and can subscribe to the gated events or call the gated methods. Have your product and security teams accept that exposure explicitly before you enable either setting.

Two properties of the event system limit accidental exposure, but not a malicious script:

  • The transcript-bearing events require exact subscriptions. They are never delivered through subscribeAll or prefix subscriptions.
  • While enableProgrammaticControl is off, the gated methods reject and the events are not delivered.

Keep both settings off unless your integration needs them. Scope any subscriber callbacks inside a module or closure, as shown above, so the data they receive stays private.

Protect identity tokens

An identityToken proves who an end user is. Treat it like a credential:

  • Mint tokens only from your backend, and keep your API key server-side.
  • Deliver tokens to the browser over HTTPS only.
  • Never log a token, on the server or the client.
  • Never place a token in a URL, query parameter, or fragment. The SDK sends it in a request body, never in a URL.
  • Mint a fresh token per attempt. Tokens are single use and expire in 15 minutes, so a stored token has no value.
  • Do not hold a token in a global variable. Fetch it, pass it to start() or reset(), and drop your reference.

See the identity guide’s security checklist for the full flow.