developer guide

Whereby Embedded is an easy-to-use video meetings API. Embed video meetings into an application or website with the REST API ↗ allowing your team to build faster and ship more often.

Get started

Anchor link for how to start a meeting

To use the Embedded API you need to be on our Embedded product ↗, and you’ll need an API key. A new key is generated from the “Embedded” section in the Embedded dashboard. API keys can be renamed and deleted.

Create a meeting by sending a HTTP request to Whereby’s servers from your server. A successful response contains a roomUrl. Your API key is secret and should only be used from your server. Create a meeting.

Embed a meeting in your website or app with an iframe. The iframe’s src attribute is specified as the roomUrl. You can customize the meeting with URL parameters.

Whereby REST API

Anchor link for Embed a meeting

Create a meeting with a HTTP request containing your API key sent from your server to Whereby’s. The response contains a roomUrl that is embedded in your client within an iframe.

Calling Whereby’s API from your client should be done through an endpoint on your server. This will help keep the API key safe from exposing it to users. For this reason, the API does not return an Access-Control-Allow-Origin header in its response.

Create a meeting

Anchor link for how to create a meeting

To create a meeting, send an HTTP request with the necessary parameters in the body. Available parameters and formats can be found in the API docs ↗. endDate is interpreted as UTC by default, but other time zones are supported by including an offset in hours and minutes. For example, Eastern Standard Time (EST) would be expressed as 2021-08-11T07:56:01-05:00.

Meetings are fully functional from the time they are created.

curl https://api.whereby.dev/v1/meetings \
  --header "Authorization: Bearer $API_KEY" \
  --header "Content-Type: application/json" \
  --request POST \
  --data @- << EOF
  "endDate": "2022-01-27T17:36:00.000Z",
  "fields": ["hostRoomUrl"]
$api_key = "YOUR_API_KEY";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.whereby.dev/v1/meetings');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, '{
  "endDate": "2022-01-27T17:36:00.000Z",
  "fields": ["hostRoomUrl"]}'

$headers = [
  'Authorization: Bearer ' . $api_key,
  'Content-Type: application/json'

curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);


echo "Status code: $httpcode\n";
$data = json_decode($response);
echo "Room URL: ", $data->{'roomUrl'}, "\n";
echo "Host room URL: ", $data->{'hostRoomUrl'}, "\n";
const fetch = require("node-fetch");


const data = {
  endDate: "2022-01-27T17:36:00.000Z",
  fields: ["hostRoomUrl"],

function getResponse() {
    return fetch("https://api.whereby.dev/v1/meetings", {
        method: "POST",
        headers: {
            Authorization: `Bearer ${API_KEY}`,
            "Content-Type": "application/json",
        body: JSON.stringify(data),

getResponse().then(async res => {
    console.log("Status code:", res.status);
    const data = await res.json();
    console.log("Room URL:", data.roomUrl);
    console.log("Host room URL:", data.hostRoomUrl);
import requests
import json


data = {
    "endDate": "2022-01-27T17:36:00.000Z",
    "fields": ["hostRoomUrl"],

headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",

response = requests.post(

print("Status code:", response.status_code)
data = json.loads(response.text)
print("Room URL:", data["roomUrl"])
print("Host room URL:", data["hostRoomUrl"])
import com.fasterxml.jackson.databind.ObjectMapper;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.Collections;
import java.util.Map;

var apiKey = "YOUR_API_KEY";
var data = Map.of(
        "endDate", "2022-01-27T17:36:00.000Z",
        "fields", Collections.singletonList("hostRoomUrl")
var request = HttpRequest.newBuilder(
        .header("Authorization", "Bearer " + apiKey)
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(new ObjectMapper().writeValueAsString(data)))

var response = HttpClient.newHttpClient().send(request, BodyHandlers.ofString());
System.out.println("Status code: " + response.statusCode());
System.out.println("Body: " + response.body());

    "meetingId": "1",
    "startDate": "2022-01-26T17:37:00.000Z",
    "endDate": "2022-01-27T17:36:00.000Z",
    "roomUrl": "https://example.whereby.com/room",
    "hostRoomUrl": "https://example.whereby.com/room?roomKey=eFhcG...i00ZG"

401 Response: API key missing or invalid

Embed a meeting

Anchor link for how to embed a meeting

Embedding a meeting into a service or app requires using an iframe with the src attribute specified as the roomUrl. Read the Allowed domains section to learn how to allow your website’s domain so that browsers don’t block the iframe.

  allow="camera; microphone; fullscreen; speaker; display-capture"

Embedding with our web component

Anchor link for embedding with our web component

We also offer a web component that will allow you to embed a Whereby room in any webpage. It provides a more readable integration, and we’ve also exposed local client events that are sent from the room to the component.

Embedding in Android

Anchor link for embedding in android

In order to embed a room in Android, add these permissions to the manifest:

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

You will need to use the WebView class. Start by creating a custom WebChromeClient class and override the onPermissionRequest method:

import android.annotation.TargetApi;
import android.app.Activity;
import android.os.Build;
import android.webkit.PermissionRequest;
import android.webkit.WebChromeClient;

public class CustomWebChromeClient extends WebChromeClient {
    private Activity activity;

    public CustomWebChromeClient(Activity parentActivity) {
        activity = parentActivity;

    public void onPermissionRequest(final PermissionRequest request) {
        activity.runOnUiThread(new Runnable() {
            public void run() {
import android.app.Activity
import android.webkit.PermissionRequest
import android.webkit.WebChromeClient

class CustomWebChromeClient(private val activity: Activity) : WebChromeClient() {
    override fun onPermissionRequest(request: PermissionRequest) {
        activity.runOnUiThread { request.grant(request.resources) }

Then, create a WebViewUtils helper class to configure the WebView. Here is a possible configuration:

import android.annotation.SuppressLint;
import android.webkit.CookieManager;
import android.webkit.WebSettings;
import android.webkit.WebView;

public class WebViewUtils {

    public static void configureWebView(WebView webView) {
        WebSettings webSettings = webView.getSettings();
        CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true);
import android.annotation.SuppressLint
import android.webkit.CookieManager
import android.webkit.WebView

object WebViewUtils {
    fun configureWebView(webView: WebView) {
        val webSettings = webView.settings
        webSettings.javaScriptEnabled = true
        webSettings.domStorageEnabled = true
        webSettings.databaseEnabled = true
        webSettings.mediaPlaybackRequiresUserGesture = false
        CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true)

Finally, set up your activity following these steps:

  1. Configure the webView.
  2. Request the permissions if needed.
  3. Add the ?skipMediaPermissionPrompt parameter to the room URL and load it.

Here is an example:

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    public String roomUrlString = ""; // Replace by your own
    private String roomParameters = "?skipMediaPermissionPrompt";

    private static final int PERMISSION_REQUEST_CODE = 1234;
    private String[] requiredDangerousPermissions = {

    private WebView webView;

    protected void onCreate(Bundle savedInstanceState) {
        this.webView = findViewById(R.id.webView);
        this.webView.setWebChromeClient(new CustomWebChromeClient(this));
        this.webView.setWebViewClient(new WebViewClient());

    protected void onResume() {
        if (this.webView.getUrl() == null) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && this.isPendingPermissions()) {
                // This explicitly requests the camera and audio permissions.
                // It's fine for a demo app but should probably be called earlier in the flow,
                // on a user interaction instead of onResume.
            } else {

    private void loadEmbeddedRoomUrl() {
        this.webView.loadUrl(roomUrlString + roomParameters);

    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode) {
                if (this.grantResultsContainsDenials(grantResults)) {
                    // Show some permissions required dialog.
                } else {
                    // All necessary permissions granted, continue loading.
                super.onRequestPermissionsResult(requestCode, permissions, grantResults);

    @RequiresApi(api = Build.VERSION_CODES.M)
    private void requestCameraAndAudioPermissions() {
        this.requestPermissions(this.getPendingPermissions(), PERMISSION_REQUEST_CODE);

    @RequiresApi(api = Build.VERSION_CODES.M)
    private String[] getPendingPermissions() {
        List<String> pendingPermissions = new ArrayList<>();
        for (String permission : this.requiredDangerousPermissions) {
            if (this.checkSelfPermission(permission) == PackageManager.PERMISSION_DENIED) {
        return pendingPermissions.toArray(new String[pendingPermissions.size()]);

    private boolean isPendingPermissions() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            return false;
        return this.getPendingPermissions().length > 0;

    private boolean grantResultsContainsDenials(int[] grantResults) {
        for (int result : grantResults) {
            if (result == PackageManager.PERMISSION_DENIED) {
                return true;
        return false;
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    var roomUrlString = "" // Replace by your own
    private val roomParameters = "?skipMediaPermissionPrompt"

    companion object {
        private const val PERMISSION_REQUEST_CODE = 1234

    private val requiredDangerousPermissions = arrayOf(

    private var webView: WebView? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        webView = findViewById(R.id.webView)

    override fun onResume() {
        if (webView!!.url == null) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && isPendingPermissions()) {
                // This explicitly requests the camera and audio permissions.
                // It's fine for a demo app but should probably be called earlier in the flow,
                // on a user interaction instead of onResume.
            } else {

    private fun loadEmbeddedRoomUrl() {
        webView!!.loadUrl(roomUrlString + roomParameters)

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
    ) {
        when (requestCode) {
            PERMISSION_REQUEST_CODE -> if (grantResultsContainsDenials(grantResults)) {
                // Show some permissions required dialog.
            } else {
                // All necessary permissions granted, continue loading.
            else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)

    @RequiresApi(api = Build.VERSION_CODES.M)
    private fun requestCameraAndAudioPermissions() {
        requestPermissions(pendingPermissions, PERMISSION_REQUEST_CODE)

    @get:RequiresApi(api = Build.VERSION_CODES.M)
    private val pendingPermissions: Array<String>
        private get() {
            val pendingPermissions: MutableList<String> = ArrayList()
            for (permission in requiredDangerousPermissions) {
                if (checkSelfPermission(permission) == PackageManager.PERMISSION_DENIED) {
            return pendingPermissions.toTypedArray()

    private fun isPendingPermissions(): Boolean {
        return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
        } else pendingPermissions.isNotEmpty()

    private fun grantResultsContainsDenials(grantResults: IntArray): Boolean {
        for (result in grantResults) {
            if (result == PackageManager.PERMISSION_DENIED) {
                return true
        return false


Embedding in iOS

Anchor link for embedding in iOS

These are the recommended approaches to embed a meeting depending on the iOS version:

  • WKWebView supports embedding pages that use WebRTC from iOS 14.5 onwards. To access the microphone and camera, it is necessary to add both NSMicrophoneUsageDescription and NSCameraUsageDescription keys to the app’s Info.plist file.

    import WebKit
    class WKWebViewController: UIViewController, WKNavigationDelegate {
        public var roomUrlString = "" // Replace by your own
        private var webView: WKWebView!
        override func viewDidLoad() {
            let config = WKWebViewConfiguration()
            config.allowsInlineMediaPlayback = true
            webView = WKWebView(frame: view.frame, configuration: config)
            webView.navigationDelegate = self
            view = webView
            guard let roomUrl = URL(string: roomUrlString) else {
            webView.load(URLRequest(url: roomUrl))
  • For iOS 14.3 and 14.4 use SFSafariViewController to open a website containing an iframe with its src specified as a Whereby meeting, alongside a custom user interface:

    import SafariServices
    class ViewController: UIViewController, SFSafariViewControllerDelegate {
        public var roomUrlString = "" // Replace by your own
        override func viewDidAppear(_ animated: Bool) {
            guard let roomUrl = URL(string: roomUrlString) else {
            let safariVC = SFSafariViewController(url: roomUrl)
            safariVC.delegate = self
            present(safariVC, animated: true)
  • Redirect to a browser (Safari by default) for iOS versions lower than 14.3:

    import UIKit
    class ViewController: UIViewController {
        public var roomUrlString = "" // Replace by your own
        override func viewDidLoad() {
            guard let roomUrl = URL(string: roomUrlString),
                UIApplication.shared.canOpenURL(roomUrl) else {

Here is an example on how to handle different solutions, depending on the iOS version:

if #available(iOS 14.5, *) {
    // Use WKWebView
} else if #available(iOS 14.3, *) {
    // Use SFSafariViewController
} else {
    // Redirect to browser app

When the app is sent to background, the camera is disabled. If you need the microphone to continue working while the app is in the background, we recommend redirecting to Safari app.

To use Whereby with Cordova (Phonegap) please use the plugin for SafariViewController

Embedding in React Native

Anchor link for embedding in React Native

Before starting, you will need to add the corresponding permissions to be able to access both the camera and microphone as described in the Android and iOS sections.

Follow this guide to add and setup the react-native-webview library in your React Native project.

Finally, add the WebView component to your code, setup the properties and fill the room URL and parameters. Here is a short example:

import React, { Component } from 'react';
import { WebView } from 'react-native-webview';

const roomUrl = ''; // Replace by your own
const roomParameters = '?needancestor&skipMediaPermissionPrompt';

export default class App extends Component {
  render() {
    return (
        source={{ uri: roomUrl + roomParameters }}

        // iOS specific:

        // Android specific:

Embedding in Flutter

Anchor link for embedding in Flutter

Before starting, you will need to add the corresponding permissions to be able to access both the camera and microphone as described in the Android and iOS sections.

We recommend using the flutter_inappwebview and permission_handler modules to handle media permissions in the webview. Update the settings in your iOS and Android projects to match the requirements. Note that there is a known issue to show the keyboard in Android webviews.

Finally, add the WebView component to your code, setup the properties and fill the room URL and parameters. Here is a short example:

import 'package:flutter/material.dart';
import 'dart:io' show Platform;
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:permission_handler/permission_handler.dart';

var _url = ""; // Replace by your own

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  Widget build(BuildContext context) {
    if (Platform.isAndroid) {
      _url += '?skipMediaPermissionPrompt';
    return MaterialApp(
      home: InAppWebViewPage(),

class InAppWebViewPage extends StatefulWidget {

  _InAppWebViewPageState createState() => new _InAppWebViewPageState();

class _InAppWebViewPageState extends State<InAppWebViewPage> {

  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text("Meeting")),
        body: Container(
            child: Column(children: <Widget>[
            child: Container(
              child: InAppWebView(
                initialUrlRequest: URLRequest(url: Uri.parse(_url)),
                initialOptions: InAppWebViewGroupOptions(
                    crossPlatform: InAppWebViewOptions(
                      mediaPlaybackRequiresUserGesture: false,
                    ios: IOSInAppWebViewOptions(
                      allowsInlineMediaPlayback: true,
                androidOnPermissionRequest: (InAppWebViewController controller,
                    String origin, List<String> resources) async {
                  await Permission.camera.request();
                  await Permission.microphone.request();
                  return PermissionRequestResponse(
                      resources: resources,
                      action: PermissionRequestResponseAction.GRANT);

Meeting customization

Anchor link for meeting customization

Meeting customization is achieved with url parameters for each iframe instance. It’s possible for each participant in a meeting to have different parameter combinations. Learn more about combining parameters.

URL ParameterDescription
?minimalApplies a minimal UI. Turns off all controls except for cam and mic.
?video=offParticipant joins the meeting with camera turned off.
?audio=offParticipant joins the meeting with microphone turned off.
?screenshare=<on|off>Show/hide the screenshare button.
?chat=<on|off>Show/hide the chat button.
?people=offHide the people button.
?leaveButton=<on|off>Show/hide the leave button.
?displayName=<name>Set display name of participant.
?background=offHide the meeting background.
?logo=offHide the logo in the room header.
?locking=offHide the room lock button.
?participantCount=offHide the participant counter.
?settingsButton=offHide the settings button.
?pipButton=offHide the Picture in Picture button.
?moreButton=offHide the more button.
?lang=<code>Set the meeting UI language to either English en, French fr, German de, Norwegian nb, Portuguese pt, Spanish es, or Japanese jp.
?floatSelfFloat the self view to the bottom right.
?breakout=<on|off>Show/hide the breakout room feature for the meeting host.
?groups=Orange,Banana,CoconutPredefine up to 20 groups for the breakout groups function.
?timer=<on|off>Show/hide the meeting timer.
?precallReview=<on|off>Determines if users see the pre-call review step.
?personality=<on|off>Can be used to turn off “cheery” UI language that may not fit certain businesses.
?metadata=<string>Gets passed on to the corresponding webhooks.

Property details

?minimalAnchor link for ?minimal

The minimal parameter applies a combination of UI adjustments to simplify the embedded meeting interface.

Hidden items: Status bar, chat button, screensharing button, leave button, and Whereby’s branding.

Shown items: Video and audio buttons.

For further adjustments, additional parameters can be combined with ?minimal. For example ?minimal&chat=on will show the chat button.

?video=offAnchor link for ?video=off

Participants join the meeting with their camera off, they can turn it on whenever they want.

Use case: A sales representative showcasing a product to a customer relaxing at home.

?audio=offAnchor link for ?audio=off

Participants join the meeting with their microphone off, they can turn it on whenever they want.

Use case: A presentation is being given in a big meeting where attendees are not expected to participate verbally.

?screenshare=<on|off>Anchor link for ?screenshare=<on|off>

Show/hide the screensharing button for the meeting participant.

Screensharing is available on all browsers that support this natively. Currently no mobile browsers support screensharing.

?chat=<on|off>Anchor link for ?chat=<on|off>

Show/hide the chat button. Messages are not stored after the meeting has ended.

?people=offAnchor link for ?people=off

Hide the people button.

Use case: The people button shows the participant list, which can be useful for bulk management of participants in bigger meetings.

?displayName=<name>Anchor link for ?displayName=<name>

Set the display name for a participant instead of prompting the user for this information.

Use case: A participant’s name may be known before they join the meeting. Including this information as a parameter will save the user from entering their name again.

?background=offAnchor link for ?background=off

Hide the default meeting background.

Use case: Hiding the meeting background allows the meeting to appear more integrated by allowing the app or service’s branding shine through as the new background.

?logo=offAnchor link for ?logo=off

Hide the logo in the room header.

Use case: Control whether or not your company logo is displayed in the room header.

?locking=offAnchor link for ?locking=off

Hide the room lock button.

Use case: When set to off the lock button won’t be displayed in the room header. Also, hosts won’t be able to lock/unlock the room through the settings menu or the keyboard shortcut.

?participantCount=offAnchor link for ?participantCount=off

Hide the participant counter.

Use case: Will display the current and maximum number of participants in the room, e.g. “2/4” or “13/100”.

?settingsButton=offAnchor link for ?settingsButton=off

Hide the settings button.

Use case: Control whether or not the settings button is displayed in the room header.

?pipButton=offAnchor link for ?pipButton=off

Hide the Picture in Picture button.

Use case: Control whether or not the Picture in Picture button is displayed in the room header. Picture in Picture lets you pop out your meeting guests’ faces while browsing other tabs or applications.

?moreButton=offAnchor link for ?moreButton=off

Hide the “…” button.

Use case: Control whether or not the “…” button is displayed in the room header.

?lang=<code>Anchor link for ?lang=<code>

Set the meeting UI language to match your product or service. Select from either English en, French fr, German de, Norwegian nb, Portuguese pt, Spanish es, or Japanese jp.

?floatSelfAnchor link for ?floatSelf

Float the self view to the bottom right.

Use case: Floating the self view to the bottom right maximizes the space for other meeting participants.

?breakout=<on|off>Anchor link for ?breakout=<on|off>

Show/hide the breakout room feature for the meeting host.

Use case: Combine bigger meetings with smaller, collaborative sessions. Your hosts can start breakout sessions where participants are split into smaller groups.

?groups=Orange,Banana,CoconutAnchor link for ?groups=Orange,Banana,Coconut

Predefine up to 20 groups for the breakout groups function.

Use case: Setting up groups ahead of time can save the host time during the meeting. Hosts will still be able to modify the group setup during the session using the in-room controls if needed.

?timer=<on|off>Anchor link for ?timer=<on|off>

Show/hide the breakout room feature for the meeting host.

Use case: Set this to “on” to have the meeting timer be displayed in the room. When set to “off”, room hosts can still activate the meeting timer from the “…” button, unless this button has been hidden via the ?minimal or ?moreButton=off parameters.

?precallReview=<on|off>Anchor link for ?precallReview=<on|off>

Determines if users see the pre-call review step.

Use case: The pre-call review step will allow users to check their video/audio settings before joining the room, but this can be skipped by setting this parameter to “off”.

?personality=<on|off>Anchor link for ?personality=<on|off>

Can be used to turn off “cheery” UI language that may not fit certain businesses.

Use case: Set this to “off” to use more neutral language for certain text strings, like replacing “Have a good one!” with “You’ve left the room” when participants leave the room.

?metadata=<string>Anchor link for ?metadata=<string>

Can be used to pass any URL-encoded string so that it is included in the corresponding webhooks related. The decoded string has a limit of 512 characters.

Use case: Set it to the user’s ID so that you can easily track through webhooks when a particular user joins or leaves a room.

Combining parameters

Anchor link for combining parameters

Further customize the meeting by combining parameters by using the ampersand symbol (&). The following example combines the embed with screenshare=off and people=off


Host privileges

Anchor link for host privileges

Hosts can join the meeting with the hostRoomUrl. They have the following features available:

  • Lock and unlock the meeting.
  • Remove, mute, and spotlight meeting participants.
  • Enter locked meetings without knocking.
  • Host privileges are valid immediately on meeting creation and persist for an hour after the endDate.

An example of the returned JSON containing the hostRoomUrl. Explore more in the API docs ↗.

    "meetingId": "1",
    "startDate": "2022-01-26T17:37:00.000Z",
    "endDate": "2022-01-27T17:36:00.000Z",
    "roomUrl": "https://example.whereby.com/room",
    "hostRoomUrl": "https://example.whereby.com/room?roomKey=eFhcG...i00ZG"

Allowed domains

Anchor link for allowed domains

By default, Embedded meetings inside an iframe will load from any domain they are hosted in. If you want to limit which domains are allowed, go to your account and add them under “Allowed domains”. Remember that you also have to allow localhost if you wish to test the iframe during development.

Please note that domains must be prefixed by https:// (except http://localhost), and have no path. Wildcards to allow all subdomains under a domain are permitted, for example https://*.domain.com. If you are using another port than 443 for https, you need to include a line allowing it, for example https://dev.domain.com:8080.

To validate which domains are allowed, follow the instructions in Troubleshooting.


Anchor link for webhooks

Webhooks ↗ are user-defined callbacks triggered by meeting events. They are configured in the account dashboard. These are the supported event types:

  • room.client.joined: Sent when a user joins a meeting after the pre-call screen if enabled.
  • room.client.left: Sent when a user leaves a meeting room, either by closing the browser tab or by click on the leave button.
  • room.session.started: Sent when a room session starts, which is when there are at least 2 users in a room.
  • room.session.ended: Sent when a room session ends. Currently, a session will end when the number of participants has been less than 2 for some time. This heuristic could change in the future to better determine that a session has ended.

Please note that webhook events are sent for interactions that happen between the creation of the meeting and an hour after the endDate of a meeting. Also consider that a particular event can be sent more than once, and that you could receive events in non-chronological order.

Event objects

Events are delivered to their corresponding webhook endpoint in JSON format, as the body of an HTTP request. The table below describes their top-level attributes.

idString that uniquely identifies the event.
apiVersionThe Whereby API version used to populate data.
createdAtISO representation of the creation date of the event.
typeThe event’s type identifier, e.g. room.client.joined
dataObject containing information associated with the event.

Data properties

Properties in data that are common to all events:

meetingIdThe identifier of the meeting that the user has joined/left or where the session has started/ended.
roomNameThe string that identifies the room assigned to the meeting. It’s the last path parameter of the roomUrl.

Additional properties in data for both room.client.joined and room.client.left:

roleNameThe client’s role depending on what URL they use to access the meeting.
numClientsNumber of clients connected to the meeting after the event.
metadataString that matches the "metadata" query parameter passed to the room URL.

The property roleName will have one of the following values:

  • host: A user joined using the hostRoomUrl.
  • visitor: A user joined using the regular roomUrl.
  • granted_visitor: Same as a visitor but can join without knocking if the room is locked.
  • member: A user with an account in your Embedded organization.
  • owner: A user with an admin account in your Embedded organization.

An example of a webhook event object:

    "id": "d7c4df48b85318352b47d2df45872bf9be87595af379e2a8ad8f1ad28b2a482e",
    "apiVersion": "1.0",
    "createdAt": "2021-01-21T16:29:59.681Z",
    "type": "room.client.joined",
    "data": {
        "roleName": "host",
        "meetingId": "134",
        "roomName": "/af0b7b66-c738-4981-887a-ad416754f32d",
        "numClients": 8,
        "metadata": "<custom-metadata>"

Validating events

To prevent from man-in-the-middle attacks ↗ , webhook requests to your endpoint contain a signature in the Whereby-Signature header. This string is generated with a unique secret that only you can view when creating or editing a webhook in the Embedded dashboard. Only Whereby and you have access to this secret, and no third party can send forged events to your endpoint. On top of that the header also includes a timestamp to help you prevent replay attacks. The header is composed of a timestamp and the signature itself, for example:

Whereby-Signature: t=1606227791,v1=94a23dc9d73e8e6abdf9d4095aee954697e9317e9649e742361b35707edd45a3

To verify an event’s signature, follow these steps:

  1. Extract the timestamp and signature by splitting the string on , then removing both t= and v1= from the resulting strings.
  2. Prepare the signedPayload string by concatenating the timestamp (as a string), the character . and the JSON event object (the request body).
  3. Calculate the HMAC: It is the SHA256 hash of signedPayload, using the endpoint’s signing secret (the one you get when creating the webhook) as the key.
  4. Compare the signature from the header to the one you just generated. To protect yourself from timing attacks consider using a constant-time equality function instead of the default equality operator of the language you’re using. Finally, to prevent replay attacks, compare the header’s timestamp with the current one and decide if the elapsed time is within your allowed threshold.

An example of a webhook event validation:

import crypto from "crypto";

const MAX_ELAPSED_TIME = 1000 * 60; // 1 minutes in milliseconds

function isWebhookEventValid({ body, headers }) {
    const wbSignature = headers["whereby-signature"];
    const matches = wbSignature.matchAll(/t=(.*),v1=(.*)/gm);

    let timestamp, signature;
    for (const match of matches) {
      timestamp = match[1];
      signature = match[2];

    const current = new Date();
    const diffTime = current - parseInt(timestamp * 1000, 10);

    const signedPayload = timestamp + "." + JSON.stringify(body);

    const sha256Hasher = crypto.createHmac("sha256", WEBHOOK_SECRET);
    const hash = sha256Hasher.update(signedPayload);
    const hashStr = hash.digest("hex");

    return (
        Buffer.from(hashStr, "utf-8"),
        Buffer.from(signature, "utf-8")
      ) && diffTime < Whereby.MAX_ELAPSED_TIME

Failed delivery

Any 5xx response to the webhook delivery request will trigger a retry, for a total of 2 retries. A short exponential backoff will be used.


Anchor link for troubleshooting

Verify an API key

Check if an API key is valid with the simple interface below. Alternatively use cURL from either a terminal window or server.

curl https://api.whereby.dev/v1/hello \
  --head \
  -H "Authorization: Bearer API_KEY"

A 200 response indicates the API key is working. A 401 response means the provided key is incorrect.

Check if a domain is allowed

Enter your Whereby organization’s subdomain then run the cURL command in a terminal window.

Note: This section is only applicable if allowed domains have been set, please see Allowed domains section for further information.

curl --head "https://YOUR_SUBDOMAIN.whereby.com/csp"

A successful response is indicated with your allowed domains included in the Content-Security-Policy’s header.


Anchor link for examples