Host Integration API
Structural reference for page-based context, init options, and the LoopsAI runtime API your site controls.
This page describes what your website can pass into the chat widget and which APIs to call per page. Visual branding and most feature flags come from the Loops AI configurator; your snippet supplies identity, page context, and user actions.
Three configuration layers
| Layer | Who sets it | When | Purpose |
|---|---|---|---|
| 1. Agent style config | Loops AI configurator (published to CDN) | Before deploy | Theme, widget mode, sidebar-on-PDP, search button defaults, copy, colors |
2. init config | Your site (LoopsAI.init or queue) | Page load | agentId, initial productContext / userContext, optional overrides (hide bubble, disable built-in Ask AI) |
| 3. Runtime API | Your site (click, route change, login) | After embed.js is live | Update context, open/close panel, search escalation, size suggestion, VTO |
Best practice
Keep design in the configurator. Use your snippet only for agentId, per-page context, and explicit user actions (open chat, Ask AI, variant change).
Global object: window.LoopsAI
| Property | Type | Description |
|---|---|---|
live | boolean | true after embed.js finished bootstrapping |
q | Array | Command queue used before the script loads (see below) |
version | string | Embed bundle version (when exposed) |
window.LoopSmartAI is the same object (legacy alias).
Bootstrap: queue + init
window.LoopsAI = { q: [] };
window.LoopsAI.q.push([
"init",
{
agentId: "YOUR_AGENT_ID",
productContext: { productCode: "YOUR_PRODUCT_CODE", pageType: "PDP" },
userContext: { id: "user-456" },
searchButtonEnabled: false,
searchEscalationEnabled: false,
},
]);
const s = document.createElement("script");
s.src = "https://chat.loopsai.com/embed.js";
s.async = true;
document.head.appendChild(s);await LoopsAI.init({
agentId: "YOUR_AGENT_ID",
productContext: { pageType: "PDP", productCode: "YOUR_PRODUCT_CODE" },
userContext: { id: "user-456" },
});init(config) — host-controlled fields
| Field | Type | Required | Description |
|---|---|---|---|
agentId | string | Yes | Web channel agent ID from configurator |
productContext | object | No | Page/product state (see Product context) |
userContext | object | No | Shopper state (see User context) |
defaultLocale | string | No | Fallback locale (en, tr, …) |
hideBubbleOnInit | boolean | No | Hide floating bubble; use your own open CTA |
embedEnabled | boolean | No | false removes widget UI entirely |
openAsSidebarOnProduct | boolean | No | true docks the panel as a sidebar on PDPs (with pageType: "PDP"); set it per page in your code — overrides the configurator default |
searchButtonEnabled | boolean | No | false disables injected Ask AI next to search inputs |
searchEscalationEnabled | boolean | No | false disables search escalation wiring |
position | string | No | bottom-right | bottom-left | top-right | top-left |
theme | string | No | light | dark (override; prefer configurator) |
offsetX, offsetY | number | No | Bubble offset (px) |
width, height | number | No | Panel size (px) |
zIndex | number | No | Stacking order on your page |
baseUrl, apiBase | string | No | Staging / custom endpoints only |
Other style fields exist on init but should normally stay in the configurator (colors, widget mode, sidebar width, quick actions, etc.). Read published values with LoopsAI.getStyleConfig() if needed.
Page-based context (recommended pattern)
Your integration should update context when the route or product changes, not only on first load.
| Page type | productContext | Runtime call | Widget behavior |
|---|---|---|---|
| Product detail (PDP) | pageType: "PDP", productCode (catalog code), optional productName, price, url | init (with openAsSidebarOnProduct: true) + updateContext on variant change | Sidebar layout when you send openAsSidebarOnProduct: true; assistant uses catalog product |
| Listing / category (PLP) | Omit pageType or clear productCode | setProductContext({}) or updateContext({}, user) | Default bubble/bottom-bar; no PDP sidebar |
| Home / static | No product fields | setUserContext only if needed | General assistant |
| Cart / checkout | Usually no productCode; keep userContext | updateContext after login step | Same; analytics may label page as non-PDP |
SPA route handler (template)
const ROUTES = {
product: /^\/products\//,
listing: /^\/collections\//,
};
function onRouteChange(pathname) {
const user = { id: currentUserId };
if (ROUTES.product.test(pathname)) {
LoopsAI.updateContext(
{
productCode: getActiveCatalogCode(),
productName: getActiveProductName(),
pageType: "PDP",
url: window.location.href,
},
user,
);
return;
}
LoopsAI.updateContext(
{
pageType: "other",
productCode: "",
productName: "",
},
user,
);
}Details: Product & User Context (PDP sidebar, catalog codes, swatches).
Tutorial: product detail page (PDP) as a sidebar
A common setup: on product pages, dock the assistant as a sidebar that already knows which product the shopper is viewing. You drive this entirely from your own code — three steps.
1. Send the sidebar flag with the product. On the PDPs where you want a sidebar, pass openAsSidebarOnProduct: true in your init config, together with the catalog code and pageType: "PDP":
window.LoopsAI.q.push([
"init",
{
agentId: "YOUR_AGENT_ID",
openAsSidebarOnProduct: true, // you decide this per page, in code
productContext: {
productCode: "YOUR_PRODUCT_CODE", // must match your Loops catalog `code`
productName: "Wide-Fit Running Shoe",
pageType: "PDP",
url: window.location.href,
},
userContext: { id: currentUserId },
},
]);openAsSidebarOnProduct: true together with pageType: "PDP" switches the layout to a sidebar for that page. It does not open the panel on its own. Because you set the flag yourself, you control which products get the sidebar — send it where it fits, omit it where it doesn't.
The configurator toggle is optional
The same Open as Sidebar on Product Pages setting exists in the configurator as a convenience for previewing the behavior while you test, before you wire it into your site. The value you pass in init always wins, so your code stays in control — you never have to enable it in the configurator to use this.
2. Open the panel. Either let the shopper open it from your own CTA:
document
.getElementById("ask-about-product")
?.addEventListener("click", () => window.LoopsAI.open());…or bring them in with ?loopsai_open=sidebar on the URL (product links inside the chat already append this), which opens the sidebar automatically on load.
3. Keep context in sync. When the shopper picks another variant, push the new code; when they leave the PDP, clear it:
// variant change — keep pageType: "PDP"
window.LoopsAI.updateContext({ productCode: "NEXT_CODE", pageType: "PDP" });
// navigated away from the PDP (SPA)
window.LoopsAI.updateContext({ pageType: "other", productCode: "" });On an SPA, the embed auto-opens the sidebar when the shopper moves into a PDP — but only when sidebar mode is active (openAsSidebarOnProduct: true) and the panel isn't already open.
Full walkthrough with color/size swatches and a GTM data-layer reader: Product & User Context.
Hide the widget on specific pages (e.g., checkout)
A common request: keep the assistant available across the storefront, but remove it completely from the purchase / checkout screen — no bubble, no bottom-bar, no chips — and bring it back automatically everywhere else.
There are two valid patterns. Pick based on how your checkout page loads.
A. Checkout is a separate page load (most storefronts, incl. Shopify)
If navigating to checkout triggers a full page load, embed.js boots fresh on that page — so the simplest, most robust fix is a per-page embedEnabled override in your init config. No DOM manipulation needed.
const HIDE_ON_PATHS = [/^\/checkout/, /^\/cart/]; // adjust to your routes
const hideWidget = HIDE_ON_PATHS.some((re) => re.test(window.location.pathname));
window.LoopsAI.q.push([
"init",
{
agentId: "YOUR_AGENT_ID",
embedEnabled: !hideWidget,
// If your agent uses the injected Ask AI search button / search escalation,
// turn it off on this page too — see the note below for why.
...(hideWidget && {
searchButtonEnabled: false,
searchEscalationEnabled: false,
}),
// ...rest of your init config
},
]);embedEnabled is page-scoped here
embedEnabled is the same field your dashboard's Active/Inactive channel toggle publishes to the CDN config. Sending it explicitly from init only overrides it for this page load — your channel stays Active everywhere else, and nothing changes in the dashboard.
With embedEnabled: false and no Ask AI search injection, the embed script exits immediately on that page: no shadow host, no iframe, zero DOM footprint, zero calls to the chat backend.
Why also disable the Ask AI search fields
The injected Ask AI search button (searchButtonEnabled / searchEscalationEnabled) has its own boot path that still runs when embedEnabled: false — so on its own, embedEnabled: false would leave a bottom-bar or sidebar launcher visible if your agent has that feature enabled in the configurator. Setting both search fields to false on the hidden page guarantees a complete exit. If your agent does not use injected Ask AI search, embedEnabled: false alone is enough and you can drop the two extra fields.
B. Checkout is a client-side route inside the same SPA (no page reload)
If your storefront is a single-page app and checkout is just a route change, embed.js only runs once per real page load — calling LoopsAI.init() again on route change would mount a second widget instance instead of updating the existing one, so don't do that here.
Instead, keep the widget mounted and toggle its visible elements directly. The widget's shadow root is created with { mode: "open" } specifically so host pages can do this — the same approach already used to hide the default bubble for a custom CTA.
const HIDE_ON_PATHS = [/^\/checkout/, /^\/cart/];
function setWidgetVisible(visible) {
const root = document.getElementById("loopsmart-ai-widget")?.shadowRoot;
if (!root) return false; // widget hasn't mounted yet
if (!visible) window.LoopsAI?.close(); // never strand an open panel behind a hidden launcher
[
"#bubble", "#close-button", "#chips-container", // bubble mode
"#bottom-bar", "#bb-chips-container", // bottom-bar mode
"#sidebar-launcher", // sidebar mode
].forEach((selector) => {
const el = root.querySelector(selector);
if (el) el.style.display = visible ? "" : "none";
});
return true;
}
function applyWidgetVisibilityForRoute(pathname, attempt = 0) {
const shouldHide = HIDE_ON_PATHS.some((re) => re.test(pathname));
const applied = setWidgetVisible(!shouldHide);
// The widget may not have mounted yet on the first paint after navigation.
// Retry a few times, then stop — so this never loops forever on a page
// where the widget legitimately isn't present (e.g. embedEnabled: false).
if (!applied && attempt < 10) {
setTimeout(() => applyWidgetVisibilityForRoute(pathname, attempt + 1), 200);
}
}
applyWidgetVisibilityForRoute(window.location.pathname);
// Wire into your router's navigation event
onRouteChange((pathname) => applyWidgetVisibilityForRoute(pathname));This only toggles display — the iframe and the shopper's conversation stay alive in the background, so when they come back from checkout the chat resumes exactly where they left it instead of restarting.
The selector list covers all three widgetMode values (bubble, bottom-bar, sidebar); whichever mode isn't active simply has no matching element in the DOM, so the call is a safe no-op for the rest.
| Scenario | Approach | Conversation state |
|---|---|---|
| Checkout = full page load | embedEnabled in init | N/A (fresh load each time) |
| Checkout = SPA route change | Toggle shadow-root elements + close() | Preserved — session persists |
URL parameters on the host page
| Query param | Set by | Effect |
|---|---|---|
loopsai_open=sidebar | Your links or widget product URLs | Sidebar mode + auto-open on load |
loopsai_open=bottom-bar | Same | Bottom-bar mode + auto-open |
loopsai_open=bubble | Same | Classic bubble + auto-open |
Search text is not passed via URL from your site in the recommended flow — use LoopsAI.openWithSearch(query) instead. See Custom Ask AI Search Button.
Runtime API reference
All methods are on window.LoopsAI (alias LoopSmartAI). Call only when LoopsAI.live === true, except queued init.
| Method | Arguments | When to use | Effect |
|---|---|---|---|
init(config) | UserConfig | Once per page load | Loads style config, mounts widget, applies initial context |
updateContext(productContext?, userContext?) | Two optional objects | SPA navigation, login, variant change | Merges into iframe; may auto-open sidebar on PDP transition |
setProductContext(ctx) | object | Product-only update | Same as updateContext(ctx, null) |
setUserContext(ctx) | object | Login/logout | Same as updateContext(null, ctx) |
open() | — | Custom CTA | Opens chat panel |
close() | — | Custom close control | Closes panel |
toggle() | — | Optional | Open/close |
openWithSearch(query) | string | Your Ask AI button | Opens panel; sends search text as escalation |
suggestSize() | — | Size CTA on PDP | Opens panel; starts size suggestion flow |
openVirtualTryOn(productData?) | object optional | VTO CTA | Opens panel; starts virtual try-on with product payload |
cartResult(result) | object | Answering a loopsai:* cart event | Sends your storefront's cart result to the widget. See Add to Cart |
isOpen() | — | Diagnostics | Returns whether panel is open |
getStyleConfig() | — | Debugging | Returns merged published style config |
Method details: JavaScript API.
Outbound events (widget → your site)
Cart actions arrive as window events (loopsai:addToCart, loopsai:removeFromCart, loopsai:updateQuantity, loopsai:getCart) that you answer with LoopsAI.cartResult. See Add to Cart.
Listen with window.addEventListener("message", …) for widget actions such as closeChat. See Events & PostMessage.
Product context object
Plain object; string values are safest (they are forwarded to the chat API as key/value context).
| Field | Type | Used for |
|---|---|---|
productCode | string | Required for product-aware chat — must match Loops catalog code |
productName | string | Display + fallback lookup |
pageType | string | "PDP" enables sidebar-on-PDP (with openAsSidebarOnProduct: true); other values do not trigger PDP sidebar |
category | string | Optional hint |
price | string | Optional |
currency | string | Optional |
url | string | Optional current PDP URL |
actionType | string | Optional; e.g. size flows (suggest-size) |
You may add custom string keys; avoid sensitive PII. The assistant primarily resolves productCode against your catalog.
User context object
| Field | Type | Notes |
|---|---|---|
id | string | Recommended stable shopper ID |
customerId | string | Legacy alias for id in older setups |
email | string | Optional |
name | string | Optional |
membership | string | Optional segment / tier |
height, weight, gender, … | string | Optional; used in size suggestion when present |
TypeScript surface (host page)
interface LoopsAI {
live?: boolean;
q?: Array<[string, unknown?]>;
init(config: HostInitConfig): Promise<void>;
updateContext(
productContext?: Record<string, string> | null,
userContext?: Record<string, string> | null,
): void;
setProductContext(ctx: Record<string, string>): void;
setUserContext(ctx: Record<string, string>): void;
open(): void;
close(): void;
toggle(): Promise<void>;
openWithSearch(query: string): void;
suggestSize(): void;
openVirtualTryOn(productData?: Record<string, string>): Promise<void>;
cartResult(result: {
requestId: string;
success: boolean;
reason?: string;
cart?: Record<string, unknown>;
}): void;
isOpen(): boolean;
getStyleConfig(): Record<string, unknown>;
}
interface HostInitConfig {
agentId: string;
productContext?: Record<string, string>;
userContext?: Record<string, string>;
defaultLocale?: string;
hideBubbleOnInit?: boolean;
embedEnabled?: boolean;
searchButtonEnabled?: boolean;
searchEscalationEnabled?: boolean;
position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
theme?: "light" | "dark";
offsetX?: number;
offsetY?: number;
width?: number;
height?: number;
zIndex?: number;
}
declare global {
interface Window {
LoopsAI: LoopsAI;
LoopSmartAI: LoopsAI;
}
}Integration checklist
- Load
embed.jsand passagentId. - On each page type, set
productContext(PDP:pageType+ catalogproductCode). - On SPA navigation, call
updateContext— do not rely on a singleinitfrom a global layout without updates. - Use
LoopsAI.open()/openWithSearch()from your buttons when you hide the default bubble or Ask AI. - Keep catalog codes aligned with the Loops product feed.
- Prefer configurator for visual and mode settings; avoid duplicating them in
init.
Related: Configuration · Product & User Context · JavaScript API