Embeddable widget

September 3, 2026 · View on GitHub

The embeddable chat widget is a drop-in <script> snippet: no build step and no npm install required. The SDK ships the TypeScript types for its window.AGO configuration object (so projects configuring the widget in TypeScript get autocomplete) and a programmatic vanilla widget (mountChatWidget) for pure TS/JS apps.

If you want a chat bubble on a marketing site with zero code, use the embed snippet. If you control the DOM but don't use a framework, use mountChatWidget. For a React/Vue/Angular app, use the framework bindings instead.

There are two widgets in this package:

Embed snippetmountChatWidget
Setup<script> tag, no buildimport from @useago/sdk/widget
Rendersfloating bubble (iframe)inline panel, side panel, or the same floating bubble (placement: "bubble")
Frameworknonenone (pure TS/JS)
Forms / suggested repliesbuilt-inyes, via options
Use whenmarketing site, zero codeyou control the DOM, no framework

Programmatic widget: mountChatWidget

The pure TS/JS equivalent of the React <ChatWidget>: a complete chat panel (header, streaming messages, input) you mount into any DOM element, no React, Vue, or Angular. It supports the same conversational forms (form creator) and clickable suggested replies.

import { mountChatWidget } from "@useago/sdk/widget";

const widget = mountChatWidget("#chat", {
  // Bring your own client, or pass `config` and one is created for you:
  config: { baseUrl: "https://YOUR-DOMAIN.useago.com" },
  title: "Book a demo",
  welcomeMessage: "Hi! Tell me about your team and I'll set up a demo.",
  // Suggested replies are clickable by default (clicking sends the reply).
  forms: [
    {
      name: "demo_request",
      description: "A request to book a product demo.",
      schema: {
        type: "object",
        properties: {
          name: { type: "string" },
          email: { type: "string" },
          company: { type: "string" },
        },
        required: ["name", "email", "company"],
      },
      // Relay to a server-configured destination (URL + secret stay server-side):
      submit: { via: "backend" },
    },
    // Or keep the schema in the backend and reference it by name:
    // { name: "demo_request" },
  ],
});

// Programmatic control:
widget.sendMessage("Hello");
widget.stop(); // interrupt the answer being generated (no-op when idle)
widget.destroy(); // removes listeners, uninstalls forms, clears the DOM

Floating bubble (placement: "bubble")

The same widget as the embed snippet, without the iframe: a launcher in the bottom-right corner, a teaser bubble a second later, and a 550px panel with a home screen, a conversation screen, and a chat history screen. Paste this into any page and it answers from the public demo account:

import { mountChatWidget } from "@useago/sdk/widget";

mountChatWidget(document.body, {
  config: { baseUrl: "https://playground.api.useago.com", agent: "generic-guide" },
  placement: "bubble",
  title: "Ask AGO",
  prompt: "Hello, how can I help you today?",
  subtitle: "Answers come from the **AGO docs**.",
  conversationStarters: [
    { label: "What can the SDK do?" },
    { label: "Pricing", message: "Tell me about pricing" },
  ],
  colors: { button: "#007bff", header: "#03182f" },
});

Swap baseUrl for your own https://YOUR-DOMAIN.api.useago.com when you have a tenant.

Screens. The panel opens on Home: the title (one line per \n), the optional markdown subtitle, the conversationStarters cards, and the composer. Sending a message, or clicking a card, lands on the conversation screen, where the footer hides and the header shows a back chevron and a new-chat button. The Chats tab lists the visitor's threads newest first with a relative time (5min ago, 2h ago, 3d ago); a row reopens that thread, and the floating "New conversation" pill starts over. hideFooter: true drops the tab bar.

Reopening the last thread. On load the widget reopens the visitor's most recent conversation if its last message is under two hours old, first from the front-side cache (persistConversation is on by default here, under the usual ago_last_thread key), then from the thread list once it arrives, so it also works where the browser blocks storage. A visitor who already navigated is never redirected. Pass autoResume: false to always land on Home; an explicit conversationId opens that thread directly.

Mobile. Under 450px the panel fills the screen, the launcher hides while it is open, the page behind it stops scrolling, and Escape or the header "X" close it. On a desktop viewport the launcher stays visible and toggles the panel.

Colors. colors takes the embed snippet's AgoWidgetColors object and maps it onto the theme tokens; theme wins over it key by key.

colors keySets
buttonlauncherBg and sendBg
headerheaderBg (a gradient works), headerText derived for contrast
agentMessageagentBubbleBg (the assistant row gets a fill)
agentMessageFontagentBubbleText
backgroundpanelBg and messagesBg
fonttext
userMessageuserBubbleBg
userMessageFontuserBubbleText

The header text is white unless the header color is light (contrast under 3:1), in which case it turns black. If you set the header color from host CSS instead, set --ago-header-text-color alongside it.

Handle. On top of the usual methods (including newConversation(), which every placement has), the bubble handle exposes screen, showScreen("home" | "chat" | "history"), and openConversation(id). The chrome's strings are translated through labels:

mountChatWidget(document.body, {
  config: { baseUrl: "https://playground.api.useago.com", agent: "generic-guide" },
  placement: "bubble",
  labels: {
    home: "Accueil",
    chats: "Discussions",
    history: "Historique",
    newConversation: "Nouvelle conversation",
    noHistory: "Aucune discussion pour le moment",
    askQuestion: "Posez une question",
    timeAgo: { minutes: "il y a {n} min", hours: "il y a {n} h", days: "il y a {n} j" },
  },
});

The bubble ignores height, welcomeMessage, bubbleStyle, agentBubble, and showAgentName (its layout is the hosted widget's), defaults title to "AGO Chatbot", width to 550 (clamped between 400px and the viewport minus 40px), mobile.breakpoint to 450, and turns feedback, loadThreads, and persistConversation on. Every element carries an ago-prefixed class name (.ago-chat-widget-launcher, .ago-chat-widget-teaser, .ago-chat-widget-bubble, .ago-chat-widget__header, .ago-chat-widget__home, .ago-chat-widget__history, .ago-chat-widget__footer).

Options

OptionTypeDefault
client?AgoClient— (provide this or config)
config?AgoConfig— (needs at least baseUrl)
conversationId?string
persistConversation?boolean | Partial<ConversationSessionOptions>— (off; true for the bubble)
loadThreads?booleanfalse (true for the bubble)
title?string"Chat" ("AGO Chatbot" for the bubble)
welcomeMessage?string | { message: string; mode?: "static" | "streaming"; speed?; followUpReplies? }"Hello! How can I help you today?"
placeholder?string"Type a message..."
allowFiles?booleanfalse
allowStop?booleantrue (see Stop button)
height?string | number500 (ignored for side panels)
placement?"inline" | "left" | "right" | "bubble""inline" (see Floating bubble)
width?string | number400 (side panels), 550 (bubble)
launcher?booleantrue (side panels and bubble)
defaultOpen?booleanfalse (side panels and bubble)
prompt?string | false"Hello, how can I help you today?" (bubble teaser)
icon?string— (bubble launcher image)
colors?AgoWidgetColors— (bubble only)
hideFooter?booleanfalse (bubble only)
subtitle?string (markdown)— (bubble home screen)
conversationStarters?Array<{ label: string; message?: string }>— (bubble home screen)
autoResume?booleantrue (bubble only)
labels?Partial<WidgetLabels>English strings (bubble only)
mobile?{ breakpoint?: number; trigger?: "tap" | "focus" | "manual" }— (automatic; see Mobile fullscreen)
logoUrl?string
showAgentName?booleanfalse
agentBubble?booleanfalse
bubbleStyle?"default" | "imessage""default"
showHeader?booleantrue
theme?WidgetTheme— (see Theming)
forms?Array<CreateFormCollectorOptions | LoadFormCollectorOptions>
formSubmittedMessage?string | ((result) => string | null)server message, else "Form submitted."
feedback?boolean | WidgetFeedbackOptionsfalse (true for the bubble; see Feedback)
toolCallForm?WidgetToolCallFormOptions— (see Ticket form)
onFollowUpClick?((reply) => void) | falsesends the reply
onOpen?() => void— (side open / inline expand)
onClose?() => void— (side close / inline collapse)
onMessageSent?(content) => void
onMessageReceived?({ id, content }) => void
onFormSubmitted?({ name, values, result }) => void
onFormError?({ name, values, error }) => void

Feedback

feedback: true puts a thumbs up / thumbs down under every finished answer. On a thumbs-down, a small panel asks what went wrong (four reason chips and a free -text box).

mountChatWidget("#ago-chat", {
  config: { baseUrl: "https://playground.api.useago.com", agent: "generic-guide" },
  feedback: true,
});

The thumb is sent the moment it is clicked, so the signal survives a visitor who ignores the panel; sending the panel then files the detailed report. A bare rating is a reaction (the thumbs counted in the dashboard); a report with reasons or a comment also lands in the AGO feedback list, the analytics and the CSV export.

Pass an object to translate the strings or watch what goes out:

mountChatWidget("#ago-chat", {
  config: { baseUrl: "https://playground.api.useago.com", agent: "generic-guide" },
  feedback: {
    labels: {
      helpful: "Utile",
      notHelpful: "Pas utile",
      whatWentWrong: "Qu'est-ce qui n'a pas marché ?",
      send: "Envoyer",
      thanks: "Merci, c'est remonté.",
      commentPlaceholder: "Autre chose ? (facultatif)",
      reasons: {
        inaccurate: "Inexact",
        incomplete: "Incomplet",
        information_not_found: "Information introuvable",
        technical_issue: "Problème technique",
      },
    },
    onSubmit: ({ messageId, rating, reasons, comment }) => track(rating),
  },
});
OptionTypeDefault
askWhy?booleantrue (set false for thumbs only)
labels?Partial<FeedbackLabels>English strings
onSubmit?({ messageId, rating, reasons, comment }) => void— (fires per accepted report: once for the thumb, again for the detailed panel)
onError?(error: Error) => void— (the row keeps its state)

The row's elements carry ago-feedback* class names, so host CSS can restyle them; it ships no stylesheet of its own. The client-level API behind it is submitFeedback.

Ticket form (the form tool call)

When the visitor asks to talk to a human, the agent's built-in ago_ticketing tool opens a contact form inside the conversation, as a form tool call. The widget renders it the way the hosted widget does, with no option to set:

mountChatWidget("#ago-chat", {
  config: { baseUrl: "https://YOUR-DOMAIN.api.useago.com", agent: "support-bot" },
});
// Visitor: "I want to talk to someone" → the form appears under the answer.

The fields come from the ticket form configured in your AGO dashboard (fetched once from GET /config): subject, typology, priority, the custom fields, the detailed context, and attachments when the tenant allows them. The form asks for an email when the client has neither userEmail nor userJwt. Submitting creates the ticket (POST /tickets), completes the tool call (POST /tool-calls/{id}/submit), and shows a green confirmation with the ticket link. Fields pre-filled by the agent are kept, and what the visitor types survives every streamed chunk.

Three variants: the inline form above; an embedded form when the ticket form is in embed mode (the tenant's HubSpot HTML is hosted in place, pre-filled, and its submission detected); and a yellow notice when the tenant does not allow this visitor to create a ticket. While a form is pending the composer is replaced by a "complete the form above" card; once the ticket exists it becomes "a ticket has been created" with a New conversation button, which is also widget.newConversation().

mountChatWidget("#ago-chat", {
  config: { baseUrl: "https://YOUR-DOMAIN.api.useago.com", agent: "support-bot" },
  toolCallForm: {
    userEmail: currentUser.email, // skip the email field for a known visitor
    successMessage: "Merci, votre demande est enregistrée.",
    labels: { subject: "Sujet", submit: "Envoyer", detailedContext: "Description" },
    onSubmitted: ({ toolCallId, ticket, values }) => track("ticket", ticket?.id),
    onError: (error) => console.warn("ticket failed", error),
  },
});
OptionTypeDefault
labels?Partial<ToolCallFormLabels>English strings
successMessage?string"Your ticket has been successfully submitted."
successUrlLabel?string"You can find it here:"
userEmail?stringthe client's userEmail
onSubmitted?({ toolCallId, toolName, mode, ticket, values }) => void
onError?(error: Error) => void
onNewConversation?() => void

Only the newest form in a thread is live; an older one is replaced by a short status line, as in the hosted widget. This is separate from the forms option (form collectors the agent fills through client functions): the two can coexist, and the ticket form never triggers onFormSubmitted. The widget renders no other tool call type; a confirmation_input is yours to handle with confirmToolCall / rejectToolCall (see Tool calls). The client-level calls behind the form are getConfig, createTicket, and submitToolCallForm.

Stop button

While the agent answers, the send button becomes a Stop button that interrupts the turn. The stream closes and the backend is told to stop generating, so the partial answer stays on screen with status CANCELED instead of the agent finishing in the background. Set allowStop: false to keep the disabled spinner shown by earlier versions.

const widget = mountChatWidget("#ago-chat", {
  config: { baseUrl: "https://playground.api.useago.com", agent: "generic-guide" },
});

widget.stop(); // same path as the Stop button; no-op when nothing is generating

Swap baseUrl for your own https://YOUR-DOMAIN.api.useago.com once you have a tenant. See Stop the answer for the client-level API.

Streamed welcome message

By default welcomeMessage is a centered placeholder that disappears once the chat starts. Pass an object with mode: "streaming" to greet the visitor with a real assistant bubble, typed out token-by-token. It plays only on a fresh visit (skipped when a stored thread is resumed) and speed sets the per-token interval in ms.

Add followUpReplies to show clickable suggestion pills under the greeting once it finishes typing. A click sends that text as the first message (the same behavior as the follow-up replies a turn returns, so onFollowUpClick still intercepts it). followUpReplies applies to mode: "streaming" only; the static placeholder has no bubble to attach pills to.

mountChatWidget("#ago-chat", {
  config: { baseUrl: "https://YOUR-DOMAIN.api.useago.com" },
  welcomeMessage: {
    message: "Hi! Tell me about your team and I'll set up a demo.",
    mode: "streaming",
    speed: 45,
    followUpReplies: ["See pricing", "Book a demo", "Talk to a human"],
  },
});

mountChatWidget returns a handle: { client, element, sendMessage, stop, newConversation, session, threads, refreshThreads, destroy } (session is present only when persistConversation is set; open/close/toggle are present for side and bubble placements and, in a browser, for inline placement, see Side panel and Mobile fullscreen; the bubble adds screen, showScreen, and openConversation). newConversation() forgets the current thread and starts over. threads is the visitor's conversation list, the vanilla equivalent of the React/Vue useConversation().conversations. It auto-loads on mount and refreshes after each turn only when loadThreads: true; otherwise it stays empty until you call refreshThreads(). To debug, hand widget.client to the dev panel (initDevPanel({ client: widget.client })); it shows the installed forms' live state and every function the agent calls.

When a form is submitted (forms auto-submit once complete by default), the widget appends a green confirmation notice below the conversation. It shows a message string from the submit response when present, otherwise formSubmittedMessage.

To run your own logic on submit (and to catch failures, which show no notice), pass onFormSubmitted / onFormError. result is the raw submit response (the third-party API's answer):

mountChatWidget("#ago-chat", {
  config: { baseUrl: "https://YOUR-DOMAIN.api.useago.com" },
  forms: [{ name: "credit" }],
  onFormSubmitted: ({ name, values, result }) => {
    console.log(name, "submitted", result);
  },
  onFormError: ({ name, error }) => {
    console.warn(name, "submit failed:", error);
  },
});

These forward the client's form:submitted / form:error events, so widget.client.on("form:submitted", ...) works too (see Events).

Message content is rendered as GitHub-flavored markdown (headings, bold, italic, strikethrough, inline + fenced code, links, images, ordered/nested lists, blockquotes, tables, and rules) by a tiny built-in parser, no extra dependencies. All message text is HTML-escaped before it reaches the DOM and link/image URLs are scheme-validated, so untrusted agent output can't inject markup. The same renderer is exported as renderMarkdown(source)DocumentFragment if you build a custom vanilla UI.

Side panel (left / right)

By default the panel renders inline, filling the target element. Set placement: "left" or "right" and it instead pins a fixed, full-height panel to that edge of the viewport that slides open and closed; pass document.body as the target for a true page overlay. A circular launcher button opens it and a "×" in the header closes it; the height option is ignored (the panel is always full-height) and the width comes from width (default 400, capped at the viewport).

const widget = mountChatWidget(document.body, {
  config: { baseUrl: "https://YOUR-DOMAIN.useago.com" },
  placement: "left", // or "right"
  width: 420, // panel width (number → px)
  // defaultOpen: true,   // start open instead of behind the launcher
  // launcher: false,     // hide the built-in button and drive it yourself
});

// Drive open/close yourself (present only for side placements):
widget.open?.();
widget.toggle?.();
widget.close?.();

The panel is themed exactly like the inline one (see Theming); it just drops its rounded corners and keeps a single divider on the inner edge. The wrapper, launcher, and close button all carry ago-prefixed class names (.ago-chat-widget-panel, .ago-chat-widget-launcher, .ago-chat-widget__close) so nothing leaks into the host page.

Mobile fullscreen

The widget fills the screen on small viewports automatically. There is no flag to set: a compact inline card behaves like a launcher on a phone, and the side panel squares off to a full-screen sheet. The mobile object only exists to tune this or hand control back to you.

mountChatWidget("#ago-chat", {
  config: { baseUrl: "https://YOUR-DOMAIN.api.useago.com" },
  logoUrl: "https://YOUR-DOMAIN/logo.svg", // shown in the fullscreen bar
});

The fullscreen bar is the sheet's header. Its leading content reuses the props the in-card header already uses: it shows logoUrl as a logo, or falls back to the title text when there is no logo. Pass title: "" for a bar with just the close button. When showHeader is true, the in-card header is hidden while full-screen so the logo/title is not duplicated.

With placement: "inline", the compact card morphs to a fixed full-screen sheet (with a logo + close bar) when the card is tapped, and back when the user closes it. The morph uses the View Transitions API where available and falls back to an instant swap, tracks the visible viewport so the input stays above the keyboard, and exposes the sheet as a role="dialog" (Escape closes it). The morph is skipped automatically when the card is already full-bleed (about full viewport height), so a dedicated full-page chat is left alone. open/close/toggle are available on the handle (no-ops on a desktop viewport):

widget.open?.(); // expand to fullscreen (mobile only)
widget.close?.(); // collapse back to the inline card

With placement: "left" | "right", the side panel squares off to a true full-screen sheet on mobile; the slide-in/out behavior is unchanged. There it is a real dialog: it takes role="dialog" and aria-modal, traps Tab, closes on Escape, locks the page behind it, and tracks the visible viewport so the on-screen keyboard never covers the composer. Opening it moves focus into the panel rather than into the text field, so the keyboard does not eat half the screen before anything has been read.

On a desktop viewport that same panel stays deliberately non-modal: the page beside it keeps its scroll and its Tab order, because it is still usable.

The page's own scroll lock is restored, not cleared: if your app already pinned <body> (for its own modal, say), the widget puts your inline styles back exactly as it found them when it releases.

On a mobile viewport message bubbles also run wider (user bubbles to 88%, agent bubbles to 92%) to reclaim the screen edge on narrow devices. This is automatic and reflows when the viewport crosses the breakpoint.

Tuning (all optional): breakpoint is the max viewport width (px) treated as mobile (default 768). A short landscape viewport counts as mobile too (max-height: 500px in landscape), so a phone turned sideways keeps the full-screen sheet and the keyboard handling instead of falling back to the desktop layout. trigger sets how the inline card enters full screen: "tap" (default, expand when anywhere on the card is tapped, while follow-up replies still send and links still follow), "focus" (expand only when the input is tapped), or "manual" (expand only via widget.open()). onOpen / onClose fire on expand/collapse (and on side-panel open/close). The fullscreen bar and spacer carry ago-prefixed class names (.ago-chat-widget-mobile-bar, .ago-chat-widget-spacer).

Resume the last thread across reloads

Set persistConversation and the widget resumes the visitor's last active thread automatically, no manual localStorage wiring. It's built on createConversationSession: the visitor is identified by a single stable widget id (ago_widget_id), and the last active thread (its id + last message time) is cached on the front. On the next mount the widget decides without a backend call whether that thread is still fresh, and if so fetches its history, so the panel shows the previous messages instead of an empty greeting. Only threads idle for less than ttlMs (default 2h, sliding) are resumed; a thread with no recorded last-message time is not.

const widget = mountChatWidget("#chat", {
  config: { baseUrl: "https://YOUR-DOMAIN.useago.com", agent: "support-bot" },
  persistConversation: true, // localStorage, widget id under `ago_widget_id`, 2h ttl
});

// Customize the storage, ttl, or supply an explicit visitor id:
mountChatWidget("#chat", {
  config: {
    /* … */
  },
  persistConversation: {
    storage: sessionStorage,
    ttlMs: 60 * 60 * 1000,
    widgetId: "visitor-42",
  },
});

widget.session?.clear(); // forget the thread, e.g. a "new chat" button
widget.session?.widgetId; // the stable visitor id (for debugging / correlation)

A passed conversationId still takes precedence as the initial thread (and its history loads the same way); persistConversation only kicks in when you don't supply one. Without it, the widget starts on an empty greeting each mount.

Theming

mountChatWidget renders inline into the host page every color, font, and corner radius from a small set of CSS custom properties with built-in fallbacks. A widget with no theming looks exactly as shipped; set a token and that part of the panel re-skins to match the surrounding page.

There are two ways to set the tokens; they do the same thing, so pick whichever fits where your brand values live:

1. Plain CSS (recommended for anything context-dependent). Target the widget root, .ago-chat-widget:

.ago-chat-widget {
  --ago-font: inherit; /* adopt the page font */
  --ago-brand-color: #2b7fff; /* user bubbles + send button */
  --ago-header-background: #1c2b4a; /* deep brand header */
  --ago-border-color: #e3e7ee;
}

/* CSS unlocks things a one-shot value can't: dark mode, responsive, hover. */
@media (prefers-color-scheme: dark) {
  .ago-chat-widget {
    --ago-panel-background: #0d1117;
    --ago-text-color: #e6edf3;
  }
}
@media (max-width: 700px) {
  .ago-chat-widget {
    --ago-radius: 0;
  } /* flatten for full-bleed mobile */
}

2. The theme option (a typed convenience; sets the same variables for you). Best when the colors come from JavaScript (a tenant config, a CMS value):

mountChatWidget("#chat", {
  config: { baseUrl: "https://YOUR-DOMAIN.useago.com" },
  theme: {
    font: "inherit",
    brand: "#2b7fff",
    headerBg: "#1c2b4a",
    border: "#e3e7ee",
  },
});

The theme keys are a strict subset of what CSS can do (it's set once at mount, so no media queries or hover); CSS variables override it if both are present.

Token reference

CSS variabletheme keyDefaultApplies to
--ago-fontfontIBM Plex Sans stackWhole panel (inherit adopts the page font)
--ago-radiusradius16pxPanel container corners
--ago-message-radiusmessageRadius16px (20px with bubbleStyle: "imessage")Message bubble and suggested-reply pill corners
--ago-brand-colorbrand#03182fUser bubbles + send button (and header, unless headerBg is set)
--ago-brand-text-colorbrandText#fffText on brand
--ago-header-backgroundheaderBgbrandHeader background
--ago-header-text-colorheaderText#e8f0feHeader title
--ago-panel-backgroundpanelBg#fffContainer, input row, pills, source cards
--ago-messages-backgroundmessagesBg#fbfbfbScrolling messages area
--ago-text-colortext#30373eAssistant messages, agent name, source labels
--ago-muted-text-colormutedText#6b6d6fEmpty-state welcome message
--ago-border-colorborder#dee3e8Panel, input, pills, cards (set transparent to hide)
--ago-accent-coloraccent#1b5fc4Source badges + suggested-reply hover outline
--ago-agent-bubble-backgroundagentBubbleBg#f1f3f5Assistant message bubble fill (when agentBubble is on)
--ago-agent-bubble-text-coloragentBubbleTexttextAssistant message text
--ago-user-bubble-backgrounduserBubbleBg#fff (bubble)User message card (placement: "bubble")
--ago-user-bubble-text-coloruserBubbleText#0a0a0a (bubble)User message text (placement: "bubble")
--ago-launcher-backgroundlauncherBg#007bff (bubble)Floating launcher circle (placement: "bubble")
--ago-launcher-text-colorlauncherText#fffLauncher glyph
--ago-send-button-backgroundsendBgbrandComposer send/stop button and ticket form submit (bubble)
--ago-panel-widthpanelWidth550pxBubble panel width (clamped to 400px and the viewport minus 40px)

With placement: "bubble", --ago-header-background accepts a gradient and --ago-header-text-color is derived from it for contrast unless you set it; --ago-panel-background and --ago-accent-color there default to the hosted widget's #f8faff and #003edf.

Error messages stay red by design, and a couple of incidental tints (file/source chip background, streaming dots) are fixed neutrals that read on any light surface. To embed the panel flush inside a frame you already drew, match --ago-border-color to your frame's color (it's shared with the header/input dividers) and set --ago-radius to match, or drop your wrapper's own border and let the widget's be the single frame.


Embed snippet

Configure window.AGO before loading the widget script:

<script>
  window.AGO = {
    basepath: "https://YOUR-DOMAIN.useago.com",
    widgetApiKey: "YOUR-WIDGET-API-KEY",
    defaultAgent: "support-bot",
    title: "Support",
    prompt: "Hi! How can I help?",
  };
</script>
<script
  async
  src="https://useago.github.io/widgetjs/frame.js"
  crossorigin="anonymous"
></script>

(Grab the recommended integrity hash from your AGO dashboard, and add the domain where you embed the widget to your allowed domains list.)

The same UI is available without the iframe through mountChatWidget(document.body, { placement: "bubble", colors }), see Floating bubble.


Typing the config

import type { AgoWidgetConfig, AgoWidgetColors } from "@useago/sdk/widget";

const config: AgoWidgetConfig = {
  basepath: "https://YOUR-DOMAIN.useago.com",
  widgetApiKey: "YOUR-WIDGET-API-KEY",
  defaultAgent: "support-bot",
  title: "Support",
  colors: { button: "#03182f", header: "#03182f" },
};

window.AGO = config;

The package augments the global Window type, so window.AGO is typed for you anywhere this import is in scope.


AgoWidgetConfig

FieldTypeDescription
basepathstringYour AGO instance URL (required).
widgetApiKeystringWidget API key from your AGO dashboard (required).
defaultAgent?stringAgent id/slug to start conversations with.
email?stringPre-fill / identify the end user.
title?stringHeader title.
icon?stringURL of the launcher/header icon.
prompt?stringGreeting / opening message.
notifications?booleanEnable proactive notification bubble.
notificationMessage?stringText for the notification bubble.
colors?AgoWidgetColorsTheme overrides (see below).
hideFooter?booleanHide the "powered by" footer.
jwt? / authToken?stringAuthenticated-session tokens.
permission?stringPermission name applied to requests.
metadata?Record<string, unknown>Arbitrary metadata sent with the session.

AgoWidgetColors

button, header, agentMessage, agentMessageFont, background, font, userMessage, userMessageFont: all optional CSS color strings.


Relationship to the SDK

The widget and the SDK talk to the same AGO backend. Notably, the core SDK's createAgo() auto-detects an existing window.AGO object, so you can have the embedded widget and script your own SDK-driven interactions on the same page without re-declaring config.


See also: Core SDK · Configuration