Customer Webhooks
Customer webhooks deliver RTC lifecycle events to a customer's HTTPS endpoint.
Supported events
Current event sources:
room.startedroom.endedparticipant.joinedparticipant.leftusage.finalizedcredit.lowcredit.exhaustedcredit.restoredpayment.succeededpayment.failedpayment.expiredpayment.canceledpayment.refunded
Create an endpoint
POST /v1/webhook-endpoints
Authorization: Bearer sk_live_xxx
Content-Type: application/json
{
"url": "https://customer.example.com/webhooks/rtc",
"events": [
"room.started",
"room.ended",
"participant.joined",
"participant.left",
"usage.finalized"
]
}
The create response contains a signing secret such as:
whsec_xxxxxxxxx
Store it securely. The API does not expose the secret again.
Delivery envelope
{
"id": "evt_xxx",
"type": "participant.joined",
"created_at": "2026-09-28T16:00:00Z",
"project_id": "prj_xxx",
"data": {
"room_id": "room_xxx",
"participant_id": "part_xxx"
}
}
Headers
Content-Type: application/json
User-Agent: Kallo-Webhooks/0.1
X-RTC-Webhook-Id: evt_xxx
X-RTC-Timestamp: 1790611200
X-RTC-Signature: v1=<hex-hmac>
The signed content is:
X-RTC-Timestamp + "." + raw_request_body
Algorithm:
HMAC-SHA256(webhook_secret, signed_content)
Use the exact raw request body bytes. Do not parse and re-serialize JSON before verification.
Node.js verification
import { verifyWebhookSignature } from "@kallolive/server";
const valid = await verifyWebhookSignature({
secret: process.env.KALLO_WEBHOOK_SECRET!,
timestamp: req.headers["x-rtc-timestamp"] as string,
signature: req.headers["x-rtc-signature"] as string,
body: rawBody,
});
if (!valid) {
throw new Error("invalid webhook signature");
}
The default replay tolerance is five minutes.
Go verification
err := kallo.VerifyWebhookSignature(
os.Getenv("KALLO_WEBHOOK_SECRET"),
r.Header.Get("X-RTC-Timestamp"),
r.Header.Get("X-RTC-Signature"),
rawBody,
5*time.Minute,
time.Now(),
)
if err != nil {
http.Error(w, "invalid webhook", http.StatusUnauthorized)
return
}
Retry policy
Failed deliveries are retried approximately at:
10 seconds
1 minute
5 minutes
30 minutes
2 hours
6 hours
24 hours
After the retry window is exhausted, the delivery moves to dead.
Customers must process webhook events idempotently using the event id.
Success response
Any HTTP status from 200 through 299 marks the delivery successful.
Redirects are not followed.
Security
Outbound webhook delivery:
- requires HTTPS
- rejects localhost/private/link-local destinations
- resolves DNS immediately before connecting
- rejects hostnames resolving to a private/local address
- does not use environment HTTP proxies
- limits response body capture
- uses a delivery timeout
- signs every request
These protections reduce SSRF risk from customer-configured callback URLs.
Reliability
Media lifecycle events are generated transactionally with state/usage changes where possible.
The reconciler can recover missing LiveKit join/leave lifecycle events and still generate:
- room start
- participant join
- participant leave
- finalized usage
Manual room termination also finalizes open usage sessions before producing room.ended.