Skip to content

Installation

Installing the widget takes two steps: load the script, then initialize it with your workspace slug. Chat, ideas, roadmap and changelog live inside your app; the widget loads in the background and runs in a Shadow DOM, so it never touches your styles.

With the default launcher, that's all it takes for the floating button to show up on your page.

1. Add the script

Paste this near the end of your page, right before the closing </body> tag:

<script>  window.Jotly = window.Jotly || function () { (Jotly.q = Jotly.q || []).push(arguments) };</script><script async src="https://jotly.si/widget.js"></script>

The first line creates the global window.Jotly function and queues any calls you make before widget.js loads; once it loads, it runs them in order.

2. Initialize the widget

After the script, initialize Jotly with your slug:

<script>  Jotly('init', 'YOUR_WORKSPACE');</script>

Replace YOUR_WORKSPACE with your workspace slug. You'll find it in your dashboard URL (/app/YOUR_WORKSPACE) and in Settings → Install widget, where the snippet already includes your slug. The same slug powers your public portal at /b/YOUR_WORKSPACE.

Minimal example

This is the shortest complete installation:

<script>  window.Jotly = window.Jotly || function () { (Jotly.q = Jotly.q || []).push(arguments) };  Jotly('init', 'YOUR_WORKSPACE');  // Optional: identify your user  Jotly('identify', { user: { id: 'user_123', email: 'ana@company.com', firstName: 'Ana' } });</script><script async src="https://jotly.si/widget.js"></script>

A single tag (no inline script)

If your Content Security Policy (CSP) blocks inline scripts, a single tag is enough. The widget initializes itself from data-project; if you also call Jotly('init', …), your call takes precedence over the attribute.

<script async src="https://jotly.si/widget.js" data-project="YOUR_WORKSPACE"></script>

Optional: identify your users

If your product already knows who the user is, identify them after init:

const { viewer, identityTrust, warnings } = await Jotly.identify({  user: {    id: 'user_123',            // required (or a signed token)    email: 'ana@company.com',    firstName: 'Ana',    lastName: 'López',    avatar: 'https://…/ana.png',    traits: { plan: 'pro', monthlySpend: 499 },  },  companies: [{ id: 'acme', name: 'Acme Inc' }],});

You need either user.id or a token signed on your server. Repeating the same input during a live session does nothing, and if you call it several times, the last call wins. To make sure no one can impersonate another user, sign the identity on your server as described in Identify users. The legacy format ({ id, email, name, signature }) still works.

Init options

You can also configure the widget options in Settings → Install widget; options you pass to init override them. init returns a promise with the state and only rejects on programming errors (INVALID_PROJECT_ID, INVALID_OPTIONS).

Jotly('init', 'YOUR_WORKSPACE', {  widget: {                         // false turns it off (createEmbed and identify still work)    launcher: true,                 // false, or { selector: '#help' } to use your own button    whispers: true,                 // short-lived notices next to the launcher    notifications: { updates: 'whisper' }, // 'whisper' | 'full' | 'none'    position: 'right',              // 'right' | 'left'    offset: { bottom: 20, side: 20 },      // px    theme: 'auto',                  // 'auto' | 'light' | 'dark'  },  locale: 'en-US',                  // defaults to the browser language  pageContext: 'origin',            // 'origin' | 'path' | 'none': how much of the URL your team sees in chat  onReady: function () {},  onError: function (error) {},     // { code, message, recoverable }});

With notifications.updates: 'full', a new changelog entry opens the widget on that article at most once per page load, and only when the tab is visible and focused and the widget is closed. With 'whisper', a notice shows up next to the launcher for 7 seconds.

Browser API

Every method can be called as Jotly('open', { to: 'roadmap' }) or, once the script has loaded, as Jotly.open({ to: 'roadmap' }). Calls made before widget.js loads are queued and run in order.

// Promise with the state once init finishes (never rejects on network errors: check state.status)const state = await Jotly.ready(); // Open at an exact spot; resolves once the panel is visibleJotly.open({ to: 'home' });Jotly.open({ to: 'roadmap' });Jotly.open({ to: 'notifications' });Jotly.open({ to: 'feedback' });Jotly.open({ to: 'feedback', compose: true });            // new requestJotly.open({ to: 'feedback', submissionId: 'pst_123' });  // a single requestJotly.open({ to: 'updates', updateId: 'chg_123' });       // a single changelog entryJotly.open({ to: 'messages', compose: { text: 'Hi, I need help with…' } }); Jotly.close();Jotly.toggle(); // Appearance and languageJotly.setTheme('dark');           // 'auto' | 'light' | 'dark'await Jotly.setLocale('en-US'); // Read-only stateJotly.getState(); // When the user signs out of your appawait Jotly.logout(); // Tear everything down; on kiosks, also clear anonymous conversations, drafts and the remembered contactJotly.destroy({ clearConversationData: true });

Events

Jotly.on(event, handler) returns a function that stops listening. Listeners survive destroy().

EventReceives
readyThe state. If it's already ready, it fires right away.
statechangeThe state, on every change.
openThe state when the panel opens.
closeThe state when the panel closes.
identify{ viewer, identityTrust }
unread{ count }
presence{ availability: 'online' | 'neutral' }
error{ code, message, recoverable }
destroyThe state after destroy().
const off = Jotly.on('unread', function ({ count }) {  document.querySelector('#help-badge').textContent = count || '';});off(); // stop listening Jotly.on('error', function (error) {  if (!error.recoverable) console.error(error.code, error.message);});

Listening to unread or presence keeps the count up to date even with widget: false.

State

{  status: 'ready',                   // 'idle' | 'initializing' | 'ready' | 'error' | 'destroyed'  projectId: 'YOUR_WORKSPACE',  locale: 'en-US',  unreadCount: 1,                    // chat replies + unseen changelog entries; null at first  presence: 'online',                // 'online' | 'neutral' ('neutral' doesn't mean offline)  lastError: null,  widget: { enabled: true, open: false, position: 'right', theme: 'auto' },  session: { status: 'anonymous', identityTrust: null, viewer: null },}

Errors

Every error has the shape { code, message, recoverable }. Errors reach the error event, onError and state.lastError; promises reject with the same object.

CodeWhen
INVALID_PROJECT_IDThe project id is malformed or the workspace doesn't exist.
INVALID_OPTIONSAn option passed to init, setTheme, setLocale or createEmbed is invalid.
INVALID_NAVIGATIONThe open target or the navigate path is invalid.
INVALID_IDENTIFYidentify was called without a token or user.id.
IDENTIFY_FAILEDThe server rejected the identity (invalid signature, a workspace that requires signing, etc.). It's the only network error that rejects the promise.
BOOTSTRAP_FAILEDThe configuration couldn't be loaded; the state stays in error.
NOT_READYA method was called after init failed.
WIDGET_DISABLEDopen was called with widget: false.
CONVERSATIONS_DISABLEDopen({ to: 'messages' }) with messages turned off; Home opens instead.
LAUNCHER_NOT_FOUNDNo element matches launcher.selector (it keeps watching in case one shows up).
EMBED_CONTAINER_NOT_FOUNDThe embed container doesn't exist.
DESTROYEDA method was called after destroy; call init again.

Embeds in your page

Jotly.createEmbed renders the portal (feedback, roadmap and changelog) inside your app in an iframe, without its navigation bar and with the same identity as the widget. You can have several at once.

<div data-jotly-embed style="height: 720px"></div><script>  Jotly('init', 'YOUR_WORKSPACE', { widget: false });  var portal = Jotly.createEmbed({    // container: '#feedback',      // defaults to [data-jotly-embed]    path: '/roadmap',               // defaults to '/'    basePath: '/feedback',          // syncs the URL: /feedback/p/<slug>    theme: 'auto',    onReady: function () {},    onError: function (error) {},  });  portal.navigate('/updates');  // portal.destroy();</script>
  • With basePath, every page change inside the embed updates your site's URL through history, and the browser's back and forward buttons move the embed. Loading a URL under basePath opens that same page in the embed.
  • The iframe and your page only talk through jotly: messages (ready, route:change, navigate, identify, logout), and each side verifies the other's window and origin.
  • You can also embed the portal by hand: any page under /b/YOUR_WORKSPACE accepts ?navbar=hidden and ?theme=light|dark|auto.

Next.js

With next/script you load it once for the whole site and call window.Jotly from any component.

app/layout.tsx
// app/layout.tsximport Script from "next/script"; export default function RootLayout({ children }) {  return (    <html lang="en">      <body>        {children}        <Script id="jotly-init" strategy="afterInteractive">          {`window.Jotly = window.Jotly || function () { (Jotly.q = Jotly.q || []).push(arguments) };Jotly('init', 'YOUR_WORKSPACE');`}        </Script>        <Script src="https://jotly.si/widget.js" strategy="afterInteractive" />      </body>    </html>  );}

Verify the installation

After reloading the page:

  1. The widget launcher appears in the bottom corner.
  2. Clicking it opens the widget.
  3. You can move between messages, ideas, roadmap and changelog inside the widget.
  4. In the console, Jotly.getState().status returns 'ready'.

If you use your own button (launcher: { selector } or launcher: false), the widget can be installed correctly even though no floating button shows up.

Troubleshooting

The widget doesn't show up

Check these first:

  • The widget.js script is on the page
  • Jotly('init', …) runs with the right slug (or the tag has data-project)
  • launcher isn't set to false and widget isn't set to false
  • Jotly.getState().lastError is empty

The widget shows up, but doesn't recognize the user

This almost always means the installation is fine but the identity is missing. Start with Identify users.

If you send a signature that doesn't match, Jotly rejects the identification with IDENTIFY_FAILED instead of creating an unverified user.

See also Feedback widget in your app