Skip to content

Server API reference

Your backend drives Komozu with the secret key: members, channels (one per event or group), members of private channels, channel admins, pinned messages, moderation, reported messages. The @komozu/node SDK wraps every route below; this page is the HTTP reference.

Authorization: Bearer kz_sec_…
  • Missing or unknown key → 401 INVALID_SECRET_KEY.
  • Any request carrying an Origin header (i.e. from a browser) → 403 SECRET_KEY_IN_BROWSER. The secret key never goes to a browser or a mobile app.
  • Rate limit per project: RATE_LIMIT_SERVER_PER_MINUTE (600 requests per minute by default). Over it → 429 RATE_LIMIT_EXCEEDED with RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset and Retry-After.
  • Allowed domains don’t apply to the server API.
  • Errors are JSON { code, error, message } with a stable code. Invalid input → 400 VALIDATION_ERROR.
  • Members are identified by their external id: the user id in your own database (1–128 characters).
  • Channels (:channel segment) by slug or UUID, or by their externalId when the request carries ?by=externalId (never guessed: an external id could look like another channel’s slug). With @komozu/node, pass { externalId: 'evt_1102' }.
  • The slug is generated from the name at creation (Tournament – Nov 2 → tournament-nov-2, then -2, -3… if taken) and never changes. externalId is optional, unique in the project and immutable.
Method & pathBody / queryResponseErrors
PUT /v1/server/members/:externalId{ name (1–64), avatarUrl?: https:// URL | null }201 created / 200 updated, { member, token }400
PATCH /v1/server/members/:externalId{ name?, avatarUrl? }200 { member }400, 404 MEMBER_NOT_FOUND
GET /v1/server/members/:externalId—200 { member }404
GET /v1/server/members?limit=1..100 (50)&cursor=&search=200 { members, nextCursor }400
DELETE /v1/server/members/:externalId?deleteMessages=true|false (false)200 { success }404
POST /v1/server/members/:externalId/ban{ reason?: string ≤ 500 }201 { ban } (200 if already banned)404
DELETE /v1/server/members/:externalId/ban—200 { success } (idempotent)404 member
POST /v1/server/members/:externalId/revoke-tokens—200 { token } (new token)404 MEMBER_NOT_FOUND

member = { externalId, displayName, avatarUrl, createdAt, updatedAt, lastSeenAt, banned }. The member list is sorted by external id; nextCursor is the last external id of the page. search matches the name or the id.

When PUT or PATCH changes the name or the avatar of an existing member, open chats of the project rename that member’s messages right away (member.updated), without reloading.

token is the member’s token for the chat: it doesn’t expire, store it with the user and write it in your pages (Identify your users). PUT returns a new valid token each time (earlier ones keep working until you call revoke-tokens). revoke-tokens refuses every token issued before it (401 TOKEN_REVOKED in the chat), disconnects the member’s open chats and returns a new token.

Method & pathBody / queryResponseErrors
POST /v1/server/channels{ name (1–255), access?: 'members' | 'private' = 'members', externalId?, attachmentsEnabled?: boolean = true }201 { channel } (slug generated)400, 403 CHANNEL_LIMIT_EXCEEDED, 409 CHANNEL_EXTERNAL_ID_TAKEN
GET /v1/server/channels—200 { channels }
GET /v1/server/channels/:channel?by=externalId200 { channel }404 CHANNEL_NOT_FOUND
PATCH /v1/server/channels/:channel{ name?, access?, attachmentsEnabled? } (slug immutable)200 { channel }400, 404
DELETE /v1/server/channels/:channel—200 { success }404
GET /v1/server/channels/:channel/members—200 { members } (each with its role)404
PUT /v1/server/channels/:channel/members{ memberIds: string[1..100] }200 { success } (idempotent)404 MEMBER_NOT_FOUND (all or nothing, lists the unknown ids), 404 channel
DELETE /v1/server/channels/:channel/members{ memberIds: string[1..100] }200 { success } (idempotent)404 channel
PUT /v1/server/channels/:channel/members/:externalId/role{ role: 'user' | 'admin' }200 { member }400, 404 MEMBER_NOT_FOUND, 404 channel
GET /v1/server/channels/:channel/messages?limit=1..100 (50)&cursor=200 { messages, nextCursor }404 channel
GET /v1/server/channels/:channel/pins—200 { messages } (latest pinned first, 50 at most)404 channel

channel = { id, slug, name, access, externalId, attachmentsEnabled, createdAt, updatedAt }. Access levels: members (any member of the project) or private (only the members added to the channel). Reading and writing always need a member token: there is no anonymous access. attachmentsEnabled: false turns images and videos off in the channel (the chat hides the button; uploads are refused with 403 ATTACHMENTS_DISABLED).

message = { id, channelId, content, authorType, member, owner, authorRole, attachments, pinnedAt, editedAt, createdAt }. authorType is member or owner (the project owner, posting from the dashboard; owner is { displayName }). member is null for a deleted member. authorRole is the author’s role in the channel: admin (the owner included), user, or null for a deleted member. pinnedAt is null unless the message is pinned; editedAt is null unless its author edited it (message.updated in the chat).

A channel member is user (default) or admin. In the chat, an admin deletes any message, pins and unpins and removes a member from a private channel, on that channel only. Banning from the whole project stays with you (server API, dashboard).

  • On a private channel, admin adds the member to the channel if needed; user keeps their access.
  • On a members channel, the role only adds rights, never access: GET …/members lists the admins only, and user removes the admin row.
  • Open chats pick the change up right away (member.role_changed), without reconnecting.
Method & pathBodyResponseErrors
DELETE /v1/server/messages/:messageId—200 { success }404 MESSAGE_NOT_FOUND (scoped to the project)
PUT /v1/server/messages/:messageId/pin—200 { message } (idempotent)404 MESSAGE_NOT_FOUND, 409 PIN_LIMIT_EXCEEDED (50 per channel)
DELETE /v1/server/messages/:messageId/pin—200 { message } (idempotent)404 MESSAGE_NOT_FOUND

Members can report a message from the chat (Report button, optional reason: spam, harassment, inappropriate, other). Reports wait here, and in the dashboard’s Reports page, until you act.

Method & pathBodyResponseErrors
GET /v1/server/reports—200 { reports } (open reports grouped by message, most recently reported first, 100 messages at most)
POST /v1/server/reports/:messageId/dismiss—200 { dismissed } (number of reports closed; the message stays)404 REPORT_NOT_FOUND (no open report for this message in the project)

report = { message, channel: { id, slug, name }, reportCount, reasons, firstReportedAt, lastReportedAt }. reasons counts the reports per reason ({ spam: 2 }; reports without a reason only count in reportCount). Nothing identifies who reported: no member id, no IP address.

To act on a reported message: DELETE /v1/server/messages/:messageId (its reports go with it), POST /v1/server/members/:externalId/ban for its author (then dismiss, or delete the message), or …/dismiss to keep the message.

  • Deleting a member: their messages are kept and shown as “Deleted member”, or deleted with deleteMessages=true (message.deleted is sent for each, files are purged). Their open chats are disconnected.
  • Banning a member: they can’t read or post in any channel of the project until unbanned; their open chats are disconnected (access.revoked).
  • Removing a member from a private channel: their open chat on that channel is disconnected.
  • Changing a channel’s access / deleting a channel: every open chat of that channel re-checks its access (deleting removes messages, integrations, channel members and files).
  • Deleting a message: it disappears live for everyone (pinned or not); its files and its reports are deleted.
  • Pinning / unpinning: every reader sees it live (message.pinned / message.unpinned).
  • Changing a role: open chats show or hide the admin actions and badges right away (member.role_changed).
  • Reporting a message (chat): the project owner gets an email (one at most every 15 minutes per project). The same member reporting the same message again within 24 hours counts once. A reported message comes back after a dismiss only if someone reports it again.
const channel = await komozu.channels.create({ name: 'Tournament – Nov 2', externalId: `evt_${event.id}` });

Then, on the event page, @komozu/react shows it with <KomozuChat clientId="kz_live_…" channel={{ externalId: 'evt_1102' }} />. Without React, create one integration per channel in the dashboard.