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) => {}
});| Option | Default | Description |
|---|---|---|
widget | true | true shows the widget with defaults. false disables it. An object configures it. |
widget.launcher | true | false hides the default button. { selector } binds the widget to your own element. |
widget.whispers | true | Ephemeral 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. |
locale | workspace setting | Language, 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. |
onReady | Called when the SDK is ready. | |
onError | Called 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();| Event | Payload | When it fires |
|---|---|---|
ready | state | Loading settled. Fires immediately if already settled. |
statechange | state | Any state change. |
open | state | Widget became visible. |
close | state | Widget was hidden. |
identify | { viewer, identityTrust } | Identity changed, including logout. |
unread | { count } | Unread count changed. |
presence | { availability } | Team availability flipped between 'online' and 'neutral'. |
error | error | Any SDK error. |
destroy | state | The 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 dataclearConversationData 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.
| Code | Meaning |
|---|---|
INVALID_PROJECT_ID | init was called with an empty project ID. |
INVALID_OPTIONS | init or setLocale received malformed options, or setTheme received a value other than 'auto', 'light', or 'dark'. |
INVALID_NAVIGATION | open received an unknown or invalid target. |
INVALID_IDENTIFY | identify input is missing both token and user.id. |
IDENTIFY_FAILED | The server rejected the identify call, for example an invalid token. |
BOOTSTRAP_FAILED | The SDK could not load. Check the project ID and network. |
NOT_READY | A call ran while the SDK was in a failed state. |
WIDGET_DISABLED | open was called but the widget is disabled. |
CONVERSATIONS_DISABLED | Messages are disabled for this workspace. The widget opens home instead. |
LAUNCHER_NOT_FOUND | The launcher.selector element does not exist. |
SIDEBAR_LOAD_FAILED | The widget UI failed to load. |
EMBED_CONTAINER_NOT_FOUND | The embed container does not exist. |
DESTROYED | A call ran after destroy(). |
UNKNOWN | Anything else. |