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 |
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.
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"> and the first <h1>. It never reads the rest of the page's text. 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 itself is not stored word for word. The conversation turn records only the page title, the address and how many characters were sent, and the transcript in the dashboard shows an On page chip on those turns.
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. Put in it only what describes the screen. 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
Why is AgentencyWidget undefined in my script?
The widget script is deferred, so an inline script placed after it runs first. Put your calls in a click handler, an agentency:ready listener, or AgentencyWidget.ready(callback). Listening with window.addEventListener("agentency:…") is safe anywhere.
Can I open the chat with a question already sent?
Yes. Call AgentencyWidget.sendMessage("your question") — it opens the chat and sends the text as if the visitor typed it. Without code, add data-agentency-open and data-agentency-message="your question" to any button.
How do I know when the visitor closes the chat?
Listen for the close event: window.addEventListener("agentency:close", handler) works from anywhere on the page, and AgentencyWidget.on("close", handler) works once the widget has loaded. It fires only on a real open-to-closed change.
Does sending a message from code cost credits?
Yes. sendMessage() and a data-agentency-message button go through the same path as a typed message, so each one spends message credits at the chatbot's model rate and counts toward the widget rate limits. prefill() only fills the composer and costs nothing until the visitor sends.
Do I need JavaScript to make my own button open the chat?
No. Add data-agentency-open to any element and it opens the chat; data-agentency-toggle and data-agentency-close work the same way. Elements your framework adds after page load are picked up too.
Can the chatbot know which page the visitor is on?
Yes, with page context. Call AgentencyWidget.setPageContext({ title, url, text }) from your code, or add data-page-context="auto" to the script tag so the widget reads the page title, address, meta description and first heading at send time. The chatbot uses it only when a question refers to the page, answers still come from your knowledge base, and the text itself is never stored. The owner can turn it off, keep it manual (the default) or set it to auto in the Website embed settings. Never put personal data or secrets in it.
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
Five gates in order — plan, 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.