Skip to content

JavaScript SDK

Headless client for your own UI. Prefer widgets? Use the Angular UI Kit or Flutter UI Kit.

Before code: mint a session token on your server. Never ship sk_* to the browser.
Guided path: Quick start → JavaScript SDK.

Fast path

Do this in order — most integrations are these five steps.

1. Install

bash
pnpm add @sendsar/chat-sdk-javascript

2. Connect

ts
import Sendsar from "@sendsar/chat-sdk-javascript";

const session = await fetch("/api/chat/session", {
  method: "POST",
  credentials: "include",
}).then(async (r) => {
  if (!r.ok) throw new Error(await r.text());
  return r.json(); // { token, apiUrl, chatUserId, … }
});

const client = Sendsar.init({ apiUrl: session.apiUrl });
await client.connect({ userId: session.chatUserId, token: session.token });

3. Conversation list (inbox)

ts
import { SOCKET_EVENT } from "@sendsar/chat-sdk-javascript";

const rooms = await client.listRooms();
setConversations(rooms); // your ConversationList state

// Keep inbox live even when no thread is open
client.on(SOCKET_EVENT.NEW_MESSAGE, (msg) => {
  bumpConversation(msg); // last preview, time, sort, unread
});

4. Message thread (open room)

Mount when the user opens a chat; destroy when they leave.

ts
import { createRoomSubscription } from "@sendsar/chat-sdk-javascript";

const sub = createRoomSubscription(client, {
  roomId: openRoomId,
  userId: session.chatUserId,
  onInitialMessages: (messages) => setMessages(messages),
  onMessage: (msg) => {
    appendById(msg);           // MessageList
    bumpConversation(msg);     // ConversationList
    void client.hydrateFileAccessUrls([msg]);
  },
  onMessageUpdated: (msg) => replaceById(msg), // edit / delete / reaction / pin
});

// on leave thread:
sub.destroy();

5. Send

ts
const sent = await client.sendMessage(openRoomId, {
  parts: [{ type: "text", text: "Hello" }],
});
appendById(sent);
bumpConversation(sent);

Screens (what to update)

ScreenShowsYour component
Conversation listRooms, last message, unreadConversationList / inbox page
Message threadBubbles + composer for one roomIdMessageList / chat page

Also common: Room info (members), Call UI.

HappensConversation listMessage thread
createRoomAdd rowOptional: open it
New message (send* or new-message)Bump preview / unreadAppend if that room is open
Edit / delete / reaction / pin (message-updated)Preview only if it was latestReplace by message.id
forwardMessageBump target roomsAppend in open target
deleteConversationRemove rowClose if open
clearHistoryClear previewEmpty list
Members changeOptional subtitleSystem line + Room info
Incoming callOptional badgeCall UI

WARNING

message-updatedreplace the bubble by id. Never append a second one.


Messaging APIs

Peers must exist (logged in once, or upserted with sk_*). Prefer externalId for stable DMs.

Create room & send text

ts
const room = await client.createRoom({
  externalId: `dm:${session.chatUserId}:user_bob`,
  participants: [{ id: session.chatUserId }, { id: "user_bob" }],
});
// → Conversation list: add row

const sent = await client.sendMessage(room.id, {
  parts: [{ type: "text", text: "Hello" }],
});
// → Thread: append · List: bump · Event: new-message

Edit, delete, reply

ts
await client.updateMessage(roomId, messageId, {
  parts: [{ type: "text", text: "Updated" }],
}); // → replace by id · message-updated

await client.deleteMessage(roomId, messageId);
// → replace by id (deletedAt) · message-updated

await client.sendMessage(roomId, {
  parts: [{ type: "text", text: "Replying!" }],
  parentMessageId: messageId,
}); // → append (+ quote) · new-message

Reactions

Emoji on a message. Toggle = add or remove for the current user.

ts
await client.toggleReaction(roomId, messageId, { emoji: "👍" });
// → replace by id (reactions) · message-updated

Pin & forward

ts
await client.pinMessage(roomId, messageId);
await client.unpinMessage(roomId, messageId);
// → replace by id · optional getPinnedMessages()

await client.forwardMessage(roomId, messageId, { targetRoomIds: [otherRoomId] });
// → target rooms: new-message + bump list

Send files

ts
const message = await client.sendFileMessage(roomId, {
  file: input.files[0],
  onProgress: (n) => setUploadPercent(n),
});
// → append · hydrateFileAccessUrls for preview URLs after live events

Members & conversation

ts
await client.addParticipant(roomId, { userId: otherUserId });
await client.removeParticipant(roomId, otherUserId);
client.on("room-participants-changed", (ev) => {
  if (ev.roomId === roomId) refreshMembers(); // getRoom
});

await client.deleteConversation(roomId); // → remove from list
await client.clearHistory(roomId);       // → empty thread (you only)

Voice & video (optional)

Enable in the dashboard, then:

bash
pnpm add @sendsar/call-sdk-javascript
ts
import { CallClient } from "@sendsar/call-sdk-javascript";

const calls = new CallClient({ chat: client });
calls.on("incoming", (invite) => showIncomingCall(invite)); // Call UI
calls.on("localTrack", ({ track }) => track.attach(localVideo));
calls.on("remoteTrack", ({ track }) => track.attach(remoteVideo));
await calls.start(room.id, { type: "video" });

More: Quick start Calls step.


Checklist

  • [ ] Session from your BFF — no sk_* in the client
  • [ ] Conversation list: listRooms + new-message bumps
  • [ ] Message thread: createRoomSubscription (destroy on leave)
  • [ ] message-updated → replace by id
  • [ ] Files: hydrateFileAccessUrls when showing previews
  • [ ] Calls (if needed): separate package + dashboard flag

Next