Retily

Widget JavaScript API

Everything an embedding page may rely on. The Retily chat widget and contact form fire events on window that you can forward to your own analytics, and the chat exposes window.Retily so your own buttons and links can open it.

You do not need any of this to track conversions. Redirects, Google Tag Manager, GA4, Google Ads, Meta, TikTok, LinkedIn and Snap can all be configured without code, under Tracking in your widget settings. This page is for the cases that go further.

Send this to your developer

https://retily.com/docs/widget-js-api

Stability

Everything here is a stable contract within /v1/. Once your analytics is wired to retily:form_submitted, a rename on our side would break your site silently — your conversions would simply stop, with nothing going red anywhere. So we do not rename.

Stable
Event names, detail field names and their types, window.Retily method names and signatures, and when an event fires (we will not narrow the conditions).
Additive
New events, new fields on an existing detail, and new methods may appear at any time. Read the fields you need by name and ignore the rest; do not assume a detail has exactly the keys listed here, and do not switch exhaustively over event names.
Breaking
Renaming or removing an event, method or field, or changing a field's type, requires a new /v2/ script path. /v1/ keeps being served and keeps working. window.Retily.version tells you which you loaded.
Not covered
CSS class names, the shadow DOM's structure, the network endpoints the widgets call, and anything the widgets write to the console. These are internals and change without notice.

Events

All events are CustomEvents dispatched on window under a retily: prefix. They are fire-and-forget: a listener that throws is reported to your own error handler and never interrupts the widget. Load order does not matter — a listener registered before our script parses still receives them.

retily:readywidget.js
The chat launcher is on the page and Retily.open() will work.
retily:chat_openedwidget.js
The chat panel opened.
retily:chat_closedwidget.js
The chat panel closed.
retily:chat_startedwidget.js
A visitor sent their first message and a conversation was created.
retily:chat_endedwidget.js
The conversation ended.
retily:form_submittedform.js
The server accepted a contact-form submission.

chat_opened is not chat_started

The first is panel visibility: it fires when someone clicks the launcher, and also when the widget opens itself on a configured trigger — which is not something the visitor did at all. The second is the conversation. If you are counting leads, count chat_started.

retily:form_submitted

window.addEventListener("retily:form_submitted", (e) => {
  // e.detail = { widgetId, organizationId, ticketNumber }
  gtag("event", "generate_lead", { widget_id: e.detail.widgetId });
});
widgetIdstring
The form's data-widget value.
organizationIdstring
The workspace the form belongs to.
ticketNumbernumber | null
The ticket the submission created — a stable key for deduplicating.

Fires only after the server accepted the submission, so it counts real leads: a validation failure, a rate limit, a failed captcha or a dropped connection all leave it silent. It also does not fire in the builder's live preview, where an editor “submits” a draft without creating anything.

It does not carry what the visitor typed. No name, no email, no message. A windowevent is readable by every other script on your page, tag managers included, and shipping a visitor's email address into that is not a decision an embedded form should make on your behalf. If you need the submitted values, use a webhook (Settings → Webhooks, event form.submitted) — that is a server-to-server delivery to a URL you control, and it does carry them.

ticketNumberis your workspace's ticket counter, so it is sequential: a script reading it on two different days could estimate how many tickets you took in between. Three things bound that — the number is already shown to the visitor in the success message, the event is only readable by scripts you chose to put on your own page, and it is a count of tickets and nothing else. We keep it because it is the field that makes a conversion deduplicable. If that trade is wrong for your site, do not forward it.

retily:chat_started / retily:chat_ended

window.addEventListener("retily:chat_started", (e) => {
  // e.detail = { orgSlug, widgetId, conversationId }
  gtag("event", "generate_lead", { source: "chat" });
});
orgSlugstring
The embed's data-org.
widgetIdstring | null
null on a normal chat embed — the widget is resolved on our side from the slug. Only set when the tag carries an explicit data-widget.
conversationIdstring
The conversation — a stable key for deduplicating.
reason"visitor" | "resolved"
End only. visitor when they ended the chat themselves, resolved when the assistant created a ticket and closed it out.

chat_started fires once per conversation, at the moment the visitor's first message creates one. It does not fire when a returning visitor's existing conversation is restored, and it does not fire when a dropped session cookie is re-issued and the conversation reattached — counting a cookie re-issue as a new lead would inflate your funnel invisibly, so those paths stay silent. Under-counting a genuine restart is the deliberate trade.

One gap in chat_ended worth knowing

It is fired by the visitor's browser. So if one of your agents resolves a conversation from the Retily dashboard after the visitor has already closed the tab, no event is emitted — there is no page left to emit it.

Read it as a reliable signal of how a visitor left, not as a complete audit of conversation outcomes. For that, use your Retily reporting, which sees every resolution regardless of where it happened.

retily:chat_opened / retily:chat_closed

window.addEventListener("retily:chat_opened", (e) => {
  // e.detail = { orgSlug, widgetId, source }
  if (e.detail.source !== "proactive") gtag("event", "chat_open");
});
orgSlugstring
The embed's data-org.
widgetIdstring | null
As above.
source"launcher" | "api" | "proactive"
Open only. launcher is a click on the bubble, api is a Retily.open() call from your page, and proactive is the widget opening itself on a configured trigger.

source matters. A proactive open — time on page, URL match, returning visitor — is the widget opening itself; the visitor did nothing. Counting those as engagement inflates your funnel, so they are labelled rather than hidden.

retily:ready

window.addEventListener("retily:ready", () => {
  document.querySelector("#live-chat").hidden = false;
});

Detail is { orgSlug, widgetId }. Fires when the launcher is in the DOM, before the conversation session is restored — the widget is openable from that moment. It does notfire when the widget will not appear at all: a missing script attribute, an unknown workspace, or a domain that is not in the widget's allow-list. That is what makes it usable as a guard — reveal your own chat entry point when this fires and it can never be a link that does nothing.

window.Retily

Present when widget.js is on the page. A page embedding only the contact form has no window.Retily— that is deliberate, so the object's presence is a reliable “the chat is here” check.

Retily.open()
Opens the chat panel. No-op if already open.
Retily.close()
Closes it. No-op if already closed.
Retily.toggle()
Opens or closes.
Retily.isOpen()
Returns a boolean.
Retily.version
1.
Retily.instances
Every chat widget on the page, in mount order. See below.

The object is created the moment widget.js parses — before it has fetched its configuration — so a handler bound at DOMContentLoaded always finds it. An open() that arrives before the widget has mounted is remembered and applied on mount, not dropped, so an early click still opens the chat.

Wiring your own “Live chat” link

<a href="/contact/" id="live-chat" hidden>Live chat</a>

<script>
  // Only offer the link once the widget is actually there; until then
  // the anchor's href is a real fallback page.
  addEventListener("retily:ready", () => {
    document.getElementById("live-chat").hidden = false;
  });

  document.getElementById("live-chat").addEventListener("click", (e) => {
    if (window.Retily?.open) {
      e.preventDefault();
      window.Retily.open();
    }
  });
</script>

Keep the href pointing somewhere real. If the widget is blocked — an ad blocker, a content security policy, a domain that is not allow-listed — the link stays hidden and the visitor follows it as an ordinary link instead of clicking something dead.

If window.Retily is already taken

If your page defines its own window.Retily, we leave it completely alone: no methods assigned, no properties added, nothing overwritten. You get one console.warn and that is all. The retily: events are dispatched on window and are unaffected, so an integration built on events keeps working; only the method bag is unavailable. Renaming your global restores it.

Two chat widgets on one page

Retily.open() and friends stay bound to the widget that mounted first, for the whole life of the page. A second embed never repoints them, because you bind your “Live chat” link once and the object you captured must not change underneath you.

Retily.instances.find((w) => w.orgSlug === "support").open();

Every widget is in Retily.instances, in mount order, each carrying the same four methods plus the orgSlug and widgetId identifying it. A second embed also logs one console.warn — usually it means the same snippet is installed twice, in your tag manager and in your page template, which is worth fixing rather than working around. Both widgets emit their own events with their own orgSlug, so events never need disambiguating.

Notes

Load order
Does not matter. The events are on window, so a listener registered before the scripts parse still receives them.
Both widgets
form.js and widget.js each resolve their own <script> tag at load. Embedding both is fine, and they cannot collide over window.Retily: only widget.js ever defines it.
Shadow DOM
The chat lives in a shadow root, so your CSS cannot reach inside it and your selectors will not find its elements. Drive it through the methods above.

Configuring conversion tracking without code is under Tracking in your widget settings. For anything not covered here, or a field you need that is missing, get in touch — see retily.com.