Webhooks

Mirror Kardolive state into your app, trigger pushes, audit, fire workflows.

1. Register an endpoint

POST https://kardocloud.com/v1/me/webhooks
Authorization: Bearer kl_live_xxx
{
  "url": "https://your-app.example.com/api/webhooks/kardolive",
  "events": [
    "chat.message.created",
    "chat.message.edited",
    "chat.message.deleted",
    "channel.member.joined",
    "session.started",
    "session.ended",
    "session.summary.ready",
    "call.invited",
    "call.answered",
    "call.declined",
    "call.ended",
    "stream.started",
    "stream.ended",
    "recording.ready"
  ]
}
→ { "id": "wh_xxx", "url": "...", "secret": "<HMAC secret — save this>", "events": [...] }

The secret is returned once. Save it; you'll use it to verify signatures on incoming webhooks.

2. Verify the HMAC signature

Every webhook POST has these headers:

X-Kardolive-Signature: sha256=<hex digest>
X-Kardolive-Event: chat.message.created
X-Kardolive-Delivery: <unique delivery id>
Content-Type: application/json

The signature is HMAC-SHA256 of the raw request body using your endpoint secret.

NestJS verifier

// apps/backend/src/webhooks/kardolive.controller.ts
import { Controller, Post, Body, Headers, RawBody, BadRequestException, Req } from '@nestjs/common';
import { createHmac, timingSafeEqual } from 'crypto';
import { ConfigService } from '@nestjs/config';

@Controller('webhooks/kardolive')
export class KardoliveWebhookController {
  constructor(private readonly config: ConfigService) {}

  @Post()
  async handle(
    @RawBody() raw: Buffer,
    @Headers('x-kardolive-signature') sig: string,
    @Headers('x-kardolive-event') eventName: string,
    @Req() req: any,
  ) {
    const expected = 'sha256=' + createHmac('sha256', this.config.get('KARDOLIVE_WEBHOOK_SECRET')!)
      .update(raw).digest('hex');
    if (!sig || !timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
      throw new BadRequestException('bad signature');
    }
    const event = JSON.parse(raw.toString('utf8'));
    // dispatch on eventName
    switch (eventName) {
      case 'chat.message.created':
        await this.notifyContactOnNewMessage(event.data);
        break;
      case 'session.summary.ready':
        await this.attachSummaryToDeal(event.data);
        break;
      // ...
    }
    return { ok: true };
  }
}

Important: NestJS must be configured with rawBody: true on bootstrap for the raw buffer to reach your handler. Add to main.ts:

const app = await NestFactory.create(AppModule, { rawBody: true });

Plain Node (Express)

import express from 'express';
import { createHmac, timingSafeEqual } from 'crypto';

const app = express();
app.post('/webhooks/kardolive', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-kardolive-signature'];
  const expected = 'sha256=' + createHmac('sha256', process.env.KARDOLIVE_WEBHOOK_SECRET)
    .update(req.body).digest('hex');
  if (!sig || !timingSafeEqual(Buffer.from(expected), Buffer.from(String(sig)))) {
    return res.status(400).send('bad signature');
  }
  const event = JSON.parse(req.body.toString('utf8'));
  console.log(req.headers['x-kardolive-event'], event);
  // ...process...
  res.json({ ok: true });
});

3. Event catalog

Chat

EventPayload (data field)
chat.message.createdfull message: { id, channelId, authorId, authorName, body, parentId?, mentions[], attachments?, createdAt }
chat.message.editedsame shape, plus editedAt
chat.message.deleted{ id, channelId, redacted: true, redactedBy }
channel.member.joined{ channelId, userId, role }
channel.member.left{ channelId, userId }
chat.moderation.action{ channelId, kind: 'timeout'|'ban'|'mute'|'slowmode', target?, by, reason? }

Sessions (conferences, livestreams, webinars)

EventWhen
session.createdNew session row inserted
session.startedstate went waiting → live
session.endedstate went to ended
session.updatedlayout / lock / waitingRoom changed
session.participant.updatedrole / perms changed
session.participant.kickedParticipant kicked
session.participant.promotedAudience → guest / cohost
session.handraise.raisedAudience raised hand
session.handraise.loweredHand lowered (by host)
session.spotlightSpotlight pin changed
session.muted-allHost muted everyone
session.recording.startedComposite recording began
session.recording.stoppedRecording finalized
session.breakouts.createdBreakouts spawned
session.breakouts.closedBreakouts ended
session.summary.readyAI summary written

Calls

EventWhen
call.invitedCaller initiated; callee gets push
call.answeredCallee accepted
call.declinedCallee declined before answer
call.endedEither party hung up after answer

Streams (RTMP/HLS)

EventWhen
stream.startedPublisher connected; HLS available
stream.endedPublisher disconnected; HLS sealed
recording.readyVOD file uploaded; playback URL ready

Envelope shape

{
  "event": "chat.message.created",
  "tenantId": "tnt_xxx",
  "occurredAt": "2026-05-12T10:11:12.345Z",
  "data": { /* event-specific payload */ }
}

4. Retry behavior

5. Test endpoints with the Webhook Tester

The dashboard has a built-in tester at /app/webhook-tester. It:

  1. Gives you a unique https://kardocloud.com/v1/webhooks/capture/<token> URL
  2. Captures every request made to it with full headers + body
  3. Lets you replay captures back to your real endpoint to test your handler

Use this during your app integration development before pointing webhooks at your production server.

Next

API reference →