Skip to content

Webhooks#

A webhook is an endpoint URL registered in OpenVidu Meet. You can register several, and each one has its own:

  • Events filter: every event, or only the selected event types.
  • Room scope: events of every room, or only of one room.
  • Status: active, or paused (the endpoint is kept but receives nothing).

Every active webhook whose filters match an event receives it independently, with the same signature and retry policy.

Reference#

Visit OpenVidu Meet Webhooks reference documentation for a complete list of all available webhook events. They include:

Configuration#

Webhooks are managed from the "Embedded" page of the OpenVidu Meet app, where admin users can add, edit, pause, test and delete them. Testing sends a fake testEvent to the endpoint and reports whether it answered successfully.

Webhook list in the Embedded page of the OpenVidu Meet console Webhook list in the Embedded page of the OpenVidu Meet console

Info

Webhook events are signed with the API key, so deliveries fail until an API key has been generated.

REST API reference#

Webhooks can also be managed programmatically through the REST API, authenticating with the API key or as an admin user. See the REST API specification for the request bodies and response schemas.

Operation HTTP Method Reference
Create a webhook POST Reference
List webhooks GET Reference
Get a webhook GET Reference
Update a webhook PUT Reference
Delete a webhook DELETE Reference
Test a webhook POST Reference

Validate events#

OpenVidu Meet signs all webhook events with your API key, so you can verify their authenticity. This way you can ensure that the events received by your application's backend are coming from your actual OpenVidu Meet deployment and have not been tampered with.

Each webhook event includes two headers that you should use to validate the request:

  • x-signature: HMAC SHA256 signature of the request body, created by OpenVidu Meet using your API key.
  • x-timestamp: Unix timestamp (in milliseconds) when the webhook was sent.

The steps to validate a webhook event in your backend are the following, given that you have access to the HTTP request body and headers:

  1. Get the x-signature and x-timestamp headers from the request.
  2. Compare the x-timestamp header value with the current Unix timestamp. If the difference is greater than a predefined threshold (e.g., 2 minutes), reject it to prevent replay attacks .
  3. Concatenate in a single string the x-timestamp header value + character . + the request body exactly as received, before parsing it. Signing a re-serialized copy of the parsed body is not guaranteed to reproduce the bytes that were signed, and fails outright in languages whose JSON encoder escapes non-ASCII characters.
  4. Create a HMAC SHA256 hash of the string of point 3) using your OpenVidu Meet API key as the key.
  5. Compare the computed hash of point 4) with the x-signature header value. Do a time safe comparison to avoid timing attacks . If they match, the request is valid.

Below there are code snippets in different languages, showing the exact implementation of the above steps.

Checkout working example

import crypto from "crypto";

const OPENVIDU_MEET_API_KEY = "YOUR_API_KEY";
const MAX_WEBHOOK_AGE = 120 * 1000; // 2 minutes in milliseconds

// `rawBody` is the request body as received. With Express, keep it with
// express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString("utf8"); } })
function isWebhookEventValid(rawBody, headers) {
    const signature = headers["x-signature"]; // (1)!
    const timestamp = parseInt(headers["x-timestamp"], 10);

    if (!signature || !timestamp || isNaN(timestamp)) {
        return false;
    }

    const current = Date.now();
    const diffTime = current - timestamp;
    if (diffTime >= MAX_WEBHOOK_AGE) { // (2)!
        // Webhook event too old
        return false;
    }

    const signedPayload = `${timestamp}.${rawBody}`; // (3)!
    const expectedSignature = crypto // (4)!
        .createHmac("sha256", OPENVIDU_MEET_API_KEY)
        .update(signedPayload, "utf8")
        .digest("hex");

    return crypto.timingSafeEqual( // (5)!
        Buffer.from(expectedSignature, "hex"),
        Buffer.from(signature, "hex")
    );
}
  1. 1) Get the x-signature and x-timestamp headers from the request.
  2. 2) Compare the x-timestamp header value with the current Unix timestamp. If the difference is greater than a predefined threshold (e.g., 2 minutes), reject it to prevent replay attacks .
  3. 3) Concatenate in a single string the x-timestamp header value + character . + the request body exactly as received, before parsing it. Signing a re-serialized copy of the parsed body is not guaranteed to reproduce the bytes that were signed, and fails outright in languages whose JSON encoder escapes non-ASCII characters.
  4. 4) Create a HMAC SHA256 hash of the string of point 3) using your OpenVidu Meet API key as the key.
  5. 5) Compare the computed hash of point 4) with the x-signature header value. Do a time safe comparisson to avoid timing attacks . If they match, the request is valid.

Checkout working example

package com.example;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Map;

public class WebhookValidator {
    private static final long MAX_WEBHOOK_AGE = 120 * 1000; // 2 minutes in milliseconds
    private static final String OPENVIDU_MEET_API_KEY = "YOUR_API_KEY";

    // Declare the controller parameter as `@RequestBody String rawBody` so Spring hands over
    // the body as received instead of a parsed object.
    public static boolean isWebhookEventValid(String rawBody, Map<String, String> headers) {
        String signature = headers.get("x-signature"); // (1)!
        String ts = headers.get("x-timestamp");
        if (signature == null || ts == null) return false;

        long timestamp;
        try {
            timestamp = Long.parseLong(ts);
        } catch (NumberFormatException e) {
            return false;
        }

        long current = System.currentTimeMillis();
        long diffTime = current - timestamp;
        if (diffTime >= MAX_WEBHOOK_AGE) { // (2)!
            // Webhook event too old
            return false;
        }

        String signedPayload = timestamp + "." + rawBody; // (3)!

        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(
                new SecretKeySpec(
                    OPENVIDU_MEET_API_KEY.getBytes(StandardCharsets.UTF_8), // (4)!
                    "HmacSHA256"
                )
            );
            byte[] expected = mac.doFinal(signedPayload.getBytes(StandardCharsets.UTF_8));
            byte[] actual = hexToBytes(signature);

            return timingSafeEqual(expected, actual); // (5)!
        } catch (Exception e) {
            return false;
        }
    }

    // Helper method to convert hex string to byte array
    private static byte[] hexToBytes(String hex) {
        int len = hex.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
                                + Character.digit(hex.charAt(i + 1), 16));
        }
        return data;
    }

    // Time safe comparison to prevent timing attacks
    private static boolean timingSafeEqual(byte[] a, byte[] b) {
        if (a.length != b.length) return false;
        int result = 0;
        for (int i = 0; i < a.length; i++) {
            result |= a[i] ^ b[i];
        }
        return result == 0;
    }
}
  1. 1) Get the x-signature and x-timestamp headers from the request.
  2. 2) Compare the x-timestamp header value with the current Unix timestamp. If the difference is greater than a predefined threshold (e.g., 2 minutes), reject it to prevent replay attacks .
  3. 3) Concatenate in a single string the x-timestamp header value + character . + the request body exactly as received, before parsing it. Signing a re-serialized copy of the parsed body is not guaranteed to reproduce the bytes that were signed, and fails outright in languages whose JSON encoder escapes non-ASCII characters.
  4. 4) Create a HMAC SHA256 hash of the string of point 3) using your OpenVidu Meet API key as the key.
  5. 5) Compare the computed hash of point 4) with the x-signature header value. Do a time safe comparisson to avoid timing attacks . If they match, the request is valid.

Checkout working example

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "crypto/subtle"
    "encoding/hex"
    "encoding/json"
    "net/http"
    "strconv"
    "time"
)

const (
    maxWebhookAge      = 120 * 1000 // 2 minutes in milliseconds
    openviduMeetApiKey = "YOUR_API_KEY"
)

func isWebhookEventValid(bodyBytes []byte, headers http.Header) bool {
    signature := headers.Get("x-signature") // (1)!
    tsStr := headers.Get("x-timestamp")
    if signature == "" || tsStr == "" {
        return false
    }

    timestamp, err := strconv.ParseInt(tsStr, 10, 64)
    if err != nil {
        return false
    }

    current := time.Now().UnixMilli()
    diffTime := current - timestamp
    if diffTime >= maxWebhookAge { // (2)!
        // Webhook event too old
        return false
    }

    signedPayload := tsStr + "." + string(bodyBytes) // (3)!

    mac := hmac.New(sha256.New, []byte(openviduMeetApiKey)) // (4)!
    mac.Write([]byte(signedPayload))
    expected := mac.Sum(nil)

    actual, err := hex.DecodeString(signature)
    if err != nil {
        return false
    }

    return subtle.ConstantTimeCompare(expected, actual) == 1 // (5)!
}
  1. 1) Get the x-signature and x-timestamp headers from the request.
  2. 2) Compare the x-timestamp header value with the current Unix timestamp. If the difference is greater than a predefined threshold (e.g., 2 minutes), reject it to prevent replay attacks .
  3. 3) Concatenate in a single string the x-timestamp header value + character . + the request body exactly as received, before parsing it. Signing a re-serialized copy of the parsed body is not guaranteed to reproduce the bytes that were signed, and fails outright in languages whose JSON encoder escapes non-ASCII characters.
  4. 4) Create a HMAC SHA256 hash of the string of point 3) using your OpenVidu Meet API key as the key.
  5. 5) Compare the computed hash of point 4) with the x-signature header value. Do a time safe comparisson to avoid timing attacks . If they match, the request is valid.

Checkout working example

import hmac
import hashlib
import time

MAX_WEBHOOK_AGE = 120 * 1000  # 2 minutes in milliseconds
OPENVIDU_MEET_API_KEY = "YOUR_API_KEY"

# `raw_body` is the request body as received: with Flask, `request.get_data(as_text=True)`.
def is_webhook_event_valid(raw_body, headers):
    signature = headers.get("x-signature")  # (1)!
    timestamp_str = headers.get("x-timestamp")
    if not signature or not timestamp_str:
        return False

    try:
        timestamp = int(timestamp_str)
    except ValueError:
        return False

    current = int(time.time() * 1000)
    diff_time = current - timestamp
    if diff_time >= MAX_WEBHOOK_AGE:  # (2)!
        return False

    signed_payload = str(timestamp) + "." + raw_body  # (3)!

    expected = hmac.new(  # (4)!
        OPENVIDU_MEET_API_KEY.encode('utf-8'),
        signed_payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, signature)  # (5)!
  1. 1) Get the x-signature and x-timestamp headers from the request.
  2. 2) Compare the x-timestamp header value with the current Unix timestamp. If the difference is greater than a predefined threshold (e.g., 2 minutes), reject it to prevent replay attacks .
  3. 3) Concatenate in a single string the x-timestamp header value + character . + the request body exactly as received, before parsing it. Signing a re-serialized copy of the parsed body is not guaranteed to reproduce the bytes that were signed, and fails outright in languages whose JSON encoder escapes non-ASCII characters.
  4. 4) Create a HMAC SHA256 hash of the string of point 3) using your OpenVidu Meet API key as the key.
  5. 5) Compare the computed hash of point 4) with the x-signature header value. Do a time safe comparisson to avoid timing attacks . If they match, the request is valid.

Checkout working example

<?php

const MAX_WEBHOOK_AGE = 120 * 1000; // 2 minutes in milliseconds
const OPENVIDU_MEET_API_KEY = "YOUR_API_KEY";

// `$rawBody` is the request body as received: `file_get_contents('php://input')`.
function isWebhookEventValid($rawBody, $headers)
{
    $signature = $headers['x-signature'] ?? null; // (1)!
    $timestampStr = $headers['x-timestamp'] ?? null;
    if (!$signature || !$timestampStr) {
        return false;
    }

    $timestamp = filter_var($timestampStr, FILTER_VALIDATE_INT);
    if ($timestamp === false) {
        return false;
    }

    $current = intval(microtime(true) * 1000);
    $diffTime = $current - $timestamp;
    if ($diffTime >= MAX_WEBHOOK_AGE) { // (2)!
        return false;
    }

    $signedPayload = $timestamp . '.' . $rawBody; // (3)!

    $expected = hash_hmac('sha256', $signedPayload, OPENVIDU_MEET_API_KEY); // (4)!

    return hash_equals($expected, $signature); // (5)!
}

?>
  1. 1) Get the x-signature and x-timestamp headers from the request.
  2. 2) Compare the x-timestamp header value with the current Unix timestamp. If the difference is greater than a predefined threshold (e.g., 2 minutes), reject it to prevent replay attacks .
  3. 3) Concatenate in a single string the x-timestamp header value + character . + the request body exactly as received, before parsing it. Signing a re-serialized copy of the parsed body is not guaranteed to reproduce the bytes that were signed, and fails outright in languages whose JSON encoder escapes non-ASCII characters.
  4. 4) Create a HMAC SHA256 hash of the string of point 3) using your OpenVidu Meet API key as the key.
  5. 5) Compare the computed hash of point 4) with the x-signature header value. Do a time safe comparisson to avoid timing attacks . If they match, the request is valid.

Checkout working example

using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

public class WebhookValidator
{
    private const long MAX_WEBHOOK_AGE = 120 * 1000; // 2 minutes in milliseconds
    private const string OPENVIDU_MEET_API_KEY = "YOUR_API_KEY";

    public static bool IsWebhookEventValid(string body, Dictionary<string, string> headers)
    {
        if (!headers.TryGetValue("x-signature", out var signature) || // (1)!
            !headers.TryGetValue("x-timestamp", out var timestampStr))
        {
            return false;
        }

        if (!long.TryParse(timestampStr, out long timestamp))
        {
            return false;
        }

        long current = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
        long diffTime = current - timestamp;
        if (diffTime >= MAX_WEBHOOK_AGE) // (2)!
        {
            return false;
        }

        string signedPayload = $"{timestamp}.{body}"; // (3)!

        using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(OPENVIDU_MEET_API_KEY))) // (4)!
        {
            byte[] expected = hmac.ComputeHash(Encoding.UTF8.GetBytes(signedPayload));
            byte[] actual = Convert.FromHexString(signature);

            return CryptographicOperations.FixedTimeEquals(expected, actual); // (5)!
        }
    }
}
  1. 1) Get the x-signature and x-timestamp headers from the request.
  2. 2) Compare the x-timestamp header value with the current Unix timestamp. If the difference is greater than a predefined threshold (e.g., 2 minutes), reject it to prevent replay attacks .
  3. 3) Concatenate in a single string the x-timestamp header value + character . + the request body exactly as received, before parsing it. Signing a re-serialized copy of the parsed body is not guaranteed to reproduce the bytes that were signed, and fails outright in languages whose JSON encoder escapes non-ASCII characters.
  4. 4) Create a HMAC SHA256 hash of the string of point 3) using your OpenVidu Meet API key as the key.
  5. 5) Compare the computed hash of point 4) with the x-signature header value. Do a time safe comparisson to avoid timing attacks . If they match, the request is valid.

Checkout working example

require 'openssl'
require 'json'

MAX_WEBHOOK_AGE = 120 * 1000 # 2 minutes in milliseconds
OPENVIDU_MEET_API_KEY = "YOUR_API_KEY"

# `raw_body` is the request body as received: with Sinatra, `request.body.read`.
def webhook_event_valid?(raw_body, headers)
    signature = headers['x-signature'] # (1)!
    timestamp_str = headers['x-timestamp']
    return false if signature.nil? || timestamp_str.nil?

    begin
        timestamp = Integer(timestamp_str)
    rescue ArgumentError
        return false
    end

    current = (Time.now.to_f * 1000).to_i
    diff_time = current - timestamp
    return false if diff_time >= MAX_WEBHOOK_AGE # (2)!

    signed_payload = "#{timestamp}.#{raw_body}" # (3)!

    expected = OpenSSL::HMAC.hexdigest('SHA256', OPENVIDU_MEET_API_KEY, signed_payload) # (4)!

    OpenSSL.fixed_length_secure_compare(expected, signature) # (5)!
end
  1. 1) Get the x-signature and x-timestamp headers from the request.
  2. 2) Compare the x-timestamp header value with the current Unix timestamp. If the difference is greater than a predefined threshold (e.g., 2 minutes), reject it to prevent replay attacks .
  3. 3) Concatenate in a single string the x-timestamp header value + character . + the request body exactly as received, before parsing it. Signing a re-serialized copy of the parsed body is not guaranteed to reproduce the bytes that were signed, and fails outright in languages whose JSON encoder escapes non-ASCII characters.
  4. 4) Create a HMAC SHA256 hash of the string of point 3) using your OpenVidu Meet API key as the key.
  5. 5) Compare the computed hash of point 4) with the x-signature header value. Do a time safe comparisson to avoid timing attacks . If they match, the request is valid.

Checkout working example

use chrono::Utc;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::collections::HashMap;

type HmacSha256 = Hmac<Sha256>;

fn is_webhook_event_valid(body_str: &str, headers: &HashMap<String, String>) -> bool {
    let signature = match headers.get("x-signature") { // (1)!
        Some(sig) => sig,
        None => return false,
    };

    let timestamp_str = match headers.get("x-timestamp") {
        Some(ts) => ts,
        None => return false,
    };

    let timestamp: i64 = match timestamp_str.parse() {
        Ok(ts) => ts,
        Err(_) => return false,
    };

    // Check timestamp age
    let current = Utc::now().timestamp_millis();
    let diff_time = current - timestamp;
    if diff_time >= MAX_WEBHOOK_AGE { // (2)!
        return false;
    }

    // Create signed payload using the raw body string
    let signed_payload = format!("{}.{}", timestamp, body_str); // (3)!

    // Calculate HMAC
    let mut mac = match HmacSha256::new_from_slice(OPENVIDU_MEET_API_KEY.as_bytes()) { // (4)!
        Ok(mac) => mac,
        Err(_) => return false,
    };

    mac.update(signed_payload.as_bytes());
    let expected = mac.finalize().into_bytes();
    let expected_hex = hex::encode(expected);

    // Timing-safe comparison
    if signature.len() != expected_hex.len() {
        return false;
    }

    let mut result = 0u8;
    for (a, b) in signature.bytes().zip(expected_hex.bytes()) { // (5)!
        result |= a ^ b;
    }
    result == 0
}
  1. 1) Get the x-signature and x-timestamp headers from the request.
  2. 2) Compare the x-timestamp header value with the current Unix timestamp. If the difference is greater than a predefined threshold (e.g., 2 minutes), reject it to prevent replay attacks .
  3. 3) Concatenate in a single string the x-timestamp header value + character . + the request body exactly as received, before parsing it. Signing a re-serialized copy of the parsed body is not guaranteed to reproduce the bytes that were signed, and fails outright in languages whose JSON encoder escapes non-ASCII characters.
  4. 4) Create a HMAC SHA256 hash of the string of point 3) using your OpenVidu Meet API key as the key.
  5. 5) Compare the computed hash of point 4) with the x-signature header value. Do a time safe comparisson to avoid timing attacks . If they match, the request is valid.

Failures and retries#

OpenVidu Meet will automatically retry sending webhooks in case of failures. For example, if your server is down or returns an error response.

It will retry 5 times, with an exponential backoff (meaning it will wait longer between each retry).

Info

Your server must respond with a 2xx HTTP status code to acknowledge that you have received the webhook event. The timeout granted by OpenVidu Meet to do so is 5 seconds. If your server takes longer than that to respond, or if it sends any status code other than 2xx, OpenVidu Meet will consider it a failure and trigger a retry.