Chat

DMs, group chats, public channels, stream chat. Full real-time messaging plane.

1. Channel types — pick one

TypeUse caseNotes
direct1:1 DM between two usersIdempotent — same pair always returns the same channel
groupNamed group (project room, order thread, support ticket)Invite-only; owner + members
publicAnyone can joinHas a name; community / town hall
streamChat scoped to a live stream/sessionAuto-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:

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:

FeatureWS commandEvent you receive
Send messagechat.sendchat
Threaded replychat.send with parentIdchat
Edit messagechat.editchat.edited
Delete messagechat.deletechat.deleted
Reaction (per-message)reaction.add with messageIdreaction
Reaction (channel-wide)reaction.add without messageIdreaction
Typing indicatortyping.start / typing.stoptyping
Read marker (per-user)read.markread
Poll create / vote / closepoll.create / poll.vote / poll.closepoll
Q&A queueqa.ask / qa.upvote / qa.answerqa
Gift (e-commerce)gift.sendgift
Super chat (paid pin)superchat.sendsuperchat
Moderation actionsmod.timeout / mod.ban / mod.mute / mod.slowmode / mod.bannedwordsmod.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.

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)

ActionWS command (host role)Effect
Timeout usermod.timeout {userId, durationSec}Read-only until timeout expires
Ban / unbanmod.ban / mod.unbanPermanent block / unblock
Mute / unmutemod.mute / mod.unmuteSame as timeout but indefinite
Slow modemod.slowmode {onSec}Min seconds between posts per user
Banned word listmod.bannedwords {words}Auto-redact on send
Delete any messagemod.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).

  1. One-time per project: upload your Firebase service-account JSON and/or your APNs .p8 key in Project → Push notifications.
  2. 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.

Next

Live stream → · Conference → · Calls →