Webhooks
Mirror Kardolive state into your app, trigger pushes, audit, fire workflows.
On this page
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
| Event | Payload (data field) |
|---|---|
chat.message.created | full message: { id, channelId, authorId, authorName, body, parentId?, mentions[], attachments?, createdAt } |
chat.message.edited | same 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)
| Event | When |
|---|---|
session.created | New session row inserted |
session.started | state went waiting → live |
session.ended | state went to ended |
session.updated | layout / lock / waitingRoom changed |
session.participant.updated | role / perms changed |
session.participant.kicked | Participant kicked |
session.participant.promoted | Audience → guest / cohost |
session.handraise.raised | Audience raised hand |
session.handraise.lowered | Hand lowered (by host) |
session.spotlight | Spotlight pin changed |
session.muted-all | Host muted everyone |
session.recording.started | Composite recording began |
session.recording.stopped | Recording finalized |
session.breakouts.created | Breakouts spawned |
session.breakouts.closed | Breakouts ended |
session.summary.ready | AI summary written |
Calls
| Event | When |
|---|---|
call.invited | Caller initiated; callee gets push |
call.answered | Callee accepted |
call.declined | Callee declined before answer |
call.ended | Either party hung up after answer |
Streams (RTMP/HLS)
| Event | When |
|---|---|
stream.started | Publisher connected; HLS available |
stream.ended | Publisher disconnected; HLS sealed |
recording.ready | VOD 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
- Kardolive retries failed deliveries (non-2xx) up to 3 times with exponential backoff (1s, 2s, 4s).
- After all retries fail, the delivery is marked
failed. Visible in the dashboard under Webhooks → Deliveries. - You can manually retry any failed delivery from the dashboard, or via
POST /v1/me/webhooks/deliveries/<id>/retry. - Always return 2xx quickly. If you need to do heavy work, ACK immediately and process async.
5. Test endpoints with the Webhook Tester
The dashboard has a built-in tester at /app/webhook-tester. It:
- Gives you a unique
https://kardocloud.com/v1/webhooks/capture/<token>URL - Captures every request made to it with full headers + body
- 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.