Conference & interactive sessions
Multi-party WebRTC rooms — Zoom-style meetings, interactive livestreams, webinars, breakouts.
On this page
1. Create a session
From your backend:
POST https://kardocloud.com/v1/me/sessions
Authorization: Bearer kl_live_xxx
Content-Type: application/json
{
"type": "conference",
"name": "Q4 all-hands",
"projectId": "<your project>",
"hostId": "user_42",
"capacity": 50,
"recordingEnabled": true,
"transcriptionEnabled": true,
"waitingRoom": false,
"layout": "grid",
"inviteUserIds": ["user_42", "user_99"] // optional pre-invites
}
→ { "id": "sess_abc", "state": "live", ... }
| Type | Use case | Defaults |
|---|---|---|
conference | Zoom-style meeting | grid layout, all-publish |
webinar | Large audience, host-controlled stage | viewer-by-default |
livestream | Interactive show (Polyv/Vhall style) | spotlight layout |
call | 1:1 — see Calls → | speaker layout, capacity 2 |
2. Join from a browser
The host UI uses useSession from React quickstart — same hook works for guests, just pass role: 'guest'.
Minimal flow:
// 1) your app client asks its backend for a session token
const { token, sfuUrl, role, perms } = await fetch('/api/kardolive/session-token', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: 'sess_abc', role: 'guest' }),
}).then((r) => r.json());
// 2) Open the SFU WebSocket
const ws = new WebSocket(`${sfuUrl}/?token=${token}&room=sess_abc`);
// 3) Run the mediasoup-client handshake (use the useSession hook)
The perms field tells the client what the user can publish: { audio, video, screen }. Disable buttons your role can't use.
3. Roles & permissions
| Role | Can publish? | Can moderate? | Notes |
|---|---|---|---|
host | audio + video + screen | ✅ everything | Session creator |
cohost | audio + video + screen | ✅ mute / spotlight / kick | Co-streaming, PK battles |
guest | audio + video | ❌ | Default invited participant |
viewer | ❌ read-only | ❌ | Webinar audience |
Change a participant's role mid-session:
PATCH /v1/me/sessions/:id/participants/:userId
Authorization: Bearer kl_live_xxx
{ "role": "cohost", "canPublishScreen": true }
4. Moderation
All four moderation endpoints are POST + take a userId or empty body. Trigger from your backend or directly from a host's browser using fetch with their JWT.
// mute everyone except hosts/cohosts
POST /v1/me/sessions/:id/moderate/mute-all
Authorization: Bearer kl_live_xxx
{ "exceptHost": true }
// spotlight (pin) one participant
POST /v1/me/sessions/:id/moderate/spotlight
{ "userId": "user_42" } // pass null to clear
// promote a guest to cohost
POST /v1/me/sessions/:id/moderate/promote
{ "userId": "user_42", "role": "cohost" }
// kick a participant
DELETE /v1/me/sessions/:id/participants/<userId>
Each fires a webhook event (session.muted-all, session.spotlight, etc.) so other clients can react instantly.
5. Hand-raise → promote
The interactive-livestream pattern. Audience member taps "raise hand", host decides whether to bring them on stage.
// Audience member raises hand (their JWT lets them call this for themselves)
POST /v1/me/sessions/:id/moderate/hand-raise
{ "userId": "user_42" }
// → fires webhook session.handraise.raised
// → host UI updates with the queue
// Host taps "Let them in" → promote
POST /v1/me/sessions/:id/moderate/promote
{ "userId": "user_42", "role": "guest" } // gives them publish perms
// → hand-raise is cleared automatically
// → fires session.participant.promoted
The new guest's existing JWT is auto-updated server-side — they can start publishing immediately. No re-mint needed.
6. Layout switching
The session has a server-side layout field. Switching it broadcasts to all clients so everyone shows the same arrangement.
PATCH /v1/me/sessions/:id
{ "layout": "speaker" } // grid | speaker | sidebar | spotlight
Clients receive a session.updated event over chat WS and re-render. The recorder also uses this layout for its composite output.
7. Breakout rooms
Spawn N child sessions from a parent conference with one call:
POST /v1/me/sessions/<parent>/breakouts
{
"count": 3,
"assignments": {
"1": ["user_42", "user_99"],
"2": ["user_5", "user_7"],
"3": ["user_11"]
}
}
→ [
{ "id": "sess_b1", "name": "... · Breakout 1", "parentSessionId": "sess_parent" },
...
]
Each child is a real session. Assigned users get pre-invited; pass their JWTs as ?room=<breakoutId> when they're ready to switch.
Close all breakouts and return to the main session:
POST /v1/me/sessions/<parent>/breakouts/close
→ { "closed": 3 }
8. Composite recording
Triggers a multi-publisher FFmpeg composite on the recorder. Output MP4 has all participants tiled per the current layout.
POST /v1/me/sessions/:id/recording/start
{ "layout": "composite" } // grid | speaker | sidebar | composite
→ { "id": "rec_xxx", "state": "recording", "layout": "composite", ... }
POST /v1/me/sessions/:id/recording/<rec>/stop
GET /v1/me/sessions/:id/recordings
When done, the recording's state moves to ready and playbackUrl is populated.
9. AI session summary
If transcriptionEnabled: true when you created the session, Kardolive runs the transcript
through Anthropic Claude after the session ends and produces a structured summary.
// when host clicks "End"
POST /v1/me/sessions/:id/end
→ { "state": "ended", "endedAt": "..." }
// summary worker fires async; takes 10–30 s
// later
GET /v1/me/sessions/:id/summary
→ {
"ready": true,
"generatedAt": "2026-05-12T10:11:12.000Z",
"summary": {
"tldr": "Team discussed Q4 roadmap and approved India expansion.",
"highlights": [
"Revenue up 30%",
"Two new hires for the India team",
"January launch target confirmed"
],
"actionItems": [
"Alice owns India hiring",
"Bob writes Q1 OKRs by Friday"
],
"decisions": [
"Approve India region budget of $200k"
]
}
}
You can also subscribe to session.summary.ready via webhook.
10. Full your app team-meeting example
your app user clicks "Start team meeting" on a deal page. We want chat + video + recording + auto-summary.
// ── your app backend: create the session ─────────────────────
@Post('deals/:dealId/start-meeting')
async startMeeting(@Req() req: any, @Param('dealId') dealId: string) {
const deal = await this.deals.findById(dealId);
const memberIds = [req.user.id, ...deal.participants];
const r = await fetch('https://kardocloud.com/v1/me/sessions', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.KARDOLIVE_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'conference',
name: `Deal: ${deal.name}`,
projectId: process.env.KARDOLIVE_PROJECT_ID,
hostId: req.user.id,
recordingEnabled: true,
transcriptionEnabled: true,
layout: 'grid',
inviteUserIds: memberIds,
}),
});
const session = await r.json();
// Save link to your app deal record
await this.deals.attachMeeting(dealId, session.id);
return { sessionId: session.id };
}
// ── your app client: open the conference UI ──────────────────
function DealMeetingButton({ dealId }: { dealId: string }) {
const [sessionId, setSessionId] = useState<string | null>(null);
const start = async () => {
const r = await fetch(`/api/deals/${dealId}/start-meeting`, { method: 'POST' });
setSessionId((await r.json()).sessionId);
};
return sessionId
? <ConferenceView sessionId={sessionId} onEnd={() => setSessionId(null)} />
: <button onClick={start}>Start team meeting</button>;
}
// ── ConferenceView uses useSession hook from React quickstart ─
function ConferenceView({ sessionId, onEnd }: { sessionId: string; onEnd: () => void }) {
const { localRef, remotes, state } = useSession(sessionId);
return (
<div className="grid">
<video ref={localRef} autoPlay muted playsInline />
{remotes.map((r) => (
<video key={r.peerId} autoPlay playsInline
ref={(el) => el && (el.srcObject = r.stream)} />
))}
<button onClick={() => { endSession(sessionId); onEnd(); }}>End</button>
</div>
);
}
// ── your app backend: subscribe to summary webhook ────────────
@Post('webhooks/kardolive')
async handleWebhook(@Body() event: any) {
if (event.event === 'session.summary.ready') {
await this.deals.attachSummary(
event.data.sessionId,
event.data.summary, // { tldr, highlights, actionItems, decisions }
);
}
}