Auth & tokens
How your server signs the JWTs that your clients use to connect to Kardolive.
On this page
1. One-time setup
- Sign up at
https://kardocloud.com/app/signup(or use the SSO bridge when available). - Create a Project at
/app/projects. One project per environment is the norm:your-app-devyour-app-stagingyour-app-prod
- Toggle on the features you'll use: Chat, Live streaming, Conferencing, Voice/Video call, Recording, Transcription, Webhooks.
- Open the project → API keys → mint a key with scopes:
chat:token— mint chat JWTschannels:read+channels:write— manage channelsstreams:read+streams:write— manage live streamsrecordings:read+recordings:writewebhooks:read+webhooks:write
- Store the
kl_live_*key in your secrets store. Treat it like a database password.
Security boundary: the kl_live_* key authenticates your server to Kardolive. End-user clients never see this key. They use short-lived JWTs your server mints.
2. The full integration flow
┌──────────────┐ 1. user logs into your app ┌────────────────┐
│ │ ──────────────────────────────▶ │ │
│ your app │ │ your app │
│ client │ 2. ask for chat/video token │ backend │
│ (browser/ │ ──────────────────────────────▶ │ (NestJS, etc) │
│ mobile) │ │ │
│ │ │ HOLDS │
│ │ │ kl_live_xxx │
│ │ 4. { token, chatUrl, sfuUrl } │ │
│ │ ◀────────────────────────────── │ │
└──────┬───────┘ └────────┬───────┘
│ │
│ 5. open WebSocket with token │ 3. POST /v1/me/channels/token
│ │ with kl_live_xxx
│ ▼
│ ┌─────────────────┐
│ 6. real-time traffic │ │
└──────────────────────────────────────────────▶│ Kardolive │
│ API + chat WS │
│ + mediasoup │
└─────────────────┘
Two things to notice:
- The
kl_live_key never leaves your server. - Real-time traffic is direct — your backend doesn't proxy chat or media. That's why Kardolive scales independently.
3. Server-side: mint user tokens
your backend exposes one endpoint per surface that exchanges the long-lived API key for a short-lived user JWT. The user's identity is whatever your app already knows about them.
NestJS — chat token endpoint
// your-app/apps/backend/src/kardolive/kardolive.controller.ts
import { Body, Controller, Post, Req, UseGuards } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtAuthGuard } from '../auth/jwt.guard';
@Controller('kardolive')
@UseGuards(JwtAuthGuard)
export class KardoliveController {
constructor(private readonly config: ConfigService) {}
@Post('chat-token')
async chatToken(
@Req() req: { user: { id: string; name?: string } },
@Body() body: { channelId?: string },
) {
const res = await fetch('https://kardocloud.com/v1/me/channels/token', {
method: 'POST',
headers: {
Authorization: `Bearer ${this.config.get('KARDOLIVE_API_KEY')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId: req.user.id, // your users ID
displayName: req.user.name,
channelId: body.channelId, // optional: scope to one channel
}),
});
if (!res.ok) throw new Error(`Kardolive token mint failed: ${res.status}`);
return res.json(); // { token, chatUrl, expiresAt }
}
}
NestJS — session token endpoint (covers live, conference, call)
// your-app/apps/backend/src/kardolive/kardolive.controller.ts
@Post('session-token')
async sessionToken(
@Req() req: { user: { id: string; name?: string } },
@Body() body: { sessionId: string; role?: 'host' | 'cohost' | 'guest' | 'viewer' },
) {
const res = await fetch(
`https://kardocloud.com/v1/me/sessions/${body.sessionId}/token`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${this.config.get('KARDOLIVE_API_KEY')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId: req.user.id,
displayName: req.user.name,
role: body.role ?? 'guest',
}),
},
);
if (!res.ok) throw new Error(`Kardolive session token failed: ${res.status}`);
return res.json();
// returns:
// { token, sfuUrl, chatUrl, sessionId, role,
// perms: { audio: bool, video: bool, screen: bool },
// waitingRoom: bool, expiresAt }
}
Plain Node.js — same idea, no framework
// Express / Fastify / raw Node
app.post('/api/kardolive/chat-token', async (req, res) => {
const r = await fetch('https://kardocloud.com/v1/me/channels/token', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.KARDOLIVE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId: req.user.id,
displayName: req.user.name,
channelId: req.body.channelId,
}),
});
res.status(r.status).json(await r.json());
});
4. Client-side: use the token
The client never sees kl_live_*. It only ever sees its own short-lived JWT.
React / browser — chat connection
// 1) fetch a token from your server
const tokRes = await fetch('/api/kardolive/chat-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channelId: 'chn_abc123' }),
});
const { token, chatUrl } = await tokRes.json();
// 2) open the chat WebSocket
const ws = new WebSocket(
`${chatUrl}/v1/chat/socket?room=chn_abc123&token=${token}`,
);
// 3) send & receive
ws.onopen = () =>
ws.send(JSON.stringify({ type: 'chat.send', data: { body: 'hi' } }));
ws.onmessage = (e) => console.log(JSON.parse(e.data));
See Chat → for a full React component that wraps this.
5. Security: what to put where
| Token | Lifetime | Lives on | Used by |
|---|---|---|---|
kl_live_* API key |
Until you revoke | your server (env var / secrets manager) | Server → Kardolive REST API only |
| User JWT (chat / session / SFU) | 1 hour | Mint per-request, return to client | Client WebSocket connections |
Common mistakes:
- Putting
kl_live_*in client-side JS — anyone can scrape your bundle and use your quota. - Reusing one JWT across many users — every user should get their own JWT with their own
userIdclaim, so analytics and per-user moderation work. - Storing JWTs in localStorage long-term — they expire in 1 hour. Mint fresh each session.
Next
You've got auth wired. Pick your client surface:
React quickstart → ·
Vanilla JS → ·
Chat →