Skip to content

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:

LiveKit docs

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:

  1. Verify the JWT's signature with your LIVEKIT_API_SECRET.
  2. Hash the raw body with SHA-256, base64-encode it, and compare against the token's sha256 claim.

The LiveKit server SDKs do both for you:

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();
});
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)
}

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
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)
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
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,
    }
}
<?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.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.