Control the widget from your code
Open, close, send a message and react to events from your own page code — plus data attributes that need no JavaScript at all.
Browse topics
What it is
Once the widget script is on your page it publishes one global object, window.AgentencyWidget. Your own code can use it to open and close the chat, send a message on the visitor's behalf, start a new conversation, and be told when things happen inside the chat — it opened, it closed, a message was sent, a reply arrived.
The events also arrive as ordinary browser events on window, named agentency:<event>, so a page can listen for them before the widget script has even loaded. That detail matters more than it sounds; see the load-order section below.
None of this needs a build step or a package. It is plain JavaScript on the same page as the <script> tag, and it works on every plan that includes the widget.
When you would use it
- You hid the default bubble and drew your own "Chat with us" button, and you want that button to do more than open the chat.
- A pricing page has an "Ask about this plan" button that should open the chat with the question already sent, or already typed in so the visitor can edit it.
- You want to react when the visitor closes the chat — dim a highlight on your button, log an analytics event, show something else.
- You run a single-page app and need to remove the widget on some routes and bring it back on others.
- You want your own analytics to know how many questions were asked, without reading them from the dashboard.
If all you need is a button that opens the chat, you do not need any JavaScript at all. Skip to Buttons without code.
Where to find it
Open Dashboard → Chatbots → your chatbot → Integrations → Web widget and choose the JavaScript API & events option. The drawer has a quick-start snippet with your real values filled in, and below it a reference listing every method, every event and its data, and the HTML attributes — each with a copy button.
The Your own button option has a shorter version: expand Control the chat from your code under the snippet to see the essentials a custom button most often needs — send a message, know when the chat closes, start over, and the no-code "open and ask" attribute — with a link to the full reference.
Load order: the one trap
The embed snippet carries defer. That is deliberate — the widget must never slow your page down — but it means the script runs after your page has finished parsing. An inline <script> placed right after it runs first, when window.AgentencyWidget does not exist yet.
This does not work:
<script src="https://your-agentency-site.com/js/chatbot-widget.js" defer
data-widget-token="YOUR_WIDGET_TOKEN"
data-api-base-url="https://api.your-agentency-domain.com"></script>
<script>
// Runs before the widget has loaded: "AgentencyWidget is not defined"
AgentencyWidget.on("close", function () {
console.log("chat closed");
});
</script>
This works anywhere on the page, before or after the widget tag:
<script>
// Browser events on window are safe before the widget exists.
window.addEventListener("agentency:close", function (event) {
console.log("chat closed", event.detail.instanceId);
});
// Wait for the widget before calling its methods.
window.addEventListener("agentency:ready", function () {
AgentencyWidget.on("message:sent", function (detail) {
console.log("visitor asked:", detail.text);
});
});
</script>
The rule: window.addEventListener("agentency:…") is safe at any point. AgentencyWidget.* calls belong inside a click handler, inside an agentency:ready listener, or inside AgentencyWidget.ready(callback), which runs your callback once the widget has mounted — immediately, if it already has.
Methods
All of these live on window.AgentencyWidget. On a page with one widget — which is nearly every page — they act on that widget.
Open and close
open()— opens the chat window.close()— closes it.toggle()— opens if closed, closes if open.isOpen()—truewhile the chat window is open.
The conversation
sendMessage(text)— opens the chat if it is closed, then sendstextexactly as if the visitor had typed it and pressed Enter. Returns a promise that resolves totruewhen the message went into the conversation andfalsewhen it was refused: the text was empty or over 2 000 characters, a reply was still being written, or there is no widget on the page. Pass{ open: false }as a second argument to send without opening. Every message sent this way spends a message credit like any other — see Message credits.prefill(text)— opens the chat and putstextinto the composer without sending it, so the visitor can edit before they hit send.resetConversation()— starts a fresh conversation, the same as the visitor choosing New conversation from the chat's menu. Returns a promise that resolves once the new conversation is ready, so you canawaitit before sending.openHistory()/closeHistory()— show or hide the visitor's list of past conversations, when chat history is enabled for the chatbot.
Visitor and language
identify({ userId, userName, userData })— tells the chat who is talking;identify(null)clears it on sign-out. Details in Name visitors on the widget.setLanguage(code)— switches the chat's language to match your own site's language switcher;"default"goes back to the chatbot's language.
Page context
setPageContext({ title, url, text })— tells the chat which page, step or screen the visitor is on, as hidden text sent with their messages. A plain string is short for{ text };nullclears it. Details in Page context below.clearPageContext()— removes whatsetPageContext()set.getPageContext()— the page context the next message will carry, after clipping and cleaning, as{ title, url, text }: what your code set or, in auto mode, a fresh reading of the page.nullwhen there is none, or when page context is off.
Events
on(event, handler)— runhandlerevery timeeventhappens. Returns a function that removes the listener again.off(event, handler)— remove a listener.once(event, handler)— runhandlerthe first time only.
Lifecycle
ready(callback)— runcallbackonce the widget has mounted; runs immediately if it already has. Your callback receives the widget instance. If the widget never manages to mount (a wrong token, a blocked script), the callback never runs —readymeans mounted, not "the script loaded".destroyAll()— remove every widget from the page. Use it when a single-page app leaves a route that had the chat.getInstance(id)/getAllInstances()— for pages with more than one widget (rare). Each instance object has the sameopen,close,sendMessage,onand so on, acting on that instance alone.
identify(), setLanguage() and the page context methods apply to every widget on the page; the others act on the first one.
Events
Each event carries a small detail object. With AgentencyWidget.on() the handler receives detail directly; with window.addEventListener("agentency:…") it is event.detail. Every detail includes instanceId. It never includes your widget token or the visitor's session token, so it is safe to forward to analytics.
| Event | When | detail |
|---|---|---|
ready | The widget has mounted on the page. Fires before any timed auto-open, and not at all if the widget failed to mount | instanceId |
open | The chat window went from closed to open. autoOpen is true when the timed auto-open did it | instanceId, autoOpen |
close | The chat window went from open to closed | instanceId |
message:sent | A visitor message entered the conversation — typed, sent by sendMessage(), or from a data-agentency-message button | instanceId, text, sessionId |
message:received | The chatbot's reply finished. text is the reply in Markdown; queryId is a string | instanceId, text, queryId, sessionId |
conversation:new | A fresh conversation began — the visitor chose New conversation, the idle window passed, or your identify() call named a different person | instanceId |
lead:captured | The visitor submitted the lead capture form and it was accepted. Choosing Skip does not fire it. No field values are included | instanceId |
context:updated | The page context changed. source is "api" after setPageContext(), "cleared" after clearPageContext(), and "auto" when a message went out with a reading of the page that differs from the last one. chars is the length of the text. The text and URL are never included | instanceId, source, chars |
destroy | The widget was removed from the page. close is not fired for this | instanceId |
error | The widget showed the visitor a notice instead of a reply. type is rateLimited, quotaExhausted, accessRevoked, connectionError, genericError, processError or offline. The notice text and the visitor's message are not included | instanceId, type |
Two things that do not fire message:received: the welcome bubble, and the widget's own notices such as "please wait a moment" when a rate limit is hit. Only real replies count.
open and close fire only on a real change. Calling open() on a chat that is already open fires nothing.
Event names are not checked when you subscribe: AgentencyWidget.on("closed", …) is accepted without a warning and simply never fires. Copy the names from the table above.
A copy-paste example that dims your own button while the chat is open and logs every question the visitor asks:
<script>
window.addEventListener("agentency:open", function () {
document.body.classList.add("chat-open");
});
window.addEventListener("agentency:close", function () {
document.body.classList.remove("chat-open");
});
window.addEventListener("agentency:message:sent", function (event) {
console.log("Visitor asked:", event.detail.text);
});
</script>
If one of your handlers throws an error, the widget catches it, prints a warning to the browser console, and carries on — your other handlers still run and the chat keeps working.
Buttons without code
Any element on your page can control the chat with a data-agentency-* attribute. No JavaScript, and it works for elements your framework adds later.
<button type="button" data-agentency-open>Chat with us</button>
<button type="button" data-agentency-open
data-agentency-message="What does the Pro plan include?">
Ask about the Pro plan
</button>
<button type="button" data-agentency-toggle>Chat</button>
<button type="button" data-agentency-close>Close chat</button>
data-agentency-open— opens the chat.data-agentency-toggle— opens or closes it.data-agentency-close— closes it.data-agentency-message="…"— on an open or toggle element: open the chat and send that text. It firesmessage:sentand spends credits like any typed question.
Leave the attribute value empty on a normal page. On a page with more than one widget, set it to the instance id you want, for example data-agentency-open="support-chat".
Pair these with Hide the launcher button in the configurator when you want your own button to be the only way in. See Change the widget look and position.
Colour mode
data-theme on the widget's script tag forces that embed to light, dark or auto (follow the visitor's system). It wins over the colour mode saved on the chatbot. Leave it off to use the saved mode. Any other value is ignored. The attribute belongs on the script tag, not on the page around the widget. How to choose the saved mode: Chatbot appearance.
Page context
Visitors often ask about the screen in front of them: "what does this button do?", "explain this page", "why is this step asking for my VAT number?". The chatbot cannot see your page, so on its own it has no idea what "this" means. Page context fixes that: your page hands the widget a short, hidden description of where the visitor is, and the widget sends it along with their messages. The visitor never sees it in the chat.
There are two ways to provide it.
From your code. Call setPageContext() with whatever describes the current screen best — a plan name, a checkout step, the settings panel that is open. Call it again whenever that changes, and clearPageContext() when there is nothing useful to say:
<script>
window.addEventListener("agentency:ready", function () {
AgentencyWidget.setPageContext({
title: "Pro plan checkout",
url: location.origin + location.pathname,
text: "Step 2 of 3: billing details. Plan: Pro, billed yearly.",
});
});
// Later, when the visitor leaves checkout:
// AgentencyWidget.clearPageContext();
window.addEventListener("agentency:context:updated", function (event) {
console.log("page context", event.detail.source, event.detail.chars);
});
</script>
A plain string works too: AgentencyWidget.setPageContext("Step 2 of 3: billing details") is the same as passing { text: "…" }.
Without code. Add data-page-context="auto" to the widget's script tag:
<script src="https://your-agentency-site.com/js/chatbot-widget.js" defer
data-widget-token="YOUR_WIDGET_TOKEN"
data-api-base-url="https://api.your-agentency-domain.com"
data-page-context="auto"></script>
Each time the visitor sends a message, the widget reads the page's title, its address without the query string or # part, the <meta name="description">, the first <h1> and the beginning of the page's main content — the text inside <main>, <article> or the element marked role="main" (the whole body when a page has none), up to the 2 500-character limit. It skips navigation, headers, footers, sidebars, hidden elements, scripts and the chat widget itself, and it never reads the address bar's query string or # part. Without the attribute — or with data-page-context="manual", the default — the widget sends only what setPageContext() gave it. When both are in play, what you set with setPageContext() wins over what the widget reads itself.
Limits. The widget tidies the page context before sending it, and the server checks it again:
textis clipped to 2 500 characters andtitleto 200.urlmust be a fullhttp://orhttps://address of at most 2 048 characters; anything else is dropped rather than shortened.- Runs of spaces and line breaks are collapsed into one space, and control characters are removed.
- If nothing is left after that, no page context is sent at all.
getPageContext() returns exactly what the next message will carry after this tidying, so you can check what the widget kept.
What the chatbot does with it. The page context is a hint, not a source of answers:
- It is used only when the visitor's question points at the page — "this button", "here", "this page". The chatbot decides that message by message. Greetings, questions about something else, and answers served from the cache never use it.
- Facts still come only from your knowledge base. The page context helps the chatbot understand what the visitor is referring to; it does not add new information the chatbot will repeat as fact.
- The text is treated as untrusted data, never as instructions. A line on your page such as "ignore your rules" is read as page content, not obeyed.
- A reply that used page context is never cached and reused for another visitor.
- The text is saved so you can see what the chatbot saw. Each conversation turn records the page title, the address and how many characters were sent, and the page text itself is kept once per page — every message sent from the same text shares one copy — and shown only inside your dashboard: the transcript marks those turns with an On page chip, and opening it shows the page text. Nobody outside your account can read it, and it never appears in the chat.
Turning it on or off. Open Integrations → Web widget, open the Website embed option, and choose the Page context setting:
- Off — the widget never sends page context, and the server refuses it even if a page tries.
- Manual (the default) — only what your code passes to
setPageContext(). - Auto — the same as adding
data-page-context="auto": the widget reads the page itself unless your code has set something.
Click Save as default to apply the choice to every page that already has the widget, within a few minutes and without touching your site. If you only copy the snippet instead, the choice travels as a data-page-context attribute on that one tag.
A data-page-context attribute on the script tag overrides Manual or Auto for that page. Nothing overrides Off.
Privacy. Page context goes to the AI model whenever a question uses it, and it counts toward that reply's usage like the rest of the conversation. The text is stored with your chatbot (once per page, deleted with the chatbot) and visible to your account only. Put in it only what describes the screen — with Auto, remember that the widget reads the page's main content, so a page that shows a customer's own details will send those too; use Manual or clearPageContext() on such pages. Never include personal data — names, email addresses, phone numbers, order or account details — or secrets such as passwords, API keys and session tokens. To tell the chat who the visitor is, use identify() instead; see Name visitors on the widget.
Single-page apps
The snippet mounts the widget once per page load. In a single-page app the page never reloads, so:
- Leaving a route that had the chat: call
AgentencyWidget.destroyAll(). Not doing so is the usual cause of two bubbles appearing after a few navigations. - Coming back: call
AgentencyWidget.init({ widgetToken: "…", apiBaseUrl: "…" })with the same two values that are on your script tag. - Your listeners:
destroyAll()removes listeners you attached to an instance object. Listeners registered withAgentencyWidget.on()and withwindow.addEventListener("agentency:…")survive and will fire for the next widget you mount. - Naming the visitor: call
identify()as soon as your own session resolves. It is safe to call before the chat has finished mounting. - Page context: call
setPageContext()again on every route change, orclearPageContext()on routes with nothing to describe — otherwise the previous screen's description goes along with the next message. Withdata-page-context="auto"there is nothing to do: the widget reads the page afresh on every send.
Common problems
"AgentencyWidget is not defined" in the console.
Your inline script ran before the deferred widget script. Move the call into a click handler, an agentency:ready listener, or AgentencyWidget.ready(callback). See the load-order section above.
sendMessage() resolved to false and nothing happened.
The text was empty, longer than 2 000 characters, or the chatbot was still writing a previous reply — the widget refuses a second message until the first is answered, the same as the visitor's own send button. It also resolves to false when the widget had to show a notice instead of sending: the visitor is sending too quickly, or the chat could not reach the server to start a session. There is also nothing to send to if the widget never mounted: check The widget is not showing.
My close listener never fires.
If you attached it with AgentencyWidget.on("close", …) in an inline script, that script probably threw before it got there (the problem above). Use window.addEventListener("agentency:close", …) instead — it works from anywhere. Also check the spelling: a misspelt event name ("closed", "onClose") is accepted silently and never fires.
message:received fires for some replies but not others.
It fires only for real replies from the chatbot. The welcome message and the widget's own notices — rate limit, out of credits, connection lost — are not replies and never fire it.
My button with data-agentency-message opens the chat but sends nothing.
The attribute only works on an element that also has data-agentency-open or data-agentency-toggle. It does nothing on data-agentency-close.
Two bubbles after navigating around my app.
Call destroyAll() when leaving the route, as described above. See also Embed the website widget.
Common questions
Was this article helpful?
Related articles
Embed the website widget
Copy one script tag from Integrations → Web widget, paste it on your published pages, then allow your domain and verify.
Change the widget look and position
The launcher lives on Integrations → Web widget; the chat window's avatar and colours live on chatbot Settings.
Name visitors on the widget
Pass your own user's id and name so conversations show who is chatting. It is a label, never a login.
The widget is not showing
Four gates in order — activation, the snippet, allowed domains, the browser — before you touch your theme.
Ready to try it on your own content?
Create a free workspace, add a document, and ask the questions your team is tired of answering.