Embedding Widgets

AgentPress ships two embeddable widgets that can be added to any public site with a single <script> tag:

  • Chat widget — real-time conversational agent with support for inline, overlay, sidebar, and collapsible bubble modes.
  • Assessment widget — multi-step assessments (BVA calculator and AI Maturity Assessment).

Both widgets expose a small, imperative JavaScript API on window so you can control them from your own buttons, forms, and analytics code.

Quick Start

Chat widget

<div id="agentpress-chat"></div>
<script>
  window.__AGENTPRESS_CHAT_CONFIG__ = {
    apiKey: "pk_live_...",
    containerId: "agentpress-chat",
  };
</script>
<script src="https://your-api-host/bundles/chat/widget.js" async></script>

The widget's layout — inline, overlay, sidebar, or collapsible — is controlled by the server-side embed credential configuration. You do not need to specify it on the host page.

For overlay, sidebar, and collapsible widgets, you can omit the container div and containerId; the bundle creates its own body-level host.

<script>
  window.__AGENTPRESS_CHAT_CONFIG__ = {
    apiKey: "pk_live_...",
  };
</script>
<script src="https://your-api-host/bundles/chat/widget.js" async></script>

You can also override the saved presentation mode per page while keeping the same credential, agent scope, knowledge base, and theme defaults. For example, use the full overlay bar on a homepage and the lower-right collapsible bubble on interior pages:

<!-- Homepage -->
<script>
  window.__AGENTPRESS_CHAT_CONFIG__ = {
    apiKey: "pk_live_...",
    mode: "overlay",
  };
</script>
<script src="https://your-api-host/bundles/chat/widget.js" async></script>
<!-- Interior pages -->
<script>
  window.__AGENTPRESS_CHAT_CONFIG__ = {
    apiKey: "pk_live_...",
    mode: "collapsible",
  };
</script>
<script src="https://your-api-host/bundles/chat/widget.js" async></script>

The collapsible mode launches as a small lower-corner bubble and expands into a lower-corner chat box. Full-page expand is not part of the launch surface.

To mount more than one chat widget on a page, define window.__AGENTPRESS_CHAT_CONFIGS__ before loading the bundle. Each entry should have a stable id.

<div id="sales-chat"></div>
<div id="support-chat"></div>
<script>
  window.__AGENTPRESS_CHAT_CONFIGS__ = [
    {
      id: "sales",
      apiKey: "pk_live_...",
      containerId: "sales-chat",
    },
    {
      id: "support",
      apiKey: "pk_live_...",
      containerId: "support-chat",
    },
  ];
</script>
<script src="https://your-api-host/bundles/chat/widget.js" async></script>

Assessment widget

<div id="assessment-widget"></div>
<script src="https://your-api-host/bundles/assessment/widget.js"></script>
<script>
  window.AgentPressAssessmentEmbed.mount({
    containerId: "assessment-widget",
    apiBaseUrl: "https://your-api-host",
    apiKey: "pk_live_...",
    // Pick one:
    bvaConfigId: "<bva-config-id>",
    // maturityConfigId: "<maturity-config-id>",
  });
</script>

Imperative Control

Both widgets expose an API on window.AgentPressChat and window.AgentPressAssessmentEmbed respectively. The window.AgentPressChat object exists only after widget.js has executed, unless you install the preloader stub below first.

The default window.AgentPressChat.open(), .sendMessage(), and event methods target the single-widget/default embed. For multiple widgets, keep the instance returned by create() or look it up with get(id).

const salesChat = await window.AgentPressChat.create(
  {
    apiKey: "pk_live_...",
    containerId: "sales-chat",
  },
  { id: "sales" },
);

salesChat.open();
const sameSalesChat = window.AgentPressChat.get("sales");
await sameSalesChat?.sendMessage("I need help with billing");

salesChat.destroy();
window.AgentPressChat.destroyAll();

Widget lifecycle: what ready means

The chat widget boots in a fixed sequence:

  1. widget.js executeswindow.AgentPressChat is installed and any preloader _q queue is drained. Before this point the global does not exist and reading any method on it throws.
  2. The widget authenticates — it exchanges your publishable key (and authToken, if configured) with the AgentPress API and applies the server-side widget configuration. This always involves a network round-trip.
  3. The layout mounts and registers its controller, then the ready event fires.

ready therefore means fully interactive, not just "DOM mounted": authentication has succeeded, the server configuration is applied, and open(), close(), toggle(), and isOpen() work immediately.

How each method behaves at each boot stage:

MethodsBefore widget.js executesAfter execute, before readyAfter ready
open() / close() / toggle()throws (global is undefined)silently does nothingworks
isOpen()throwsreturns falseworks
sendMessage() / newThread()throwsqueued, runs at readyworks
on() / off()throws (unless using the preloader stub)worksworks

Two properties of ready matter when writing integration code:

  • It does not replay. A handler registered with on("ready", ...) after the widget has already booted never fires. Registering during normal page-load script execution is always safe — the authentication round-trip in step 2 guarantees your listener wins the race — but code that can run later (deferred bundles, SPA route mounts) should use the sticky promise instead: window.AgentPressChat.get("default")?.ready.then(...) resolves immediately when the widget is already up. "default" is the instance id of the auto-initialized widget.
  • It can fire more than once. The widget re-emits ready when it re-mounts (destroy() + init(), or a mode change). Treat it as a level — set a flag — rather than doing one-shot work like adding event listeners inside the handler.

In closed-launcher modes (sidebar, overlay, collapsible) the chat panel itself lazy-renders on first open. This does not affect the API: open() and toggle() work at ready, and sendMessage() opens the widget and dispatches once the panel mounts.

All events also dispatch on document as agentpress:* CustomEvents (e.g. agentpress:widget-ready), so code elsewhere on the page can observe the lifecycle with a plain document.addEventListener and no preloader stub.

Chat: open / close / toggle

window.AgentPressChat.open();
window.AgentPressChat.close();
window.AgentPressChat.toggle();
window.AgentPressChat.isOpen(); // boolean

These work for overlay, sidebar, and collapsible modes once the widget is ready (they are silent no-ops before then — see the lifecycle above). In inline mode, open/close/toggle are no-ops (the widget is always visible). When you have multiple chat widgets, call these methods on the specific instance returned from create() or get(id).

Chat: send a message from your own button

// Send into the currently-open thread
await window.AgentPressChat.sendMessage("Ask about pricing");

// Start a new thread with an initial message
await window.AgentPressChat.sendMessage("Ask about pricing", {
  newThread: true,
});
// → opens the widget, creates a new thread, sends the message

Options:

OptionDefaultDescription
newThreadfalseCreate a new thread before sending
agentIdcurrent agentOverride which agent receives the message
openWidgettrue for overlay/sidebar/collapsibleOpen the widget before sending. Set false to send silently

The call returns { threadId } once the message has been dispatched.

Chat: create a new thread

// Empty new thread (useful for "Start a new chat" buttons)
await window.AgentPressChat.newThread();

// New thread with a prefilled first message
await window.AgentPressChat.newThread({
  initialMessage: "I'm interested in the enterprise plan.",
});

Hide the built-in launcher

If you want to drive the sidebar entirely from your own UI — say, a navbar button — set triggerStyle: "none" in your embed credential's sidebar config (via the admin console's Embed settings). The panel mounts offscreen and stays hidden until your code calls .open().

// Your own host-page button
document.getElementById("my-chat-button").addEventListener("click", () => {
  window.AgentPressChat.open();
});

Three launcher options are available:

StyleRenderedTypical use
"tab" (default)Edge-attached tab, always visibleStandard chat widget experience
"fab"Floating action buttonCorner launcher, minimal footprint
"none"NothingHost page provides its own trigger

The legacy showTrigger: false field still works and is treated as triggerStyle: "none".

Open the sidebar automatically on first load

Sidebar widgets support an openByDefault: true flag (configured in the admin console's Embed settings) that opens the panel as soon as the widget mounts. Users can still close it, and subsequent page loads respect the flag again. Pair this with triggerStyle: "none" if you want the widget to open exactly once per page with no persistent trigger.

Events

Subscribe to lifecycle events with .on():

const unsubscribe = window.AgentPressChat.on("opened", ({ mode }) => {
  console.log(`Widget opened in ${mode} mode`);
});

// Later
unsubscribe();

Available chat events:

EventDetail
ready{ mode } — fired once the widget finishes auth + layout resolution
opened{ mode, width?, side? }
closed{ mode }
thread_created{ threadId, agentId }
message_sent{ threadId, text, role: "user" }
message_received{ threadId, text, role: "assistant" } — reserved
voice_started{ threadId } — reserved
voice_ended{ threadId, durationMs? } — reserved

Events also fire as native CustomEvents on document, namespaced agentpress:widget-ready, agentpress:thread-created, etc. Use whichever style fits your codebase:

document.addEventListener("agentpress:widget-opened", (e) => {
  console.log(e.detail); // { mode, width?, side? }
});

Assessment Widget API

The assessment widget exposes a parallel control surface:

// Reset the user's progress and restart from step 1
await window.AgentPressAssessmentEmbed.reset();

// Subscribe to events
window.AgentPressAssessmentEmbed.on("completed", (detail) => {
  if (detail.type === "maturity") {
    console.log("Snapshot:", detail.snapshotId, detail.pdfUrl);
  } else {
    console.log("BVA results:", detail.results);
  }
});

Available assessment events:

EventDetail
ready{ type: "bva" | "maturity" }
step_changed{ step }
completedBVA: { type: "bva", results }; Maturity: { type: "maturity", snapshotId, pdfUrl? }
error{ error: { message } }

open(), close(), and toggle() are reserved on the assessment widget — they are no-ops today (the widget is always inline) but are stable API so host integrations don't have to be rewritten when non-inline assessment modes ship.

TypeScript Types

For host-page TypeScript projects, the widget exposes its type surface on window. You can paste this ambient declaration into your project:

type ChatEvent =
  | "ready"
  | "opened"
  | "closed"
  | "thread_created"
  | "message_sent"
  | "message_received"
  | "voice_started"
  | "voice_ended";

interface AgentPressChatConfig {
  apiKey: string;
  containerId?: string;
  threadId?: string;
  initialUserMessage?: string;
  ui?: Record<string, unknown>;
  mode?: "inline" | "overlay" | "sidebar" | "collapsible";
  theme?: "light" | "dark" | "auto";
  authToken?: string;
  providerName?: string;
  orgSlug?: string;
  overlayConfig?: Record<string, unknown>;
  sidebarConfig?: Record<string, unknown>;
  collapsibleConfig?: Record<string, unknown>;
  sessionToken?: string;
  threadMetadata?: Record<string, unknown>;
}

interface AgentPressChatInstance {
  readonly id: string;
  readonly ready: Promise<void>;
  init(): Promise<void>;
  destroy(): void;
  open(): void;
  close(): void;
  toggle(): void;
  isOpen(): boolean;
  sendMessage(
    text: string,
    options?: {
      newThread?: boolean;
      agentId?: string;
      openWidget?: boolean;
    },
  ): Promise<{ threadId: string }>;
  newThread(options?: {
    initialMessage?: string;
    agentId?: string;
    openWidget?: boolean;
  }): Promise<{ threadId: string }>;
  on(event: ChatEvent, handler: (detail: unknown) => void): () => void;
  off(event: ChatEvent, handler: (detail: unknown) => void): void;
}

declare global {
  interface Window {
    __AGENTPRESS_CHAT_CONFIG__?: AgentPressChatConfig;
    __AGENTPRESS_CHAT_CONFIGS__?: Array<
      AgentPressChatConfig & { id?: string }
    >;
    AgentPressChat?: {
      init(containerId?: string): Promise<void>;
      create(
        config: AgentPressChatConfig,
        options?: { id?: string; containerId?: string },
      ): Promise<AgentPressChatInstance>;
      get(id: string): AgentPressChatInstance | undefined;
      destroy(): void;
      destroyAll(): void;
      setLogLevel(level: "debug" | "info" | "warn" | "error"): void;
      open(): void;
      close(): void;
      toggle(): void;
      isOpen(): boolean;
      sendMessage(
        text: string,
        options?: {
          newThread?: boolean;
          agentId?: string;
          openWidget?: boolean;
        },
      ): Promise<{ threadId: string }>;
      newThread(options?: {
        initialMessage?: string;
        agentId?: string;
        openWidget?: boolean;
      }): Promise<{ threadId: string }>;
      on(event: ChatEvent, handler: (detail: unknown) => void): () => void;
      off(event: ChatEvent, handler: (detail: unknown) => void): void;
      _q?: Array<[string, unknown[]]>;
    };
    AgentPressAssessmentEmbed?: {
      init(containerId?: string): void;
      mount(config: unknown): void;
      destroy(): void;
      open(): void;
      close(): void;
      toggle(): void;
      reset(): Promise<void>;
      on(event: string, handler: (detail: unknown) => void): () => void;
      off(event: string, handler: (detail: unknown) => void): void;
    };
  }
}
export {};

Pre-init Buffering

Because the widget bundle is usually loaded with async, host page code can run before window.AgentPressChat exists. If your page has its own launcher button, do not bind a click handler that immediately calls window.AgentPressChat.toggle() unless you know the bundle has already loaded.

For custom launchers, install a tiny preloader before the async script, bind the click handler once, and gate clicks on the widget's ready event:

<button id="my-chat-button" disabled>Open chat</button>

<script>
  window.__AGENTPRESS_CHAT_CONFIG__ = {
    apiKey: "pk_live_...",
  };

  window.AgentPressChat = window.AgentPressChat || {
    _q: [],
    on: function (event, handler) {
      this._q.push(["on", [event, handler]]);
      return function () {};
    },
  };

  const chatButton = document.getElementById("my-chat-button");
  let chatReady = false;

  // Bind once; the guard makes clicks no-ops until the widget is usable.
  chatButton?.addEventListener("click", () => {
    if (chatReady) window.AgentPressChat.toggle();
  });

  // "ready" can re-fire after a re-mount, so flip a flag here instead of
  // doing one-shot work like adding event listeners.
  window.AgentPressChat.on("ready", () => {
    chatReady = true;
    chatButton?.removeAttribute("disabled");
  });
</script>

<script src="https://your-api-host/bundles/chat/widget.js" async></script>

The widget detects _q on boot and drains it against the real API. The stub only needs to queue on(): by the time ready fires, the real API has replaced the stub, so handlers can call any method. Do not queue control calls like open() in _q — they replay the moment the script executes, before the widget has mounted, and silently do nothing (see the lifecycle table above).

Common Patterns

"Ask about X" CTA button

<button
  onclick="window.AgentPressChat.sendMessage('Tell me about your pricing', { newThread: true })"
>
  Ask about pricing
</button>

Start-over button for an assessment

<button onclick="window.AgentPressAssessmentEmbed.reset()">Start over</button>

Sync a custom button's state with the widget

const button = document.getElementById("my-chat-button");
button?.setAttribute("disabled", "true");
let chatReady = false;

button?.addEventListener("click", () => {
  if (chatReady) window.AgentPressChat.toggle();
});

window.AgentPressChat.on("opened", () => {
  if (button) button.textContent = "Close chat";
});
window.AgentPressChat.on("closed", () => {
  if (button) button.textContent = "Open chat";
});

const markReady = () => {
  chatReady = true;
  button?.removeAttribute("disabled");
};

// Covers both timings: the event fires if the widget is still booting,
// and the sticky promise resolves if it already finished — "ready" does
// not replay for late subscribers.
window.AgentPressChat.on("ready", markReady);
window.AgentPressChat.get("default")?.ready.then(markReady);

Analytics forwarding

window.AgentPressChat.on("message_sent", ({ threadId, text }) => {
  window.gtag?.("event", "chat_message_sent", {
    thread_id: threadId,
    message_length: text.length,
  });
});

On this page