Security best practices
Part of the Messaging Web SDK runs inside your page and inherits your page’s trust boundary: the loader and the interface it returns. The conversation runtime does not, as the next section explains. 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.
What runs in your page, and what does not
Two different things carry the Ada name, and only one of them runs in your origin.
The loader and the interface run in your page. That is the code you import, the object you hold, and the event callbacks you register. Everything else on this page is about protecting that surface, because it does inherit your trust boundary.
The conversation runtime does not run in your page. The SDK mounts it in a cross-origin iframe served from the Ada asset host, under a Content Security Policy that Ada sets on that document. By default, AI Agent message bodies from the conversation stay behind that frame boundary, as does the conversation runtime’s own API and WebSocket traffic. Your page’s policy does not govern those requests. Several things cross by design, including the conversation transcript on more than one path. Read the list below rather than assuming bodies stay inside.
The boundary is not total. The crossings below are the ones to plan for. Treat the list as the significant cases rather than a closed set. Verify against your own policy reports.
- The end user token, always. The SDK delivers it to your page whenever a new end
user starts a conversation, through the
ada:chatter_tokenevent and the legacychatterTokenCallbackglobal. No setting gates this. Any script running in your page can read it, so treat it as a value your page already holds. See Prefer module imports for new installations. - The end-of-conversation transcript, always. The
ada:end_conversationevent, and the legacyconversationEndCallbackglobal, deliver the conversation transcript with the session and end user identifiers. No setting gates this either. See Limit transcript exposure. - Live agent message bodies, always. The SDK raises browser notifications for messages from a live agent, and the message body is delivered to your page to fill them. No setting gates this. Your page receives the body before the browser decides whether to show a notification, so a denied permission does not keep the body out. AI Agent messages are not delivered this way. See Limit transcript exposure.
- Every other message body, when you opt in.
enableProgrammaticControlexposesgetMessages()and theada:message:sentandada:message:receivedevents, each carrying full message content. See Limit transcript exposure. - The transcript, whenever a Fire Event block runs. If your AI Agent runs a Fire
Event block, Ada enriches the event with the conversation transcript and the end
user’s variables. Ada delivers it to your page. No setting gates this, and registering
no
eventCallbacksdoes not prevent it: your page receives the payload first and the callback check happens after. Treat any AI Agent that fires events as one that sends transcripts to your page. - The whole live agent conversation, during a handoff. Handoffs run in your page. Starting one can deliver the prior conversation to the integration. Each message the end user sends passes through your page on its way to the agent platform. Each message the agent sends comes back the same way. No setting gates this.
- Handoff activity, as a DOM event. During a handoff the SDK dispatches
ada:integration:eventon your page’swindow. For Zendesk Chat the payload carries the text of each agent message with the agent’s display name. Any script on your page can read it withaddEventListener, without holding a reference to the client. This is the most reachable item on this list. See Limit transcript exposure. - Proactive campaign text, always. The SDK passes a campaign’s message text through your page to render it. For a static campaign this is text you already configured. A campaign that starts a playbook instead produces text your page did not supply. No script on your page can read either through an Ada event, and the campaign events carry no bodies.
- Per-message metadata and conversation state, always.
ada:conversation:messagepublishes the sender, the message id and the conversation id on every message.ada:conversation:change,ada:typing:start,ada:typing:stop,ada:agent:joined,ada:agent:left,ada:connection:changeandada:analyticspublish on the same ungated contract, andgetInfo()andgetMetaFields()read without a gate. None of these carry message bodies. - Network calls from your page. The SDK clears the unread badge and reads
notification status from your AI Agent’s API host, so your
connect-srcmust allow it. See Content Security Policy.
Two consequences are useful in a security review:
- Your host-page exposure is the asset host in three CSP directives, plus your AI Agent’s API host in
connect-src. See Content Security Policy. - You do not need to allow the conversation runtime’s API or WebSocket endpoints. That traffic originates inside the Ada iframe, under Ada’s policy.
Why the SDK loads the current build
The supported model is the current build, served from the Ada asset host. Ada does not support self-hosting the runtime or bundling it into your application.
The reason is patch latency. When Ada ships a security fix, every integration on the asset host receives it on the next page load. An integration that carries its own copy receives it only after someone notices the fix, reviews the change, rebuilds, and redeploys. That gap belongs to whoever owns the copy.
This is the standard model for embedded third-party scripts that handle sensitive interactions. Stripe.js, Plaid Link, and Google reCAPTCHA all require loading the current script from the vendor, for the same reason.
Pinning a CDN build is a debugging and staged-validation tool. Do not treat it as a version-freezing strategy or as a security control. A pinned integration does not receive security fixes, and Ada cannot support it.
The ada-messaging-version query parameter pins a build the same way. The SDK reads it from the page URL, and your page cannot disable it. A crafted link can therefore load an older deployed build for any visitor. The value is validated to a deployed Ada build, so this is a downgrade surface and not an injection surface.
If your change-management process needs a review window, use these instead of a frozen dependency:
- A published allowlist. The asset host in three directives, plus your AI Agent’s API host in
connect-src. Your attack surface is explicit and reviewable, andContent-Security-Policy-Report-Onlyshows you anything the published list misses. - Iframe isolation. The runtime is sandboxed off your origin, as described above. This is a stronger control than reviewing code that runs in your page.
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. Keep the returned reference in module scope, and do not create globals of your own.
Module scope limits what you expose by accident. It is not an isolation boundary. The SDK keeps its own reference on the page to enforce one client per page. A determined script on your page can still reach the client. Treat every script you load as able to reach the conversation.
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. This reduces accidental exposure. It does not isolate the client from other scripts on your page, so control what you load rather than relying on scope.
Keep meta field methods in a private scope to avoid exposing them through globals. Scripts with a client reference can call reset(), setMetaFields(), and setSensitiveMetaFields(). Set allowMetaFieldsInReset: false to limit the meta fields applied during full resets. The setting does not restrict other meta field methods. With adaEmbed, calling stop() and then start() can create a client with different settings.
A <script type="module"> block already gives you a private scope. A top-level const in a module is not visible to other scripts:
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:
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:
Limit transcript exposure
Live agent message bodies reach your page whatever you configure, to fill browser notifications. Treat them as data your page already holds. The settings below widen the exposure further. Both default to off, and both are locked on the first configuration.
enableProgrammaticControlunlockssendMessage,getMessages,getConversation,setComposerText, andsetDelegate, plus theada:message:sentandada:message:receivedevents. Those events carry full message bodies, including any personal information end users type.headlessruns 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
subscribeAllor prefix subscriptions. - While
enableProgrammaticControlis 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()orreset(), and drop your reference.
See the identity guide’s security checklist for the full flow.