Appearance
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-javascript2. 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)
| Screen | Shows | Your component |
|---|---|---|
| Conversation list | Rooms, last message, unread | ConversationList / inbox page |
| Message thread | Bubbles + composer for one roomId | MessageList / chat page |
Also common: Room info (members), Call UI.
| Happens | Conversation list | Message thread |
|---|---|---|
createRoom | Add row | Optional: open it |
New message (send* or new-message) | Bump preview / unread | Append if that room is open |
Edit / delete / reaction / pin (message-updated) | Preview only if it was latest | Replace by message.id |
forwardMessage | Bump target rooms | Append in open target |
deleteConversation | Remove row | Close if open |
clearHistory | Clear preview | Empty list |
| Members change | Optional subtitle | System line + Room info |
| Incoming call | Optional badge | Call UI |
WARNING
message-updated → replace 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-messageEdit, 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-messageReactions
Emoji on a message. Toggle = add or remove for the current user.
ts
await client.toggleReaction(roomId, messageId, { emoji: "👍" });
// → replace by id (reactions) · message-updatedPin & 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 listSend files
ts
const message = await client.sendFileMessage(roomId, {
file: input.files[0],
onProgress: (n) => setUploadPercent(n),
});
// → append · hydrateFileAccessUrls for preview URLs after live eventsMembers & 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-javascriptts
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-messagebumps - [ ] Message thread:
createRoomSubscription(destroy on leave) - [ ]
message-updated→ replace byid - [ ] Files:
hydrateFileAccessUrlswhen showing previews - [ ] Calls (if needed): separate package + dashboard flag