Skip to navigation

Iframe-only embed

Use iframe-only embed when your page must not load a script from Ada. Your code controls the Messaging SDK inside an iframe through a MessagePort. Install the host shim from npm. Choose an inline chat or a floating widget.

The embed uses the SDK’s methods, settings, and events. Only the communication medium changes, with the differences below.

In inline mode, a hidden iframe creates no conversation until it becomes visible in the top-level viewport or the end user interacts with chat. A session-dependent SDK call also counts as interaction. If the browser lacks IntersectionObserver, startup uses document visibility instead.

Choose a mode

Use mode: "inline" to place and size your own iframe. Chat fills that iframe. Use mode: "floating" for the standard launcher, greeting cards, campaigns, drawer, and optional mask. The shim creates and clips a transparent iframe. Clicks outside the widget reach your page. Floating startup follows the standard SDK behavior.

Inline setup

You need your AI Agent’s handle and cluster. Ask your Ada team for these values.

  1. Configure Allowed websites with the origins that will contain the iframe. See Launch options.

  2. Add the iframe. Replace YOUR_HANDLE and YOUR_CLUSTER with your AI Agent’s values.

    <iframe
    id="ada-chat"
    title="Chat with support"
    src="https://messaging-assets.ada.support/sdk/embed.html?ada_handle=YOUR_HANDLE&amp;ada_cluster=YOUR_CLUSTER"
    referrerpolicy="origin"
    allow="clipboard-write; microphone; autoplay"
    width="400"
    height="640"
    ></iframe>

    The assets service redirects versionless URLs to the current SDK build.

    If your page uses a Content Security Policy, allow https://messaging-assets.ada.support in frame-src. Keep allow="clipboard-write; microphone; autoplay" for the permissions the chat needs for copying and multimedia features. Do not add a sandbox attribute. The SDK applies the required restrictions to its inner frames. Do not delegate camera, fullscreen, or local-network access. A loopback custom app is unsupported in embed mode.

  3. Install the host shim. Its module has no runtime imports or network loads.

    npm install @ada-cx/messaging-embed
  4. Create the client after the iframe element exists. Set origin to https://messaging-assets.ada.support, without a path or trailing slash. Set the required handle and cluster options to the same values used in the iframe URL.

  5. Subscribe inside onConnect(signal), then call await chat.start({...}). Use ada:ready to detect readiness. start resolves when SDK startup returns; it does not wait for readiness.

import { createAdaEmbedClient } from "@ada-cx/messaging-embed";
const iframe = document.querySelector<HTMLIFrameElement>("#ada-chat");
if (!iframe) throw new Error("Chat iframe not found.");
const chat = createAdaEmbedClient({
iframe,
mode: "inline",
origin: "https://messaging-assets.ada.support",
handle: "YOUR_HANDLE",
cluster: "YOUR_CLUSTER",
onConnect: async (signal) => {
try {
await chat.subscribeEvent("ada:ready", () => {
if (!signal.aborted) iframe.dataset.chatState = "ready";
});
if (signal.aborted) return;
await chat.start({
language: "en",
metaFields: { user_id: "user-123" },
sensitiveMetaFields: { email: "alex@example.com", name: "Alex" },
});
} catch {
if (!signal.aborted) iframe.dataset.chatState = "error";
}
},
});
function removeSupportChat() {
chat.destroy();
iframe.remove();
}

The helper sends your page URL before startup and tracks navigation automatically. The SDK keeps the reported URL across stop and restart. Each new connection sends the host URL again. Set trackPageUrl: false in the helper options to disable navigation tracking.

The helper queues requests until it connects and confirms the iframe document’s identity. It retries connections for up to 30 seconds. See Error codes if connection fails. onConnect runs after each connection, including after an iframe reload. Register subscriptions and start the SDK inside that callback. Its signal aborts when the connection ends.

chat.disconnect() and chat.destroy() end the shim client. Both close ports, reject pending requests, close notifications, and remove listeners. They send disconnect to destroy the SDK and release its subscriptions. Your inline iframe remains in place. Create a new client to reconnect. If the document has ended, the shim reloads the iframe once at its own embed URL. It connects after the fresh document loads. A second ended rejection fails with ended; create a new iframe in that case. Other connection rejections or timeout also end the client. Create a new client to reconnect after a terminal error. Use await chat.stop() followed by await chat.start() for an SDK restart through the same client. An SDK destroy request sent directly over the protocol permits a later SDK start; it does not end the shim.

Floating setup

Configure Allowed websites with the same origins that you use for the standard SDK. Allow https://messaging-assets.ada.support in your page’s frame-src policy. Install the shim and call it after <body> exists:

npm install @ada-cx/messaging-embed
import { createAdaEmbedClient } from "@ada-cx/messaging-embed";
const chat = createAdaEmbedClient({
mode: "floating",
handle: "YOUR_HANDLE",
cluster: "YOUR_CLUSTER",
title: "Ada chat",
zIndex: 2147483000,
onConnect: async (signal) => {
if (signal.aborted) return;
await chat.start({
align: "right",
hideMask: false,
language: "en",
toggleCallback: (isOpen) => {
document.body.dataset.chatOpen = String(isOpen);
},
});
},
});

The shim appends one fixed iframe to <body>. It covers your layout viewport and clips itself to the visible widget regions. It sets allow="clipboard-write; microphone; autoplay", lang="en", and no sandbox. An empty clip hides the iframe from pointer input, keyboard focus, and assistive technology. Use title to change its accessible name. The default is Ada chat.

zIndex controls the whole widget’s position in your page’s stacking order. Its default is 2147483000. Give host overlays a higher stacking order when they must cover chat. A modal in the browser’s top layer remains above this iframe. Do not set transform, filter, perspective, or containing-block contain values on <html> or <body>. These styles can move or resize a fixed iframe. The shim logs one warning when it detects them.

If your host page is itself framed, the widget covers that frame’s viewport, not the outer page. Every ancestor must pass the controller rules. Browsers without location.ancestorOrigins, including Firefox, support only a host at the top level.

Use one floating embed per page. Do not load the normal SDK on that page. A second floating client, or a floating client created after an active normal SDK, throws conflict. disconnect(), destroy(), a connection rejection, or a connection timeout remove the created iframe and release ownership. Create a new client after any of these terminal states. stop() and a later start() reuse the client.

The drawer follows the SDK’s keyboard behavior. It is not modal: Tab can reach host controls while the mask shows. Calling open() focuses the outer iframe before the request. Closing returns focus to the launcher. A live embed can cover any part of the viewport while it sends heartbeats, including all host controls. There are no host-enforced coverage bounds. This accepted residual matches the authority of a script on the page.

If layout confirmation fails, the requested surface stays hidden. The shim releases every exposed region and scroll lock after six visible seconds without a heartbeat, including a launcher-sized region.

The shim announces navigate.v1 and notifications.v1 by default. Inline clients can set capabilities: [] to disable both, or pass a subset of these names. Floating clients also announce layout.v1 by default and must keep it. For layout without navigation or notifications, use capabilities: ["layout.v1"].

With navigate.v1, same-window links navigate your page through a hidden link with rel="noopener noreferrer". This host.navigate path sends no referrer. The SDK validates destinations against its safe protocols and the AI Agent’s configured allowedProtocols. The shim requires an absolute URL. By default, it allows only http:, https:, mailto:, and tel:. Pass navigation to createAdaEmbedClient to control this policy:

const navigation = {
allowedProtocols: ["https:", "mailto:", "myapp:"],
beforeNavigate: (url: string) => {
const destination = new URL(url);
return destination.protocol !== "https:" || destination.origin === location.origin;
},
};

allowedProtocols replaces the default list. Include a colon in each protocol name. An empty list refuses every destination. Default HTTP(S) navigation needs no user gesture, matching the SDK. This is an accepted residual. Restrict destinations with navigation.beforeNavigate and schemes with navigation.allowedProtocols.

beforeNavigate receives the absolute URL. Returning false or throwing refuses navigation. Executable and local-resource schemes remain blocked even if you list them, including javascript:. Custom schemes also need the AI Agent’s startup allowedProtocols setting before the SDK relays them. A refusal drops the URL and reports navigation_refused through ada:error. It does not open a new tab.

A compromised embed can still navigate within your allowed set, as a script on your page can. The default allows any HTTP(S) destination. Use beforeNavigate when you need to restrict destinations. Without this capability, same-window targets open in a new tab.

New-tab link cards and CTAs use noopener,noreferrer and send no referrer. Message-text links use rel="noopener" and referrerPolicy="origin". Article links open with noopener only. Message-text links and article links send Ada’s origin as the referrer. They open from the Ada frame and do not use host.navigate.

Call requestNotifications() from a user gesture on your page:

document.querySelector("#enable-notifications")?.addEventListener("click", async () => {
await chat.requestNotifications();
});

The shim reports host permission on connection and when it changes. Chat offers no enable action before permission is granted. Notifications appear only with granted permission while the page is hidden or chat is closed. At most 20 remain open. A click focuses your page, closes the notification, opens chat, and emits ada:web_notification:click with conversation and message IDs. Notification text reaches the shim only. It never reaches SDK event subscribers or relayed window events. The shim forwards service-worker messages with type: "ada:web_notification:click" to handleNotification. Without notifications.v1, notifications remain unsupported. A direct SDK requestNotifications protocol request returns current permission without prompting.

How it maps to the SDK

Use the SDK’s Actions, Settings, and Type signatures as the API reference. Invoke each available SDK method as await chat.<method>(...), with the SDK’s arguments and results. Subscribe through await chat.subscribeEvent(eventName, callback) using the SDK’s events and payloads. Use await chat.unsubscribeEvent(id) to remove a subscription.

Callbacks run on your page with the SDK’s behavior and permission checks. For zdChatterAuthCallback, return a token, null, or a promise for either value instead of accepting a response callback. Register subscriptions before start. Wait for ada:ready before using methods that require readiness.

Window events

The helper re-dispatches these two public events as CustomEvents on the host window. Read them with window.addEventListener(name, ...). The event’s detail contains the payload. Window events are not relayed while the helper is disconnected or checking document identity after a load. Internal integration commands and results stay inside the embed. The bridge and shim each rebuild event details from the same allowlist, including when the embed sends extra fields. They read only own data properties on plain objects, without invoking getters. They drop unlisted fields and values with the wrong type. Unknown string categories become other. Identifiers permit 1 to 64 characters from [A-Za-z0-9_.:-]. Queue positions must be finite numbers from 0 through 100000. Invalid identifiers and numbers are omitted. Message text, agent identity, credentials, and free-form payload objects are never relayed.

NamePayload
ada:errorOptional strings: code, type, frame. Required message: the fixed category "embed_error". No source message, stack, or URL.
ada:integration:eventOptional strings: integration, event, frameId, type, connectionStatus. Optional finite numbers: position, queuePosition. Optional boolean: resume.

Integration state fields come from the internal payload and appear directly in the host event’s detail. The core receives the full integration payload so handoffs keep their existing behavior.

Differences from the SDK

AreaDifference
Settings fixed by the iframe URLcluster, domain, endpoint, handle, and rolloutOverride cannot be supplied to start, including after a restart.
Settings the embed ownsIn inline mode, align, hideMask, lazy, onAdaEmbedLoaded, parentElement, and toggleCallback cannot be supplied to start. Your page controls the outer iframe.
Floating settingsFloating mode accepts align, hideMask, branding.launcher, and the toggleCallback slot. It still owns lazy, onAdaEmbedLoaded, and parentElement.
Launcher brandingInline mode rejects branding.launcher. Floating mode uses the SDK launcher settings. Other branding fields use the SDK’s behavior.
Browser methodsThe shim owns permission prompting. See Navigation and notifications.
Internal methodsacknowledgeSessionMirrorClear, deliverSessionMirrorSeed, and setTestModeBanner return unsupported, even though they appear on the SDK client.
Host contextThe helper sends the page URL before startup and tracks navigation. Only HTTP(S) URLs on the bound parent origin are accepted. Without a valid URL, initialURL stays null, _ada_url is omitted, and URL triggers do not run.
Proactive messages and greetingsInline mode suppresses proactive surfaces and card greetings. Floating mode renders them as the standard SDK does.
Custom appsThe localhost exception does not apply because the SDK runs on Ada’s origin. Ada-origin custom apps remain rejected.
NavigationSame-window links use the host capability and navigation policy. Refused URLs are dropped. Without the capability, links open in a new tab.
StorageAda storage is partitioned by host site. Frequency limits, greeting deduplication, and drawer continuity reset when the host site changes and differ from a normal SDK install on the same site.
Host settingswindow.adaSettings and window callbacks are unsupported. Send settings through start and use declared callback slots.
Host stylesHost CSS for Ada frame IDs does not reach the widget. Size the outer iframe in inline mode. Use SDK settings for floating mode.
Zendesk ChatZendesk sees the Ada embed document as its referrer.

Rejected settings return invalid_params. The SDK defaults remain privateMode: false and crossWindowPersistence: true.

Passing user data

Pass plain values through metaFields for personalization. Use sensitiveMetaFields for sensitive values needed by your configured tools. Use strings, numbers, or booleans as field values. The SDK discards nested metadata objects and arrays. getMetaFields() returns only plain metadata.

Sensitive meta fields expire after 24 hours. Ada does not store them long-term. End-user deletion by email does not find a user whose email was sent only through sensitiveMetaFields.

Send a stable identifier, such as your own user ID, as a plain metaFields value. Use an identifier that contains no contact information. Keep email, name, and similar data in sensitiveMetaFields.

Delete end users with the Bulk End-User Deletion API and its variable_match selector. Set variable to the identifier’s field name and value to its stored string value. The selector matches the latest value exactly. It does not match global or session variables.

Data that your page sets is not proof of identity. If the AI Agent must trust who the end user is, use an identity token. Mint the token on your backend. Pass it as identityToken in start or reset. Keep tokens out of URLs and logs.

After startup, update data with await chat.setMetaFields({ user_id: "user-123" }) or await chat.setSensitiveMetaFields({ email: "alex@example.com" }).

Logout and shared devices

Reset the session when an end user signs out or another person uses the device:

await chat.reset({
metaFields: {},
resetChatHistory: true,
sensitiveMetaFields: {},
});

Wait for the reset before applying data for another end user. The SDK defaults remain privateMode: false and crossWindowPersistence: true. A reload does not guarantee a new session. The example relies on the default allowMetaFieldsInReset: true. If you set it to false, full resets restore baseline plain metadata instead; review the SDK’s reset rules. Pass a fresh identity token to reset when switching to an identified session. A session reset does not replace server-side end-user deletion.

Error codes

Rejected helper operations provide code and message. Use code to handle failures, rather than matching message text.

CodeMeaning and action
already_connectedAnother helper owns this iframe document. Use one helper per iframe; reload the iframe before replacing its owner.
conflictA floating embed or normal SDK already owns this page. Destroy that client before creating a floating embed.
connect_timeoutConnection was not established or confirmed within 30 seconds. Check the final origin, handle, cluster, and Allowed websites rules, then reload.
disconnectedThe connection ended or a reply-bearing callback exceeded its deadline. Restore subscriptions after reconnect; check callback completion.
endedThe document has ended. The shim reloads an inline iframe once. If the fresh document also returns ended, create a new iframe.
internalAn internal failure or result serialization failure occurred. Record the code and operation, without sensitive data, and contact support.
invalidThe connect envelope is invalid. Check its exact v1 fields and limits.
invalid_paramsArguments, settings, wire values, or callback placement are invalid. Check the SDK signature and the differences table.
limit_exceededA message or pending-work limit was exceeded. Reduce payload size or concurrent work and release unused subscriptions.
missing_capabilityThe requested mode needs a capability that the host did not announce.
not_startedThe SDK client does not exist yet. Use start first; event subscriptions and setPageUrl may precede it.
sdk_errorThe SDK or a host callback failed. Check sdkCode for the original AdaEmbedError.code and follow the SDK method’s requirements.
timeoutA posted request exceeded its execution deadline. After a timeout, the outcome is unknown. The operation can still complete. Do not retry side-effecting calls blindly.
unsupportedThe method is unknown or unavailable through the embed. Check its name.
unsupported_modeThe requested mode is not supported. Use inline or floating with a compatible embed build.

Limits

  • Use JSON-compatible values: null, booleans, finite numbers, strings, arrays, and plain objects. Functions work only in SDK callback positions.
  • Each envelope permits 1,048,576 bytes of serialized UTF-8 JSON, depth 32, and 50,000 value nodes.
  • Each connection permits 256 callback registrations, 64 pending callback replies, and 64 pending requests.
  • The shim counts queued requests within the 64-request limit. deleteHistory, reset, selectQuickReply, sendMessage, and triggerPlaybook share at most 32 queued or pending requests.
  • The shim rejects the 33rd such request with limit_exceeded before it takes a slot, leaving 32 slots for other methods.
  • A reply or timeout frees the request’s slot in both limits.
  • The page buffers four connection candidates. Request and callback IDs must be non-empty strings of at most 128 characters.
  • The connection deadline is 30 seconds, including waits for document confirmation after a load. Repeated loads do not extend this deadline.
  • Execution deadlines start when the shim posts the request to the iframe. Time queued for connection does not consume the execution deadline.
  • During document confirmation, the shim buffers up to 64 incoming messages and, separately, 64 callback completions and navigation replies.
  • Confirmation of the same document sends buffered completions in order. A replacement document discards them. The shim never invokes a host callback again.
  • Request expiry returns timeout, frees its slot, and releases callbacks and subscriptions on both sides. It ignores late answers.
  • After a timeout, the outcome is unknown. The operation can still complete. Do not retry side-effecting calls blindly.
  • start returns when the SDK start call returns, before readiness, so it uses 30 seconds. Subscribe to ada:ready for readiness.
MethodSDK maximumExecution deadline
deleteHistory125 seconds plus 600 seconds of visibility waiting730 seconds
getInfo35 seconds queued inside the SDK plus 15 seconds for the response55 seconds
reset125 seconds plus 600 seconds of visibility waiting730 seconds
selectQuickReply120 seconds plus 600 seconds of visibility waiting725 seconds
sendMessage5 seconds for beforeSend, then 120 seconds plus 600 seconds of visibility waiting730 seconds
triggerPlaybook120 seconds plus 600 seconds of visibility waiting725 seconds
Other RPC methodsSynchronous, immediate, or at most 5 seconds for asynchronous reads and setComposerText30 seconds

The longer deadlines include five seconds beyond the SDK maximum. Visibility waiting allows an operation to wait while its chat is hidden. getConversation, getMessages, getMetaFields, and setComposerText have five-second SDK bounds; isOpen resolves immediately. requestNotifications runs on your page and waits for browser permission. The SDK sets no maximum for this wait; the shim adds none.

Over-limit requests fail with limit_exceeded. The embed drops invalid or oversized events and callbacks and reports the first drop through ada:error.

Message protocol

Use the helper above unless your team needs its own transport. The types-only @ada-cx/messaging-sdk/embed-protocol export defines the envelope shapes. Every envelope includes protocol: "ada-embed" and v: 1. Only connect uses window.postMessage, with the exact final iframe origin and one transferred MessagePort. All subsequent messages use that port.

KindPurpose
already_connectedReturns the attempt’s nonce and the documentId already owned by another port, then closes the candidate port.
callbackInvokes a registered callbackId with args and an invocationId.
callback_resultSettles a reply-bearing callback by invocationId, with ok and result or error.
connectStarts an attempt with a fresh 128-bit random nonce, encoded as 22 base64url characters without padding. Required fields also include pageUrl, mode, capabilities, and shimVersion. Invalid URL strings are ignored.
connect_rejectedReturns nonce, documentId, handle, cluster, sdkVersion, and reason, then closes the port.
connectedConfirms nonce, documentId, handle, cluster, sdkVersion, mode, and accepted capabilities.
disconnectEnds the connection and destroys the SDK. Releases all subscriptions, callbacks, and pending replies.
host.escape_listenSets the current generation’s host Escape relay with gen and active.
host.keyRelays an unprevented host Escape key with gen and key.
host.navigateSends a validated absolute url and runtime generation gen to the shim.
host.navigate_resultReturns gen, url, and ok. A refusal or failure returns false and reports an error without opening a new tab.
host.notificationSends gen, id, title, optional body and icon, plus conversation_id and message_id.
host.notification_clickReturns gen and id for a clicked notification.
host.notification_stateReports permission: default, granted, denied, or unsupported.
host.resumeReports a back-forward cache restore with gen so the embed resends its state.
host.scroll_lockSets the current generation’s single scroll lock on both document roots with gen and locked.
host.viewportReports rev, layout width and height, and visual width, height, offsets, and scale.
layout.appliedConfirms the current gen and seq after the clip is set and one animation frame passes.
layout.clipSends the complete gen, seq, and rects state, with optional full: true.
layout.heartbeatConfirms liveness with gen every two seconds while any applied clip region is exposed or scroll lock is held.
layout.resetReports a local rollback with gen after six visible seconds without a heartbeat.
release_callbacksReleases the registrations named in callbackIds.
release_requestReleases an abandoned request by id and its registrations by callbackIds. Removes matching subscriptions. The operation may still complete.
requestInvokes an SDK method with positional args and a unique pending request id.
responseSettles a request by id, with ok and result or error; void results use null.
window_eventRelays a window event’s name and detail for dispatch as a host CustomEvent.

Connect accepts exactly the listed fields. Capabilities are the only v1 extension point; unknown capability names are ignored. Send at most eight capability strings of up to 32 characters. shimVersion is a semver string of up to 32 characters. The known names are layout.v1, navigate.v1, and notifications.v1. Inline mode accepts the last two. Floating mode requires layout.v1 and can accept all three. The mode cannot change during the embed document’s lifetime.

Check protocol, version, nonce, and the 22-character base64url document ID before interpreting an answer. Handle connect_rejected before success validation. For connected, verify handle, cluster, and mode against your options. After each iframe load, the shim quarantines the connection until a new attempt confirms the current documentId. It buffers at most 64 validated old-port messages and queues unsent requests within the 64-request limit. It hides the floating layer and restores host scrolling immediately. A same-document answer processes buffered messages in order, then sends unsent requests once. A new document discards the buffer. Buffer overflow ends the connection with limit_exceeded. The shim never resends a request already sent. It requests fresh layout state after confirmation. A new document aborts the old signal, rejects held and pending requests with disconnected, and releases callbacks before binding. Keep confirmed ports by document ID until a later load proves them stale. Replace functions only in declared callback positions with { "$adaCallback": "unique-id" }. Process callbacks independently of pending requests so callbacks can invoke SDK methods. The host shim implements connection recovery, callback deadlines, and cleanup.

Security model

The embed checks Allowed websites with these rules. Missing or invalid configuration refuses control.

Entry or conditionRule
Empty listAllows any HTTP or HTTPS page.
* or http://*Allows any HTTP or HTTPS page.
https://*Allows only HTTPS pages, on any port.
Bare host, such as example.comUses HTTPS. Entries are trimmed and converted to lowercase.
*.example.comMatches subdomains, including nested subdomains, but not the apex example.com.
SchemeAn HTTPS host entry never allows HTTP. An HTTP host entry also allows HTTPS, subject to the port rule.
PortA portless entry matches only the page scheme’s default port. An explicit port must match; host:* allows any port.
Path, query, fragment, credentials, or trailing colonThe embed ignores the entry. A bare trailing slash is allowed.
Non-empty list with no usable entriesAllows nothing.
Opaque, null, or file: parentNever connects, even with an empty list or wildcard.
Ancestor framesThe parent and every ancestor frame must match.

Without location.ancestorOrigins, including Firefox, only a direct child of the top window can connect. The embed grants no more access than the SDK; keep the client and port private within your module.

Troubleshooting

SymptomCheck
The frame receives HTTP 403Restricted Agents use the same edge policy as the SDK’s core page. The Referer check compares Allowed websites entries verbatim. Use the same entries that work with the SDK.
The frame loads but never connectsCheck the controller and ancestor rules above. Config errors and invalid Allowed websites lists refuse control.
Floating chat is misplacedRemove containing-block styles on the document roots and check your host overlay stacking order.
Notifications do not appearCall the shim permission method from a host user gesture. Permission must be granted, and chat must be closed or the page hidden.
The client fails after a timeout or rejectionCreate a new shim client. Reload the iframe first if its document still belongs to another connection.