Skip to content

@komozu/core (JavaScript)

Framework-agnostic browser client for Komozu, the chat for the signed-in users of your website or app: real-time messages (Server-Sent Events), optimistic sending, typing indicators, images and videos. No UI: you draw the chat yourself. For a ready-made UI, use @komozu/react or the embed script.

Terminal window
npm install @komozu/core
import { KomozuClient } from '@komozu/core';
const client = new KomozuClient({
apiUrl: 'https://api.komozu.com',
clientId: 'kz_live_…', // publishable key (API keys page of the dashboard)
memberToken: user?.komozuToken, // the signed-in member's token, stored by your server
});
await client.connect('u15'); // channel slug (or UUID)
client.subscribe((state) => render(state.messages));
await client.sendMessage('Hello!');

@komozu/core has no UI: you draw the chat yourself. client.lookupChannel('general') (or lookupChannel({ externalId: 'evt_1102' }) for a channel created by your backend) returns the channel, whether it accepts images and videos (channel.attachmentsEnabled) and the project’s default colour. To read an integration’s settings anyway (layout, colors, channel), call client.getIntegration('int_…').

Only members read and write: your signed-in users. Your server creates each of them once (@komozu/node, members.upsert) and gets a token that doesn’t expire: it stores it with the user (Identify your users). Hand that token to the client:

OptionBehaviour
memberTokenThe stored token of the signed-in user. Without it, connect() loads nothing and makes no request: state.restriction.code is MEMBER_TOKEN_REQUIRED (show a prompt to sign in). Sending, typing and uploads fail at once with the same code.
client.setMemberToken(token | null)A user signs in or out without reloading the page: the current channel is loaded again (null = no member, MEMBER_TOKEN_REQUIRED).
tokenProvider: () => Promise<string | null>Optional, wins over memberToken: called before the first connection, 60 s before exp if the token has one, and once more if the API refuses the token. client.refreshUser() calls it again.
  • The member’s profile comes from GET /v1/members/me (state.memberDisplayName, state.memberAvatarUrl), never from the token. Only sub is read from the token, to align your own bubbles.
  • A refused token is retried once with a fresh one, then surfaces as state.restriction (TOKEN_EXPIRED, TOKEN_REVOKED, INVALID_MEMBER_TOKEN, MEMBER_NOT_FOUND).
  • Nothing identifying is stored in the browser: the token stays in memory, only the light / dark choice is kept per project.

Channels are members (any member of the project) or private (only the members added to the channel). When something blocks the reader, state.restriction carries a machine-readable code:

CodeMeaning
MEMBER_TOKEN_REQUIREDNo member token: nothing is readable
CHANNEL_ACCESS_DENIEDPrivate channel, the member isn’t in it
MEMBER_BANNEDThe member is banned from the project: nothing is readable
DOMAIN_NOT_ALLOWEDThe page isn’t on the project’s allowed domains
INTEGRATION_NOT_FOUNDUnknown or disabled integration (getIntegration)
TOKEN_EXPIRED / TOKEN_REVOKED / INVALID_MEMBER_TOKENThe member token was refused: get a new one from your backend
MEMBER_NOT_FOUNDThe token is valid but the member was never created (members.upsert)

Sending a file to a channel where images and videos are turned off fails with 403 ATTACHMENTS_DISABLED. On the Free plan, a video fails with 403 VIDEOS_NOT_ALLOWED (images only).

Real-time events: message.deleted removes the message from state.messages; message.updated replaces it (text edited by its author, editedAt set); member.updated gives the new name and avatar to every message of that member (and to state.memberDisplayName / memberAvatarUrl when it is the reader); access.revoked (the server closes the stream after a removal, a ban or an access change) makes the client reconnect once and re-check its access. Listen with client.on('messageDeleted', …) / client.on('messageUpdated', …) / client.on('memberUpdated', …) / client.on('accessRevoked', …) if you need to react.

Each open chat holds one stream. A network (IP address) can keep 50 streams open at the same time, all projects together. A project can have 30 open at once on the Free plan, 100 on Starter and 1,000 on Pro, plus a 20 % margin (36, 120 and 1,200); the owner gets an email at 80 %. Beyond that, the stream is refused with 429 RATE_LIMIT_EXCEEDED and Retry-After: 30: state.connectionStatus stays reconnecting and the client tries again after that delay.

client.editMessage(messageId, content) changes the text of the reader’s own message (attachments don’t change; the text can be emptied only if the message keeps an image or a video). client.canEditMessage(message) tells whether to show the action: a member edits their messages, with no time limit. Anyone else’s message fails with 403 MESSAGE_NOT_EDITABLE, admins included (they delete instead). The edited message carries editedAt and reaches every reader live (message.updated).

  • state.pinnedMessages: the channel’s pinned messages, latest pinned first, loaded with the channel and kept up to date live (message.pinned / message.unpinned). Each message carries pinnedAt. To show a pinned message that is far back, client.loadUntilMessage(id) loads older pages until it is in state.messages (20 pages at most).
  • state.channelRole: the reader’s role in the channel, admin, user or null (not loaded). It changes live when the project owner or your backend changes the role (member.role_changed, also client.on('roleChanged', …)).
  • state.poweredByRequired: true when the project’s plan requires “Powered by komozu” (Free), false on Starter and Pro, null until the channel is loaded.
  • Admin actions: pinMessage, unpinMessage, deleteMessage and removeChannelMember (private channel; an admin can’t remove another admin). Without the admin role they fail at once with 403 CHANNEL_ADMIN_REQUIRED, without a network call; the API checks every action anyway.
  • Reporting: client.reportMessage(messageId, reason?) (any member; reason is spam, harassment, inappropriate or other). Messages from admins and from the project owner can be reported; reporting your own message fails with 400 MESSAGE_NOT_REPORTABLE; reporting the same message twice within 24 hours counts once. 10 reports per 10 minutes per IP (429).
  • Every message carries authorType (member, or owner for the project owner posting from the dashboard) and authorRole (admin shows the Admin badge, the project owner included).
new KomozuClient({ apiUrl, clientId, memberToken?, tokenProvider?, storage?, fetch? });
client.connect(channel): Promise<void>; // slug or UUID
client.disconnect(): void;
client.destroy(): void;
client.sendMessage(content, attachments?): Promise<MessageDTO>; // optimistic (state.pendingMessages)
client.retryMessage(localId) / client.discardMessage(localId);
client.loadMoreMessages(limit?): Promise<boolean>;
client.notifyTyping(): void; // at most one signal every 2.5 s
client.uploadAttachment(file, { onProgress }): Promise<AttachmentDTO>; // images and videos, 10 MB max
client.lookupChannel(slugOrUuid | { externalId }): Promise<{ channel; project: { primaryColor; showPoweredBy }; viewer: { role } }>;
client.getIntegration(publicId): Promise<PublicIntegrationDTO>;
client.listChannels(): Promise<ChannelSummaryDTO[]>; // channels the reader can open
client.setMemberToken(token | null) / client.refreshUser();
client.setColorModePreference('light' | 'dark' | null);
client.subscribe((state) => …): () => void;
client.on('message' | 'messageDeleted' | 'messageUpdated' | 'messagePinned' | 'messageUnpinned' | 'roleChanged' | 'memberUpdated' | 'accessRevoked' | 'statusChange' | 'messagesChange' | 'stateChange' | 'error', …): () => void;
client.isOwnMessage(message): boolean;
client.canEditMessage(message): boolean; // own message
client.editMessage(messageId, content): Promise<MessageDTO>;
client.loadPinnedMessages(): Promise<MessageDTO[]>; // state.pinnedMessages, latest pinned first
client.loadUntilMessage(messageId, maxPages = 20): Promise<boolean>;
client.isChannelAdmin(): boolean; // state.channelRole === 'admin'
client.pinMessage(messageId) / client.unpinMessage(messageId): Promise<MessageDTO>; // admins
client.deleteMessage(messageId): Promise<void>; // admins
client.removeChannelMember(externalId): Promise<void>; // admins, private channel
client.reportMessage(messageId, reason?): Promise<void>; // any member

Errors returned by the API are JSON { code, error, message } with a stable code (ErrorCode of @komozu/dto). A 429 (RATE_LIMIT_EXCEEDED) carries retryAfter (seconds).