For AI agents: Find a documentation index at https://talkjs.com/llms.txt (full content at https://talkjs.com/llms-full.txt). Get a markdown version of any page by appending .md to its URL path.
Skip to content

Node.js

Node.js chat API: put your server in the conversation

With TalkJS, your Node.js server can take part in the chat. A buyer asks “Where’s my order?” in the order thread, and your server replies a moment later as your shop’s account, with the status from your order data. We’d have it listen as a user in the threads it belongs to, and use webhooks for everything else.

The code is trimmed from a listener we ran. The full version also skips the messages already in the chat when it starts.

import { getTalkSession } from '@talkjs/core';

const session = getTalkSession({ appId: APP_ID, userId: 'shop' });
// Token signed on your server with your secret key
session.setToken(userToken('shop'));

session.subscribeConversations(async (conversations) => {
  const latest = conversations?.[0]?.lastMessage;
  if (!latest || latest.sender?.id === 'shop') return;
  await session
    .conversation(conversations[0].id)
    .send(`Thanks ${latest.sender?.name}, checking your order now.`);
});
You
Hi, where’s my order? It’s #1001.
9:41 AM
SU
Thanks, Bob, checking your order now.
9:41 AM
SU
Order #1001 shipped yesterday. It should arrive on Thursday.
9:41 AM
You
Great, will I get a delivery update?
9:42 AM
SU
Yes, we’ll let you know when it’s out for delivery.
9:42 AM
Write a reply…
Carol
Where’s my order? It’s #1002.
10:02 AM
SU
Thanks, Carol, checking your order now.
10:02 AM
You
It’s packed and goes out with today’s pickup.
10:04 AM
Carol
Perfect, thanks for checking!
10:05 AM
You
You’re welcome. I’ll share the tracking details here.
10:05 AM
Write a reply…
Classtime
Decathlon
Riot Games
Hyundai

Listen as a user, or take webhooks?

Your server can follow chats in two ways, and each one sees a different slice of your app.

Listen as a user with @talkjs/coreTake webhooks
What it seesThe conversations one user is in. One subscribeConversations hears new messages in all of them. It starts with the 20 most recently active conversations, then adds any with new activityEvery message.sent in your app, plus events such as notification.triggered
How it connectsAn outgoing WebSocket from your Node process. You don’t need a public URLA public URL on your server that TalkJS sends a POST to
While your process is downIt hears nothing. When it starts again, each conversation arrives with only its last message, so a burst of messages shows up as just that one messageTalkJS retries a failed delivery “a small number of times, with a delay”
Good forA shop or seller account that answers in its own threads, or a one-off scriptCopying every message into your database, logging notifications, or filling a moderation queue

Run a chat account inside your Node process

@talkjs/core is our Data API for JavaScript, and it runs in Node.js as well as the browser. It connects as one specific user. On a server, that user is an account you make for the job, say a “Shop updates” account that posts from your order data.

Install it with npm install @talkjs/core. It has no dependencies. Then subscribe once to the account’s conversations. Whenever the account gets a message, it shows up at conversations[0].lastMessage. Our docs call this approach “much easier and more efficient” than tracking each conversation with subscribeMessages.

// Ran on Node 22.23.4 with @talkjs/core 1.12.1, Oct 2026.
// Adapted from the TalkJS Data API docs; we renamed the test users.
import { getTalkSession } from '@talkjs/core';
import { APP_ID, userToken } from './lib.mjs'; // the token code below

const session = getTalkSession({ appId: APP_ID, userId: 'shop' });
session.setToken(userToken('shop'));

const handled = new Set();
let first = true;
const sub = session.subscribeConversations(async (conversations) => {
  const latest = conversations?.[0]?.lastMessage;
  // The first snapshot is history: note it, don't answer it
  if (first) { first = false; if (latest) handled.add(latest.id); return; }
  // Skip what we've answered and the shop's own replies
  if (!latest || handled.has(latest.id) || latest.sender?.id === 'shop') return;
  handled.add(latest.id);
  await session
    .conversation(conversations[0].id)
    .send(`Thanks ${latest.sender?.name}, checking your order now.`);
});
await sub.connected;

The shop’s own replies come back in the next snapshot, so without the sender check it would keep thanking itself. In your app, swap the thank-you line for a lookup in your order database and post the status.

In our test, one subscription heard new messages in two order threads from two different buyers. It heard each one 134 to 257 ms after we started sending it through the REST API, and both replies were stored.

On Node 22, @talkjs/core uses Node’s built-in WebSocket, so you don’t need the ws package. On older Node, install ws and call registerPolyfills, as the Node.js compatibility notes show. When we removed the WebSocket on Node 22 to mimic an older Node, @talkjs/core threw an uncaught Missing WebSocket implementation error that crashed the process. A one-off script should call process.exit() after unsubscribe(): the open connection kept our test scripts alive for minutes. A server that runs all day won’t notice.

Take webhooks in Express

With webhooks on, TalkJS sends your endpoint a POST for events such as message.sent and notification.triggered. Each one has an X-TalkJS-Signature header: an HMAC-SHA256 of the X-TalkJS-Timestamp value, a dot, and the raw request body, in uppercase hex. Check it before you parse anything. In Express, that means the webhook route takes express.raw({ type: 'application/json' }) instead of express.json().

// Ran with Express 5.3.0, Oct 2026, against events we signed locally.
// Signature rule from the TalkJS security docs (webhook integrity).
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';

const SECRET_KEY = process.env.TALKJS_SECRET_KEY; // marked "use for webhooks"

function isValidTalkJSSignature(rawBody, timestamp, signature) {
  if (!timestamp || !signature) return false;
  const expected = createHmac('sha256', SECRET_KEY)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex')
    .toUpperCase();
  return expected.length === signature.length &&
    timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

const app = express();

// Keep the raw bytes for this route
app.post('/webhooks/talkjs', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = isValidTalkJSSignature(req.body, req.get('X-TalkJS-Timestamp'), req.get('X-TalkJS-Signature'));
  if (!ok) return res.status(401).send('bad signature');
  const event = JSON.parse(req.body);
  res.sendStatus(200); // ack fast, handle async
  // event.type is 'message.sent', 'notification.triggered', ...
});

We tried both parsers on events we signed ourselves. A correctly signed, pretty-printed body failed the check after express.json(): JSON.stringify(req.body) doesn’t give back the same bytes. The same body passed with express.raw().

Tokens and REST calls from Node

// lib.mjs. Ran on Node 22.23.4, Oct 2026, with Node's built-in crypto.
// Claims from the TalkJS authentication and REST API docs.
import { createHmac } from 'node:crypto';

export const APP_ID = process.env.TALKJS_APP_ID;
const SECRET = process.env.TALKJS_SECRET_KEY; // server only

const b64url = (obj) => Buffer.from(JSON.stringify(obj)).toString('base64url');

// HS256 JWT with node:crypto only
export function signHS256(payload, secret = SECRET) {
  const data = `${b64url({ alg: 'HS256', typ: 'JWT' })}.${b64url(payload)}`;
  const sig = createHmac('sha256', secret).update(data).digest('base64url');
  return `${data}.${sig}`;
}

const now = () => Math.floor(Date.now() / 1000);
export const userToken = (userId, ttl = 3600) =>
  signHS256({ tokenType: 'user', iss: APP_ID, sub: userId, exp: now() + ttl });
export const adminToken = (ttl = 30) =>
  signHS256({ tokenType: 'admin', iss: APP_ID, exp: now() + ttl });

You don’t need a JWT package. The authentication docs sign the same claims with jsonwebtoken, and TalkJS accepted tokens from both in our test. Keep the secret key on your server and out of frontend code. In the browser, the chat gets its user token through session.onNeedToken, which fetches it from an endpoint of yours. We ran that against a plain node:http route.

// Ran on Node 22.23.4, Oct 2026, with built-in fetch.
// Endpoints from the TalkJS REST API docs; we renamed the test users.
// APP_ID and adminToken() come from lib.mjs above.
async function talkjs(method, path, body) {
  const res = await fetch(`https://api.talkjs.com/v1/${APP_ID}${path}`, {
    method,
    headers: { Authorization: `Bearer ${adminToken()}`, 'Content-Type': 'application/json' },
    body: body && JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`${method} ${path} -> ${res.status} ${await res.text()}`);
  return res.json();
}

await talkjs('PUT', '/users/shop', { name: 'Shop updates' });
await talkjs('PUT', '/users/bob', { name: 'Bob' });
await talkjs('PUT', '/conversations/order_1001', { participants: ['shop', 'bob'], subject: 'Order #1001' });
await talkjs('POST', '/conversations/order_1001/messages', [
  { text: 'Hi Bob, your order shipped.', sender: 'shop', type: 'UserMessage' },
]);

REST calls take an admin token, and the REST API docs have your server make a new one for each request. Ours expire after 30 seconds. Every call returned 200 from Node, and the message showed up when we read the conversation back. If you retry a send, add an idempotencyKey to the message: TalkJS then sends it exactly once, and keeps the key for 24 hours. The default rate limit is 9 requests a second per app, with bursts of 500, shared by every server instance you run. You can ask us to raise it.

Retreat Guru logo
We were able to offload all messaging to TalkJS — hugely simplifying our development of an otherwise complex stack.
Deryk Wenaus
CTO, Retreat Guru

Ready-made chat UI you can customize

In the browser, the pre-built chat UI connects as the logged-in user, in whatever framework your frontend uses. Pick one of the three layouts below and style it to match your app.

Inbox

The TalkJS Inbox shows the chat history on the left, selected conversation on the right. It’s designed to act as the messaging center of your app, usually living on its own page.

Chatbox

The TalkJS Chatbox is the right half of the Inbox UI mode. It takes up less space and is designed to be embedded in your app near an order confirmation, a booking, or user profile.

Popup

You can also show a conversation in a popup that only shows up when the user clicks a button somewhere. When shown, the popup is positioned on the bottom right of your page.

Why not run your own socket server?

With Socket.IO, you run and scale the socket server yourself, and message history, offline notifications, and moderation are yours to build. Our JavaScript chat library guide lists the rest of that work. With TalkJS, the Node code you write is what’s on this page. We keep the message history with unlimited retention, send email, SMS, and push notifications, and run moderation with forbidden word lists and filters for contact details and spam. Development mode is free, and Basic is $279 a month for 10,000 monthly active users.

FAQs

Yes. @talkjs/core 1.12.1 ships an ES module build for import, a CommonJS build for require() and its own type definitions, so you don’t need a separate types package.

Yes. We subscribed to a conversation with @talkjs/core on Node 24.22.1 and 26.11.1 with no ws installed. It heard every message we sent over REST, with a median delay of 134 ms on Node 24 and 135 ms on Node 26, counted from the start of the send. The replies we sent from Node were stored. Node 26 also prints “ExperimentalWarning: WebSocket over HTTP2 is experimental, and subject to change.” You can ignore it: the chat works the same.

In our talkjs-examples repository on GitHub, in the rest-api, webhooks and nodejs/basic-example folders. For a guided build, our Node.js tutorial adds one-to-one chat to an Express app that keeps its data in lowdb, with the Classic SDK in the browser.