Loops AI Docs

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

LayerWho sets itWhenPurpose
1. Agent style configLoops AI configurator (published to CDN)Before deployTheme, widget mode, sidebar-on-PDP, search button defaults, copy, colors
2. init configYour site (LoopsAI.init or queue)Page loadagentId, initial productContext / userContext, optional overrides (hide bubble, disable built-in Ask AI)
3. Runtime APIYour site (click, route change, login)After embed.js is liveUpdate 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

PropertyTypeDescription
livebooleantrue after embed.js finished bootstrapping
qArrayCommand queue used before the script loads (see below)
versionstringEmbed 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

FieldTypeRequiredDescription
agentIdstringYesWeb channel agent ID from configurator
productContextobjectNoPage/product state (see Product context)
userContextobjectNoShopper state (see User context)
defaultLocalestringNoFallback locale (en, tr, …)
hideBubbleOnInitbooleanNoHide floating bubble; use your own open CTA
embedEnabledbooleanNofalse removes widget UI entirely
openAsSidebarOnProductbooleanNotrue docks the panel as a sidebar on PDPs (with pageType: "PDP"); set it per page in your code — overrides the configurator default
searchButtonEnabledbooleanNofalse disables injected Ask AI next to search inputs
searchEscalationEnabledbooleanNofalse disables search escalation wiring
positionstringNobottom-right | bottom-left | top-right | top-left
themestringNolight | dark (override; prefer configurator)
offsetX, offsetYnumberNoBubble offset (px)
width, heightnumberNoPanel size (px)
zIndexnumberNoStacking order on your page
baseUrl, apiBasestringNoStaging / 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.


Your integration should update context when the route or product changes, not only on first load.

Page typeproductContextRuntime callWidget behavior
Product detail (PDP)pageType: "PDP", productCode (catalog code), optional productName, price, urlinit (with openAsSidebarOnProduct: true) + updateContext on variant changeSidebar layout when you send openAsSidebarOnProduct: true; assistant uses catalog product
Listing / category (PLP)Omit pageType or clear productCodesetProductContext({}) or updateContext({}, user)Default bubble/bottom-bar; no PDP sidebar
Home / staticNo product fieldssetUserContext only if neededGeneral assistant
Cart / checkoutUsually no productCode; keep userContextupdateContext after login stepSame; 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.

ScenarioApproachConversation state
Checkout = full page loadembedEnabled in initN/A (fresh load each time)
Checkout = SPA route changeToggle shadow-root elements + close()Preserved — session persists

URL parameters on the host page

Query paramSet byEffect
loopsai_open=sidebarYour links or widget product URLsSidebar mode + auto-open on load
loopsai_open=bottom-barSameBottom-bar mode + auto-open
loopsai_open=bubbleSameClassic 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.

MethodArgumentsWhen to useEffect
init(config)UserConfigOnce per page loadLoads style config, mounts widget, applies initial context
updateContext(productContext?, userContext?)Two optional objectsSPA navigation, login, variant changeMerges into iframe; may auto-open sidebar on PDP transition
setProductContext(ctx)objectProduct-only updateSame as updateContext(ctx, null)
setUserContext(ctx)objectLogin/logoutSame as updateContext(null, ctx)
open()—Custom CTAOpens chat panel
close()—Custom close controlCloses panel
toggle()—OptionalOpen/close
openWithSearch(query)stringYour Ask AI buttonOpens panel; sends search text as escalation
suggestSize()—Size CTA on PDPOpens panel; starts size suggestion flow
openVirtualTryOn(productData?)object optionalVTO CTAOpens panel; starts virtual try-on with product payload
cartResult(result)objectAnswering a loopsai:* cart eventSends your storefront's cart result to the widget. See Add to Cart
isOpen()—DiagnosticsReturns whether panel is open
getStyleConfig()—DebuggingReturns 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).

FieldTypeUsed for
productCodestringRequired for product-aware chat — must match Loops catalog code
productNamestringDisplay + fallback lookup
pageTypestring"PDP" enables sidebar-on-PDP (with openAsSidebarOnProduct: true); other values do not trigger PDP sidebar
categorystringOptional hint
pricestringOptional
currencystringOptional
urlstringOptional current PDP URL
actionTypestringOptional; 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

FieldTypeNotes
idstringRecommended stable shopper ID
customerIdstringLegacy alias for id in older setups
emailstringOptional
namestringOptional
membershipstringOptional segment / tier
height, weight, gender, …stringOptional; 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

  1. Load embed.js and pass agentId.
  2. On each page type, set productContext (PDP: pageType + catalog productCode).
  3. On SPA navigation, call updateContext — do not rely on a single init from a global layout without updates.
  4. Use LoopsAI.open() / openWithSearch() from your buttons when you hide the default bubble or Ask AI.
  5. Keep catalog codes aligned with the Loops product feed.
  6. Prefer configurator for visual and mode settings; avoid duplicating them in init.

Related: Configuration · Product & User Context · JavaScript API

On this page