Chat
DMs, group chats, public channels, stream chat. Full real-time messaging plane.
On this page
1. Channel types — pick one
| Type | Use case | Notes |
|---|---|---|
direct | 1:1 DM between two users | Idempotent — same pair always returns the same channel |
group | Named group (project room, order thread, support ticket) | Invite-only; owner + members |
public | Anyone can join | Has a name; community / town hall |
stream | Chat scoped to a live stream/session | Auto-created when needed; externalRef = streamId |
2. Create a channel
From your backend (using the API key):
// NestJS example — POST /api/your-app/contacts/:id/chat
@Post('contacts/:contactId/chat')
async ensureDm(@Req() req: any, @Param('contactId') contactId: string) {
const r = await fetch('https://kardocloud.com/v1/me/channels', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.KARDOLIVE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'direct',
memberIds: [req.user.id, contactId], // exactly 2 IDs for direct
projectId: process.env.KARDOLIVE_PROJECT_ID,
externalRef: `contact-${contactId}`, // helps you trace it
}),
});
return r.json(); // { id, type:'direct', members:[...], createdAt }
}
Important: direct channels are idempotent. Creating the same pair twice
returns the same channel — safe to call on every page load.
3. Recipe: 1:1 DM between two your users
The most common flow. Three calls total:
// ── your app backend ────────────────────────────────
// 1) Get-or-create the channel
POST https://kardocloud.com/v1/me/channels
Authorization: Bearer kl_live_xxx
{
"type": "direct",
"memberIds": ["user_42", "user_99"]
}
→ { "id": "chn_dm_xxx", ... }
// 2) Mint a chat token for the calling user
POST https://kardocloud.com/v1/me/channels/token
Authorization: Bearer kl_live_xxx
{
"userId": "user_42",
"displayName": "Yousof",
"channelId": "chn_dm_xxx"
}
→ { "token": "<jwt>", "chatUrl": "wss://chat.kardocloud.com", "expiresAt": "..." }
// ── your app client ────────────────────────────────
// 3) Open WebSocket
new WebSocket('wss://chat.kardocloud.com/v1/chat/socket?room=chn_dm_xxx&token=<jwt>')
Use the useChat hook from React quickstart to wire all three steps in ~10 lines.
4. Recipe: group channel per your app "thing"
Examples:
- One channel per your app sales deal (lead + assigned rep + manager)
- One channel per support ticket (customer + agents)
- One channel per order (buyer + seller + customer service)
POST https://kardocloud.com/v1/me/channels
Authorization: Bearer kl_live_xxx
{
"type": "group",
"name": "Deal #1234 · Acme Corp",
"memberIds": ["user_42", "user_99", "user_5"],
"projectId": "<your project>",
"externalRef": "deal-1234" // for your own audit/trace
}
Adding / removing members later:
POST /v1/me/channels/:id/members { "userId": "user_77", "role": "member" }
DELETE /v1/me/channels/:id/members/:userId
Renaming or updating channel metadata
Use PATCH /v1/me/channels/:id to change a group/public channel's name,
avatar, description, or external reference. All fields are optional — send only
what you want to change. Direct (type: "direct") channels reject renames
because their identity is derived from their two members.
PATCH https://kardocloud.com/v1/me/channels/<channelId>
Authorization: Bearer kl_live_xxx
{
"name": "Weekend hike crew", // optional
"avatarUrl": "https://your-cdn.example.com/groups/42.png", // optional
"description": "Saturday morning meetups, all welcome", // optional
"externalRef": "team-42" // optional, your own id
}
→ {
"id": "chn_xxx",
"type": "group",
"name": "Weekend hike crew",
"metadata": {
"avatarUrl": "https://your-cdn.example.com/groups/42.png",
"description": "Saturday morning meetups, all welcome"
},
"members": [ ... ]
}
avatarUrl and description are stored on the channel's
metadata JSON field so they round-trip through GET responses without
schema changes.
5. Recipe: chat scoped to a live stream
When a your app user starts a live stream, automatically attach a chat channel.
// after creating the stream
POST /v1/me/channels
{
"type": "stream",
"name": "Order #1234 live walkthrough",
"externalRef": "<streamId>" // ties chat back to your stream
}
Use the channel's id as the room param in the WebSocket. The 7 example
scenarios all do this automatically when you pass ?room=<streamId> — they look
up the matching stream-chat channel by externalRef.
6. Features available out of the box
Every channel — regardless of type — supports all of these via the WebSocket protocol:
| Feature | WS command | Event you receive |
|---|---|---|
| Send message | chat.send | chat |
| Threaded reply | chat.send with parentId | chat |
| Edit message | chat.edit | chat.edited |
| Delete message | chat.delete | chat.deleted |
| Reaction (per-message) | reaction.add with messageId | reaction |
| Reaction (channel-wide) | reaction.add without messageId | reaction |
| Typing indicator | typing.start / typing.stop | typing |
| Read marker (per-user) | read.mark | read |
| Poll create / vote / close | poll.create / poll.vote / poll.close | poll |
| Q&A queue | qa.ask / qa.upvote / qa.answer | qa |
| Gift (e-commerce) | gift.send | gift |
| Super chat (paid pin) | superchat.send | superchat |
| Moderation actions | mod.timeout / mod.ban / mod.mute / mod.slowmode / mod.bannedwords | mod.action |
@Mentions are auto-parsed from message body — no extra command. Tokens with role: 'host' can use the moderation commands; others get a server-side error.
The your app client can also call the REST API directly when WebSocket isn't appropriate (server-side bots, search, history backfill). See API reference.
7. File / image / voice uploads
Two-step: get a pre-signed S3 URL, upload, then send a message referencing the URL.
// Step 1 — your your app backend asks Kardolive for an upload URL
POST https://kardocloud.com/v1/me/channels/uploads/url
Authorization: Bearer kl_live_xxx
{ "mime": "image/png", "filename": "screenshot.png", "sizeBytes": 250000 }
→ {
"uploadUrl": "https://<s3-presigned>",
"publicUrl": "https://kardocloud.com/uploads/...",
"expiresIn": 300,
"maxBytes": 26214400
}
// Step 2 — your app client uploads directly to S3 with the pre-signed PUT URL
await fetch(uploadUrl, { method: 'PUT', headers: { 'Content-Type': 'image/png' }, body: file });
// Step 3 — send a message with the attachment metadata
ws.send(JSON.stringify({
type: 'chat.send',
data: {
body: 'Here is the screenshot',
attachments: [
{ url: publicUrl, kind: 'image', mime: 'image/png', size: 250000, name: 'screenshot.png' }
]
}
}));
25 MB cap per attachment. AES-256 SSE-encrypted at rest. CDN-fronted public URL.
8. Full-text search
Postgres tsvector + GIN index. websearch_to_tsquery syntax — phrases in quotes, OR / negation supported.
GET /v1/me/channels/search/messages?q=invoice%20%22past%20due%22&channelId=chn_xyz
Authorization: Bearer kl_live_xxx
→ [{ id, channelId, authorId, authorName, body, createdAt, ... }, ...]
From the your app backend, build a "Search messages" UI in your CRM that queries this with the user's chosen channel.
9. Moderation
Two layers: built-in (per-channel) and your own (server-side).
Built-in (free, no extra code)
| Action | WS command (host role) | Effect |
|---|---|---|
| Timeout user | mod.timeout {userId, durationSec} | Read-only until timeout expires |
| Ban / unban | mod.ban / mod.unban | Permanent block / unblock |
| Mute / unmute | mod.mute / mod.unmute | Same as timeout but indefinite |
| Slow mode | mod.slowmode {onSec} | Min seconds between posts per user |
| Banned word list | mod.bannedwords {words} | Auto-redact on send |
| Delete any message | mod.delete {messageId} | Soft delete (preserved for audit) |
Your own (webhooks)
Subscribe to chat.message.created from your backend. Run any custom logic — your own AI moderator, PII scrubber, compliance check — and either let it pass or call DELETE /v1/me/channels/:id/messages/:msgId to redact.
10. Push notifications
When a user is offline and receives a DM or @mention, Kardolive can push a notification via APNs (iOS) or FCM (Android + Web).
- One-time per project: upload your Firebase service-account JSON and/or your APNs
.p8key in Project → Push notifications. - From your app client: on app launch (after the user grants permission), register the device token:
// your app client — after permission grant
const apnsOrFcmToken = await getDeviceToken(); // your platform-specific call
await fetch('/api/kardolive/register-device', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ platform: 'ios', token: apnsOrFcmToken }),
});
// your app backend
@Post('register-device')
async registerDevice(@Req() req: any, @Body() body: any) {
return fetch('https://kardocloud.com/v1/me/push/devices', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.KARDOLIVE_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: req.user.id,
platform: body.platform,
token: body.token,
projectId: process.env.KARDOLIVE_PROJECT_ID,
}),
}).then((r) => r.json());
}
That's it. From then on, whenever someone @mentions or DMs that user, Kardolive sends a push automatically.