SDK reference

The SDK is exposed as window.uj. All methods can be called at any time, even before the SDK has loaded. Calls are queued and run once ready.

uj.version

The SDK version as a string, for example 1.0.0-beta.1.

uj.init(projectId, options?)

Initializes the SDK. Returns a promise that resolves with the SDK state once loading settles.

const state = await uj.init('YOUR_PROJECT_ID', {
  widget: {
    launcher: true,          // boolean or { selector: string }
    whispers: true, // default whisper behavior
    notifications: { updates: 'whisper' }, // 'whisper', 'full', or 'none'
    position: 'right',       // 'left' or 'right'
    offset: { bottom: 20 },  // corner distance in pixels: { bottom, side }
    theme: 'auto'            // 'auto', 'light', or 'dark'
  },
  locale: 'en-US',       // optional language override
  pageContext: 'origin', // 'origin', 'path', or 'none'
  onReady: () => {},
  onError: (error) => {}
});
OptionDefaultDescription
widgettruetrue shows the widget with defaults. false disables it. An object configures it.
widget.launchertruefalse hides the default button. { selector } binds the widget to your own element.
widget.whisperstrueEphemeral notification toasts at the widget corner. Independent of the launcher: they keep appearing when the button is hidden unless set to false. An explicit update notification mode overrides this for changelog announcements only. See Controlling the widget.
widget.notifications.updates'whisper', or 'none' when whispers: false'whisper' announces updates in a toast; 'full' opens the latest unseen entry in the existing reader; 'none' suppresses update announcements while preserving unread. Explicit values override whispers for updates only.
widget.position'right'Which side of the screen the widget opens on.
widget.offset{ bottom: 20, side: 20 }Pixel distance of the launcher and whispers from the viewport edges. Useful when your own button occupies the corner.
widget.theme'auto'Follow the system, or force light or dark.
localeworkspace settingLanguage, for example 'fr-FR'.
pageContext'origin'How much of the current page URL is shared with your team in conversations. 'origin' sends the domain, 'path' includes the path, 'none' sends nothing.
onReadyCalled when the SDK is ready.
onErrorCalled on any SDK error.

The returned promise does not reject on network problems. Check state.status === 'ready' if you need to confirm loading succeeded. It rejects only for programmer errors: an empty project ID or malformed options.

An empty notifications object or updates: undefined uses the default. Malformed notification objects, unknown keys, and values outside 'whisper', 'full', and 'none' reject initialization with INVALID_OPTIONS and recoverable: false before network or DOM work.

Full mode works without a launcher, other whispers, or host subscriptions. It can show an old latest entry on the first visit and attempts one automatic opening per initialization. Manual open, toggle, or close spends that visit's allowance too. Failed or cancelled attempts are not retried during that visit. Identity changes and app navigation do not reset it; reload or reinitialization does. widget: false disables automatic presentation.

Opening requires a visible, focused page and a closed widget. An actual visible article view, through any entry point, records the article as seen. Prefetches, clicks alone, hidden content, and failed loads do not. Reading older content cannot consume a newer release. Seen history is browser-local and scoped by project and host origin, with no cross-device or cross-tab delivery guarantee. Checks use initialization, focus, and the existing roughly five-minute closed-widget poll. See Update announcements for examples and storage limits.

uj.ready()

Returns the same promise as init. Useful when the code that needs the SDK is far away from the code that initialized it.

const state = await uj.ready();

uj.identify(input)

Identifies the current user. Returns a promise with the result. See Identify users and Signed identity.

const result = await uj.identify({
  token: 'OPTIONAL_JWT_FROM_YOUR_SERVER',
  user: {
    id: 'user-123',        // required if no token is given
    email: 'ada@example.com',
    firstName: 'Ada',
    lastName: 'Lovelace',
    avatar: 'https://example.com/avatar.jpg',
    traits: { plan: 'pro' }
  },
  companies: [
    { id: 'acme', name: 'ACME Inc.', domain: 'acme.com', traits: {} }
  ]
});
 
result.viewer;        // the identified user, or null (shape below)
result.identityTrust; // 'signed', 'unsigned', or null
result.warnings;      // [{ code, message, field? }]

viewer has this shape:

{
  id: 'mem_...',        // UserJot's member ID, not the user.id you sent
  firstName: 'Ada',     // string or null
  lastName: 'Lovelace', // string or null
  avatar: { url: 'https://...', blurhash: '...' } // or null; blurhash optional
}

At least one of token or user.id is required. Calling identify again with the same input is a no-op while the resulting session is still live; after it expires, the same input is sent again. Calling it with new input switches the session. If several calls overlap, the last one wins.

This is the only method that rejects on runtime failures, with IDENTIFY_FAILED.

uj.logout()

Clears the current identity and returns the session to anonymous.

await uj.logout();

uj.open(target?)

Opens the widget. With no argument, opens where the user left off. Resolves when the widget is visible.

await uj.open();
await uj.open({ to: 'home' });
await uj.open({ to: 'feedback' });
await uj.open({ to: 'feedback', compose: true });
await uj.open({ to: 'feedback', submissionId: 'POST_ID' });
await uj.open({ to: 'roadmap' });
await uj.open({ to: 'updates' });
await uj.open({ to: 'updates', updateId: 'UPDATE_ID' });
await uj.open({ to: 'messages' });
await uj.open({ to: 'messages', compose: { text: 'Prefilled message' } });
await uj.open({ to: 'notifications' });

Rejects with INVALID_NAVIGATION for unknown targets or invalid combinations, like passing both submissionId and compose.

uj.close()

Hides the widget. Synchronous. Safe to call at any time.

uj.toggle()

Opens the widget if closed, closes it if open.

uj.createEmbed(options?)

Renders a UserJot page inside a container on your page. Returns a controller. See Embedding pages inline.

const embed = uj.createEmbed({
  container: '[data-userjot-embed]', // selector or element
  path: '/',                         // initial page
  basePath: '/feedback',             // sync with your browser URL
  theme: 'auto',
  onReady: () => {},
  onError: (error) => {}
});
 
embed.navigate('/roadmap');
embed.destroy();

Multiple embeds can exist at the same time. All of them follow the current identity.

uj.setLocale(locale)

Changes the widget language at runtime.

await uj.setLocale('de-DE');

uj.setTheme(theme)

Changes the widget theme at runtime. Accepts 'auto', 'light', or 'dark'. Synchronous, unlike setLocale.

uj.setTheme('dark');

Invalid values report INVALID_OPTIONS through the error event without changing the theme. Note that init always applies its own theme, which defaults to 'auto' when you do not pass one, so a setTheme call made before init does not survive it. Pass the theme in the init options instead.

uj.getState()

Returns a read-only snapshot of the SDK state.

{
  status: 'ready',        // 'idle' | 'initializing' | 'ready' | 'error' | 'destroyed'
  projectId: 'proj_...',
  locale: 'en-US',
  unreadCount: 3,         // number, or null before the first fetch;
                          // includes conversation replies, changelog posts,
                          // and feedback notifications
  presence: 'online',     // 'online' | 'neutral' — team availability (the green dot)
  lastError: null,        // the last error, or null
  widget: {
    enabled: true,
    open: false,
    position: 'right',      // 'left' | 'right'
    theme: 'auto'           // 'auto' | 'light' | 'dark'
  },
  session: {
    status: 'identified',   // 'anonymous' | 'identifying' | 'identified'
    identityTrust: 'signed',
    viewer: { id: 'mem_...', firstName: 'Ada', lastName: 'Lovelace', avatar: null }
  }
}

uj.on(event, listener)

Subscribes to an event. Returns an unsubscribe function.

const off = uj.on('unread', ({ count }) => updateBadge(count));
off();
EventPayloadWhen it fires
readystateLoading settled. Fires immediately if already settled.
statechangestateAny state change.
openstateWidget became visible.
closestateWidget was hidden.
identify{ viewer, identityTrust }Identity changed, including logout.
unread{ count }Unread count changed.
presence{ availability }Team availability flipped between 'online' and 'neutral'.
errorerrorAny SDK error.
destroystateThe SDK was destroyed.

Listeners survive destroy(), so you can subscribe once at page load even if you destroy and re-init the SDK.

presence is the same signal as the green dot inside the widget. 'neutral' means the SDK cannot currently prove anyone from the team is online — it never claims the team is offline, so treat it as "hide the dot", not "show offline". Subscribing to unread or presence keeps both values fresh in the background, even with the default launcher disabled.

uj.destroy(options?)

Removes the widget, all embeds, and the session from the page.

uj.destroy();
uj.destroy({ clearConversationData: true }); // also clear local conversation data

clearConversationData additionally clears the browser's local conversation state: anonymous conversation access grants, drafts, and remembered contact details. Use it on shared devices or kiosk-style setups where the next visitor should not see the previous visitor's guest conversations.

After destroy, you can call init again to start fresh.

Errors

Errors have the shape { code, message, recoverable, cause? }. They reach you through rejected promises for programmer errors, and through the error event and onError callback for everything else.

CodeMeaning
INVALID_PROJECT_IDinit was called with an empty project ID.
INVALID_OPTIONSinit or setLocale received malformed options, or setTheme received a value other than 'auto', 'light', or 'dark'.
INVALID_NAVIGATIONopen received an unknown or invalid target.
INVALID_IDENTIFYidentify input is missing both token and user.id.
IDENTIFY_FAILEDThe server rejected the identify call, for example an invalid token.
BOOTSTRAP_FAILEDThe SDK could not load. Check the project ID and network.
NOT_READYA call ran while the SDK was in a failed state.
WIDGET_DISABLEDopen was called but the widget is disabled.
CONVERSATIONS_DISABLEDMessages are disabled for this workspace. The widget opens home instead.
LAUNCHER_NOT_FOUNDThe launcher.selector element does not exist.
SIDEBAR_LOAD_FAILEDThe widget UI failed to load.
EMBED_CONTAINER_NOT_FOUNDThe embed container does not exist.
DESTROYEDA call ran after destroy().
UNKNOWNAnything else.