Appearance
HTML & vanilla JS
Use @sendsar/chat-sdk-javascript for plain HTML, Vue, Svelte, Angular, or any stack without React.
New? Follow the Quick start. Your app connects with a session token from your backend — never your API key. Details: Server setup.
Using React or Next.js? See React & Next.js.
Optional: voice & video
Signaling (startCall, acceptCall, callInvite(), …) is in @sendsar/chat-sdk-javascript. Media (mic, camera) is in media kit (planned) (+ media UI kit (planned) for React). See Voice & video calls. Chat-only accounts can skip calls.
Common mistakes
| Problem | Fix |
|---|---|
404 when your server mints a token | Call POST /users before POST /auth/token on your backend |
| Chat fails in the browser | Use the session JWT from your session route — never the API key |
| WebSocket stops after token refresh | Call connect() again, or use createSessionManager |
| No online presence | Connect with a session JWT (auth.token), not the API key |
| Acting as the wrong user | Let the SDK use chatUserId from your session response; do not hard-code another user |
Full auth reference: Authentication.
Client SDK
There is no build step required for plain HTML — load the browser bundle from a CDN or npm. Examples below use CDN (script tag), CDN (ES module), JavaScript, and TypeScript.
Install
bash
npm install @sendsar/chat-sdk-javascripthtml
<!-- Classic script — exposes global `Sendsar`. Socket.IO is bundled in. -->
<script src="https://cdn.jsdelivr.net/npm/@sendsar/chat-sdk-javascript@0.1.0/dist/sendsar.browser.min.js"></script>html
<script type="module">
import Sendsar from "https://cdn.jsdelivr.net/npm/@sendsar/chat-sdk-javascript@0.1.0/dist/sendsar.browser.esm.js";
// …
</script>Replace @0.1.0 with your installed version. unpkg works the same way.
Self-hosting
Copy node_modules/@sendsar/chat-sdk-javascript/dist/sendsar.browser.min.js to your static assets — useful before the package is published to npm (jsDelivr/unpkg require a published version).
Connect and listen
Fetch a session from your backend, then connect with the SDK. Each example below:
- Calls
GET /api/chat/session(your route from Server setup) - Connects with
token,chatUserId, andapiUrl - Joins a room and logs incoming messages
html
<script src="https://cdn.jsdelivr.net/npm/@sendsar/chat-sdk-javascript@0.1.0/dist/sendsar.browser.min.js"></script>
<script>
(async () => {
const res = await fetch("/api/chat/session", { credentials: "include" });
if (!res.ok) throw new Error("Not authenticated");
const { token, chatUserId, apiUrl } = await res.json();
const client = Sendsar.init({ apiUrl });
await client.connect({ userId: chatUserId, token });
client.joinRoom({ roomId: "YOUR_ROOM_UUID" });
client.on("new-message", (msg) => {
console.log(msg);
});
})();
</script>html
<script type="module">
import Sendsar from "https://cdn.jsdelivr.net/npm/@sendsar/chat-sdk-javascript@0.1.0/dist/sendsar.browser.esm.js";
const res = await fetch("/api/chat/session", { credentials: "include" });
if (!res.ok) throw new Error("Not authenticated");
const { token, chatUserId, apiUrl } = await res.json();
const client = Sendsar.init({ apiUrl });
await client.connect({ userId: chatUserId, token });
client.joinRoom({ roomId: "YOUR_ROOM_UUID" });
client.on("new-message", (msg) => {
console.log(msg);
});
</script>js
import Sendsar from "@sendsar/chat-sdk-javascript";
const res = await fetch("/api/chat/session", { credentials: "include" });
if (!res.ok) throw new Error("Not authenticated");
const { token, chatUserId, apiUrl } = await res.json();
const client = Sendsar.init({ apiUrl });
await client.connect({ userId: chatUserId, token });
client.joinRoom({ roomId: "YOUR_ROOM_UUID" });
client.on("new-message", (msg) => {
console.log(msg);
});ts
import Sendsar from "@sendsar/chat-sdk-javascript";
import type { Message } from "@sendsar/chat-sdk-javascript";
const res = await fetch("/api/chat/session", { credentials: "include" });
if (!res.ok) throw new Error("Not authenticated");
const { token, chatUserId, apiUrl } = await res.json();
const client = Sendsar.init({ apiUrl });
await client.connect({ userId: chatUserId, token });
client.joinRoom({ roomId: "YOUR_ROOM_UUID" });
client.on("new-message", (msg: Message) => {
console.log(msg);
});Use the room id (UUID) from your server or POST /v1/chat/rooms — not a numeric index.
Session lifecycle
Use createSessionManager for connect, token refresh, and disconnect on page hide.
js
import { createSessionManager } from "@sendsar/chat-sdk-javascript";
const manager = createSessionManager({
fetchSession: () => fetch("/api/chat/session", { credentials: "include" }),
onStateChange: (state) => {
// state.status: idle | loading | ready | offline | error
// state.client — use when status === "ready"
},
});
await manager.start();
// await manager.stop();ts
import { createSessionManager } from "@sendsar/chat-sdk-javascript";
import type { SessionManagerState } from "@sendsar/chat-sdk-javascript";
const manager = createSessionManager({
fetchSession: () => fetch("/api/chat/session", { credentials: "include" }),
onStateChange: (state: SessionManagerState) => {
if (state.status === "ready" && state.client) {
// connected — open rooms, render UI
}
},
});
await manager.start();html
<script src="https://cdn.jsdelivr.net/npm/@sendsar/chat-sdk-javascript@0.1.0/dist/sendsar.browser.min.js"></script>
<script>
const manager = Sendsar.createSessionManager({
fetchSession: () => fetch("/api/chat/session", { credentials: "include" }),
onStateChange: (state) => {
if (state.status === "ready") {
window.chatClient = state.client;
}
},
});
manager.start();
</script>Your backend session route returns token, apiUrl, chatUserId, expiresAt, and optionally displayName. The app uses apiUrl from that response — do not hard-code it separately.
Token refresh runs 5 minutes before expiry by default. If the JWT lifetime is shorter than that, refresh happens immediately after connect.
No secrets in the client
Do not embed API keys, webhook secrets, or server credentials in browser or mobile builds.
Realtime constraints
Connect before subscribing
Always await client.connect() (or wait for createSessionManager status ready) before REST calls. createRoomSubscription waits for connected if the socket is still connecting.
One active room per WebSocket
The gateway tracks one joined room at a time per connection for presence and typing. When you joinRoom for a new conversation, the previous room is left on the socket.
- Messages for all your rooms still arrive (delivered to your user channel).
- Typing and room presence apply to the room you last joined — call
joinRoomagain when the user opens a conversation.
For a single-thread messenger (one open chat at a time), this matches typical UX.
Rooms and messages
Call this after connect(). createRoomSubscription joins the room, loads history, and wires live updates.
js
import { createRoomSubscription } from "@sendsar/chat-sdk-javascript";
const sub = createRoomSubscription(client, {
roomId,
userId: chatUserId,
onInitialMessages: (messages) => {
// chronological messages
},
onMessage: (msg) => {
// live update
},
});
// cleanup
sub.destroy();ts
import { createRoomSubscription } from "@sendsar/chat-sdk-javascript";
import type { Message } from "@sendsar/chat-sdk-javascript";
const sub = createRoomSubscription(client, {
roomId,
userId: chatUserId,
onInitialMessages: (messages: Message[]) => {
renderThread(messages);
},
onMessage: (msg) => {
appendMessage(msg);
},
onMessageUpdated: (msg) => {
mergeMessage(msg);
},
});
sub.destroy();html
<script>
const sub = Sendsar.createRoomSubscription(client, {
roomId: "YOUR_ROOM_UUID",
userId: chatUserId,
onInitialMessages: (messages) => renderThread(messages),
onMessage: (msg) => appendMessage(msg),
});
// cleanup: sub.destroy();
</script>Send a message
js
await client.sendMessage(roomId, {
parts: [{ type: "text", text: "Hello" }],
clientMessageId: crypto.randomUUID(),
});ts
await client.sendMessage(roomId, {
parts: [{ type: "text", text: "Hello" }],
clientMessageId: crypto.randomUUID(),
});html
<script>
await client.sendMessage(roomId, {
parts: [{ type: "text", text: "Hello" }],
clientMessageId: crypto.randomUUID(),
});
</script>Helpers
| Export | Purpose |
|---|---|
createSessionManager | Session fetch, connect, token refresh |
createRoomSubscription | Join room, history, live updates |
ComposerTypingController | Outgoing typing while user composes |
createTenantPresenceTracker | Online users |
mergeMessagesById, reconcileSelfSentMessage | Update message lists |
Client API
| Method | Description |
|---|---|
Sendsar.init({ apiUrl }) | Create client |
connect({ userId, token }) | Authenticate + WebSocket |
disconnect() | Close connection |
listRooms, getMessages, sendMessage | REST |
updateMessage, deleteMessage, toggleReaction | Message actions |
joinRoom, leaveRoom, setTyping | Realtime |
startCall, acceptCall, declineCall, endCall, getActiveCall | Calls REST (when enabled) |
callInvite, callAccepted, callDeclined, callEnded | Call realtime listeners |
registerDeviceToken, unregisterDeviceToken | Push (after connect(); session JWT) |
on(event, handler) | Messaging event subscriptions |
Calls setup: Voice & video calls. Push setup: Push & device tokens.
Events
| Event | When |
|---|---|
new-message | Incoming message |
message-updated | Edit, reaction, or delete |
typing | Typing indicator |
tenant-presence | User online / offline |
room-read | Read receipt |
Calls (when enabled) — use typed listeners instead of raw event names:
typescript
client.callInvite((payload) => { /* incoming */ });
client.callAccepted((payload) => { /* … */ });
client.callDeclined((payload) => { /* … */ });
client.callEnded((payload) => { /* … */ });Full call flow: Voice & video calls.
Next steps
- Voice & video calls — optional signaling + media
- UI Kit
- API Reference