In-App Chat SDK
Beta
REST + SDK

Channels & Messages

Create conversation channels, send and retrieve messages over REST or the client SDK, and layer presence, typing indicators, and read receipts for a live chat UX.

Channel types

dm, group, and broadcast channels with server-controlled membership.

REST + WebSocket

Post and list history via REST; live delivery via the client SDK socket.

Presence

online / away / offline for members — updated as clients connect and idle.

Typing

Ephemeral start/stop typing events that auto-expire after a few seconds.

Read receipts

Mark a message (or latest) as read; others get read events in real time.

Ordered history

Monotonic seq per channel with cursor-based pagination for infinite scroll.

Body limit

10 KB

UTF-8 text

History page

50

default page size

Typing TTL

~3s

auto clear

Members (group)

100

hard cap (beta)

Create Channel
POST
/v1/chat-sdk/channels
Beta

Create a channel from your server with Basic Auth. Members must already have known userIds in your system (they do not need an active token at creation time).

FieldTypeRequiredDescription
type"dm" | "group" | "broadcast"
required
dm: 2 members. group: up to 100. broadcast: many readers, restricted writers.
memberIdsstring[]
required
User ids to add. For dm, must contain exactly 2 unique ids.
namestringoptionalDisplay title (max 120 chars). Recommended for group/broadcast.
metadataobjectoptionalArbitrary JSON (max 4 KB) — e.g. ticketId, orderId, locale.
JSON
{
"type": "group",
"name": "Order #1042",
"memberIds": [
"user_01HZX9K2M",
"user_01HZX9ABC",
"agent_support_01"
],
"metadata": {
"orderId": "ORD-1042",
"priority": "normal"
}
}

Response (201)

JSON
{
"success": true,
"code": "CHAT_CHANNEL_CREATED",
"message": "Channel created.",
"requestId": "REQ-1782001200001",
"timestamp": "2026-07-12T10:01:00.000Z",
"data": {
"id": "ch_01HZYA3N",
"type": "dm",
"name": "Support · Ama",
"memberIds": ["user_01HZX9K2M", "agent_support_01"],
"metadata": {},
"createdAt": "2026-07-12T10:01:00.000Z",
"updatedAt": "2026-07-12T10:01:00.000Z"
}
}
Post Message (Server REST)
POST
/v1/chat-sdk/channels/:channelId/messages
Core

Post as a specific user or as a system message. Server posts are useful for order updates, bots, and moderation notices. Connected members receive the message over WebSocket immediately.

FieldTypeRequiredDescription
bodystring
required
Message text. Max 10 KB UTF-8.
userIdstringoptionalAuthor user id. Required unless type is system. Must be a channel member for user posts.
type"text" | "system"optionalDefault text. system messages appear as platform notices.
metadataobjectoptionalClient or server metadata (max 4 KB). Use clientMessageId for idempotency.
Bash
curl -X POST 'https://api.sendexa.co/v1/chat-sdk/channels/ch_01HZYA3N/messages' \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic YOUR_DASHBOARD_BASE64_TOKEN' \
-d '{
"userId": "agent_support_01",
"body": "Thanks for writing in — I can help with order #1042.",
"type": "text",
"metadata": {
"source": "agent-console"
}
}'
List Messages (History)
GET
/v1/chat-sdk/channels/:channelId/messages
History

Cursor-based history for infinite scroll. Messages are returned newest-first by default; reverse on the client if your UI paints oldest-first.

FieldTypeRequiredDescription
limitnumber (query)optionalPage size 1–100. Default 50.
beforestring (query)optionalMessage id cursor — return messages older than this id.
afterstring (query)optionalMessage id cursor — return messages newer than this id (sync gap fill).
Bash
# First page
curl -X GET 'https://api.sendexa.co/v1/chat-sdk/channels/ch_01HZYA3N/messages?limit=50' \
-H 'Authorization: Basic YOUR_DASHBOARD_BASE64_TOKEN'
# Older page
curl -X GET 'https://api.sendexa.co/v1/chat-sdk/channels/ch_01HZYA3N/messages?limit=50&before=msg_01HZYB8P' \
-H 'Authorization: Basic YOUR_DASHBOARD_BASE64_TOKEN'
Client SDK — Send & Receive

After init + connect with a user token, members send and receive in real time. The authenticated user is the author; you do not pass userId from the client.

sendMessage

FieldTypeRequiredDescription
channelIdstring
required
Target channel. Caller must be a member.
bodystring
required
Text content (max 10 KB).
metadataobjectoptionalOptional map. Prefer metadata.clientMessageId for optimistic UI / dedupe.
JavaScript
import { SendexaChat } from '@sendexa/chat-sdk';
const chat = SendexaChat.init({ token: userChatToken });
await chat.connect();
const message = await chat.sendMessage({
channelId: 'ch_01HZYA3N',
body: 'Hi — I need help with my order.',
metadata: {
clientMessageId: crypto.randomUUID(),
},
});
console.log(message.id, message.seq, message.createdAt);
Presence

Presence is derived from active WebSocket sessions. When the last device for a user disconnects, status moves to offline after a short grace period.

StatusMeaning
onlineAt least one connected socket for this userId
awayConnected but idle past the away threshold (~5 min)
offlineNo active sockets; lastSeenAt is set
JavaScript
// Subscribe
chat.on('presence', (event) => {
// {
// userId: 'user_01HZX9K2M',
// status: 'online' | 'away' | 'offline',
// lastSeenAt: '2026-07-12T10:10:00.000Z' | null
// }
updateAvatarDot(event.userId, event.status);
});
// Optional: set local status (e.g. user opens "Do not disturb")
await chat.setPresence('away');
// Snapshot for a channel's members
const members = await chat.getChannelPresence('ch_01HZYA3N');
// [{ userId, status, lastSeenAt }, ...]
Typing Indicators

Typing events are ephemeral and not stored in history. Call startTyping on input; the server auto-clears after ~3 seconds if you do not refresh. Use a throttle (e.g. once per 2s) while the user is typing.

JavaScript
const channelId = 'ch_01HZYA3N';
let typingTimer;
function onComposerInput() {
chat.startTyping(channelId);
clearTimeout(typingTimer);
typingTimer = setTimeout(() => {
chat.stopTyping(channelId);
}, 2000);
}
chat.on('typing', ({ channelId, userId, isTyping }) => {
if (userId === me.userId) return;
setTypingLabel(
channelId,
isTyping ? `${userId} is typing…` : null
);
});
Read Receipts

Marking read advances a per-user watermark in the channel. Other members receive a read event so you can render double-check UI.

FieldTypeRequiredDescription
channelIdstring
required
Channel to update.
messageIdstringoptionalHighest message the user has seen. Omit to mark the latest message as read.
JavaScript
// When the user opens a thread or scrolls to the bottom
await chat.markRead({
channelId: 'ch_01HZYA3N',
messageId: 'msg_01HZYB8P',
});
chat.on('read', (event) => {
// {
// channelId: 'ch_01HZYA3N',
// userId: 'agent_support_01',
// messageId: 'msg_01HZYB8P',
// readAt: '2026-07-12T10:06:00.000Z'
// }
updateReadWatermark(event);
});
// Optional snapshot
const receipts = await chat.getReadState('ch_01HZYA3N');
// [{ userId, messageId, readAt }, ...]

Client Event Reference

EventPayload (summary)When
connected{ connectionId }WebSocket ready
disconnected{ reason }Socket closed / network loss
messageMessage objectNew message in a joined channel
presence{ userId, status, lastSeenAt }Member presence changes
typing{ channelId, userId, isTyping }Typing start/stop
read{ channelId, userId, messageId, readAt }Read watermark advances
error{ code, message }Auth, validation, or transport errors

Validation & Errors

Members only

Send / list / markRead fail with 403 if the acting user is not a channel member.

Idempotent client ids

Reusing the same metadata.clientMessageId within a short window returns the original message instead of duplicating.

Empty body

Whitespace-only bodies are rejected with EMPTY_MESSAGE_BODY.

HTTPCodeCause
400INVALID_CHANNEL_TYPEtype not dm | group | broadcast
400INVALID_MEMBER_LISTdm without exactly 2 members, empty list, or duplicates
400EMPTY_MESSAGE_BODYMissing or blank body
400MESSAGE_TOO_LARGEBody exceeds 10 KB
403NOT_CHANNEL_MEMBERuserId not in channel.memberIds
404CHANNEL_NOT_FOUNDUnknown channelId
429RATE_LIMIT_EXCEEDEDMessage or channel create rate limited