Skip to content

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

ProblemFix
404 when your server mints a tokenCall POST /users before POST /auth/token on your backend
Chat fails in the browserUse the session JWT from your session route — never the API key
WebSocket stops after token refreshCall connect() again, or use createSessionManager
No online presenceConnect with a session JWT (auth.token), not the API key
Acting as the wrong userLet 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-javascript
html
<!-- 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:

  1. Calls GET /api/chat/session (your route from Server setup)
  2. Connects with token, chatUserId, and apiUrl
  3. 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 joinRoom again 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

ExportPurpose
createSessionManagerSession fetch, connect, token refresh
createRoomSubscriptionJoin room, history, live updates
ComposerTypingControllerOutgoing typing while user composes
createTenantPresenceTrackerOnline users
mergeMessagesById, reconcileSelfSentMessageUpdate message lists

Client API

MethodDescription
Sendsar.init({ apiUrl })Create client
connect({ userId, token })Authenticate + WebSocket
disconnect()Close connection
listRooms, getMessages, sendMessageREST
updateMessage, deleteMessage, toggleReactionMessage actions
joinRoom, leaveRoom, setTypingRealtime
startCall, acceptCall, declineCall, endCall, getActiveCallCalls REST (when enabled)
callInvite, callAccepted, callDeclined, callEndedCall realtime listeners
registerDeviceToken, unregisterDeviceTokenPush (after connect(); session JWT)
on(event, handler)Messaging event subscriptions

Calls setup: Voice & video calls. Push setup: Push & device tokens.


Events

EventWhen
new-messageIncoming message
message-updatedEdit, reaction, or delete
typingTyping indicator
tenant-presenceUser online / offline
room-readRead 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

Sendsar — headless chat for B2B platforms