Webhooks#
Webhooks allow your application server to be notified of events of Rooms, Egress and Ingress without the need of polling. OpenVidu POSTs a signed JSON event to the URLs you configure, as Rooms start and finish, participants come and go, tracks are published, and Egress or Ingress processes change state.
To turn webhooks on, see Enable OpenVidu webhooks. This page is the reference for what arrives once they are on.
OpenVidu is API-compatible with LiveKit, so all LiveKit webhook events are supported. Visit the LiveKit docs for a complete reference of webhook management:
Events#
| Event | Fires when | Payload that carries |
|---|---|---|
room_started |
A Room is created, either by the first participant joining or by your backend creating it explicitly | room |
room_finished |
A Room ends, either because your backend deleted it or because the departure timeout elapsed after the last participant left | room |
participant_joined |
A participant finishes connecting to a Room | room, participant |
participant_left |
A participant disconnects | room, participant |
participant_connection_aborted |
A participant's connection attempt did not complete | room, participant |
track_published |
A participant starts publishing a track | room, participant, track |
track_unpublished |
A participant stops publishing a track | room, participant, track |
egress_started |
An Egress process begins (a recording or stream export) | egressInfo |
egress_updated |
An Egress process changes state while running | egressInfo |
egress_ended |
An Egress process finishes, successfully or not | egressInfo |
ingress_started |
An Ingress process begins (media imported into a Room) | ingressInfo |
ingress_ended |
An Ingress process finishes | ingressInfo |
Payload#
The body is a JSON object with lowerCamelCase field names. A participant_joined event looks like this:
{
"event": "participant_joined",
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"createdAt": "1755648000",
"room": {
"sid": "RM_GmENxWJemFqL",
"name": "my-room",
"creationTime": "1755647990",
"numParticipants": 2
},
"participant": {
"sid": "PA_dRnCwpBTgKe8",
"identity": "my-participant",
"name": "My Participant",
"state": "ACTIVE"
}
}
Only the fields relevant to the event are populated:
| Field | Type | Notes |
|---|---|---|
event |
string | One of the event names above |
id |
string | Unique id of this event |
createdAt |
int64 | Unix timestamp, in seconds, of when the event was created |
room |
object | Room information. Visit the official LiveKit documentation (Room ) for details |
participant |
object | Participant information. Visit the official LiveKit documentation (ParticipantInfo ) for details |
track |
object | Track information. Visit the official LiveKit documentation (TrackInfo ) for details |
egressInfo |
object | Egress process information. Visit the official LiveKit documentation (EgressInfo ) for details |
ingressInfo |
object | Ingress process information. Visit the official LiveKit documentation (IngressInfo ) for details |
numDropped |
int64 | The number of events that were dropped before this one. This acts as your delivery health signal: a non-zero value indicates that your application server has missed some events |
Delivery#
| Request | Value |
|---|---|
| HTTP method | POST |
content-type |
application/webhook+json |
Authorization |
A JWT signed with your API secret. Use it to verify the event |
| Body | The payload of the event, in JSON format |
Note
- Events are always delivered in order: a newer event is sent only after the older ones have been delivered (or abandoned).
- There is no guarantee of delivery. OpenVidu retries failed deliveries with exponential backoff, but if your endpoint is down for a long time or consistently returns errors, events will be abandoned.
Receiving and validating webhook events#
Never act on an unverified webhook. Your endpoint is a public URL, so anyone can POST to it; the signature is what distinguishes a real event from a forgery.
Verifying means:
- Verify the JWT's signature with your
LIVEKIT_API_SECRET. - Hash the raw body with SHA-256, base64-encode it, and compare against the token's
sha256claim.
The LiveKit server SDKs do both for you:
- Using LiveKit Node SDK .
- For a working example run the Node.js tutorial.
import express from "express";
import { WebhookReceiver } from "livekit-server-sdk";
const webhookReceiver = new WebhookReceiver("api-key", "api-secret");
// The receiver needs the raw body, not parsed JSON
app.use(express.raw({ type: "application/webhook+json" }));
app.post("/livekit/webhook", async (req, res) => {
try {
const event = await webhookReceiver.receive(req.body, req.get("Authorization"));
// event is verified: safe to act on
} catch (error) {
console.error("Error validating webhook event", error);
}
res.status(200).send();
});
- Using LiveKit Go SDK .
- For a working example run the Go tutorial.
import (
"net/http"
"github.com/livekit/protocol/auth"
"github.com/livekit/protocol/webhook"
)
func receiveWebhook(w http.ResponseWriter, r *http.Request) {
authProvider := auth.NewSimpleKeyProvider("api-key", "api-secret")
event, err := webhook.ReceiveWebhookEvent(r, authProvider)
if err != nil {
http.Error(w, "Error validating webhook event", http.StatusUnauthorized)
return
}
// event is verified: safe to act on
w.WriteHeader(http.StatusOK)
}
- Using LiveKit Ruby SDK .
- For a working example run the Ruby tutorial.
The Ruby SDK has no WebhookReceiver class. Verify the Authorization header with LiveKit::TokenVerifier, compare the body hash against the token's sha256 claim, then parse the body yourself:
require 'livekit'
require 'json'
require 'digest'
post '/livekit/webhook' do
token_verifier = LiveKit::TokenVerifier.new(api_key: 'api-key', api_secret: 'api-secret')
begin
body = request.body.read
claims = token_verifier.verify(request.env['HTTP_AUTHORIZATION'])
halt 401, "Webhook body hash mismatch" if claims.sha256 != Digest::SHA256.base64digest(body)
event = JSON.parse(body)
# event is verified: safe to act on
rescue => e
halt 401, "Error validating webhook event: #{e}"
end
end
- Using LiveKit Kotlin SDK .
- For a working example run the Java tutorial.
import io.livekit.server.WebhookReceiver;
import livekit.LivekitWebhook.WebhookEvent;
WebhookReceiver webhookReceiver = new WebhookReceiver("api-key", "api-secret");
// body is the raw request body as a String
// authHeader is the value of the "Authorization" header
WebhookEvent event = webhookReceiver.receive(body, authHeader);
// event is verified: safe to act on (receive throws if it is not valid)
- Using LiveKit Python SDK .
- For a working example run the Python tutorial.
from livekit.api import TokenVerifier, WebhookReceiver
token_verifier = TokenVerifier("api-key", "api-secret")
webhook_receiver = WebhookReceiver(token_verifier)
@app.post("/livekit/webhook")
def receive_webhook():
auth_token = request.headers.get("Authorization")
try:
event = webhook_receiver.receive(request.data.decode("utf-8"), auth_token)
# event is verified: safe to act on
return "ok"
except Exception:
return "Error validating webhook event", 401
- Using LiveKit Rust SDK .
- For a working example run the Rust tutorial.
use livekit_api::access_token::TokenVerifier;
use livekit_api::webhooks::WebhookReceiver;
async fn receive_webhook(headers: HeaderMap, body: String) -> StatusCode {
let token_verifier = TokenVerifier::with_api_key("api-key", "api-secret");
let webhook_receiver = WebhookReceiver::new(token_verifier);
let auth_header = headers
.get("Authorization")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
match webhook_receiver.receive(&body, auth_header) {
Ok(event) => {
// event is verified: safe to act on
println!("LiveKit Webhook: {:?}", event);
StatusCode::OK
}
Err(_) => StatusCode::UNAUTHORIZED,
}
}
- Using LiveKit PHP SDK .
- For a working example run the PHP tutorial.
<?php
use Agence104\LiveKit\WebhookReceiver;
$webhookReceiver = new WebhookReceiver("api-key", "api-secret");
$body = file_get_contents("php://input");
$authHeader = getallheaders()["Authorization"];
try {
$event = $webhookReceiver->receive($body, $authHeader);
// event is verified: safe to act on
} catch (Exception $e) {
http_response_code(401);
}
- Using LiveKit .NET SDK .
- For a working example run the .NET tutorial.
using Livekit.Server.Sdk.Dotnet;
var webhookReceiver = new WebhookReceiver("api-key", "api-secret");
app.MapPost("/livekit/webhook", async (HttpRequest request) =>
{
string body = await new StreamReader(request.Body).ReadToEndAsync();
string authHeader = request.Headers["Authorization"].FirstOrDefault();
try
{
WebhookEvent webhookEvent = webhookReceiver.Receive(body, authHeader);
// event is verified: safe to act on
return Results.Ok();
}
catch (Exception)
{
return Results.Unauthorized();
}
});
Each application server tutorial ships a working, validated webhook endpoint in its language.
Developing against a remote deployment#
Your local machine is not reachable from your OpenVidu deployment, so webhooks sent to localhost never arrive. Expose your local server with a tunnel and configure that public URL instead — see Send webhooks to a local application server.
Related#
- Enable OpenVidu webhooks: configuring
webhook.urlsinlivekit.yaml. - Access tokens reference: the same key pair, used in the other direction.
- Egress reference and Ingress reference: the objects
egressInfoandingressInfocarry. - Recording tutorial: egress webhook events in a real application server.
- OpenVidu Meet sends its own, higher-level webhooks. If you are embedding Meet rather than building on Platform, see the OpenVidu Meet webhooks reference.