Conference & interactive sessions

Multi-party WebRTC rooms — Zoom-style meetings, interactive livestreams, webinars, breakouts.

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", ... }
TypeUse caseDefaults
conferenceZoom-style meetinggrid layout, all-publish
webinarLarge audience, host-controlled stageviewer-by-default
livestreamInteractive show (Polyv/Vhall style)spotlight layout
call1: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

RoleCan publish?Can moderate?Notes
hostaudio + video + screen✅ everythingSession creator
cohostaudio + video + screen✅ mute / spotlight / kickCo-streaming, PK battles
guestaudio + videoDefault invited participant
viewer❌ read-onlyWebinar 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 }
    );
  }
}

Next

Calls (1:1) → · Webhooks → · API reference →