Webhooks

Webhooks

Usage:

import { WebhookReceiver } from "@huddle01/server-sdk/webhooks";
 
// Initiate webhook receiver
const webhookReceiver = new WebhookReceiver({
  apiKey: "YOUR_API_KEY",
});
 
// verify and validate webhook data using the receive func
const receivedData = await receiver.receive(req.body, header); 
 
// get properly typed data in typescript
const { data } = webhookReceiver.createTypedWebhookData(
  "meeting:started",
  receivedData.payload,
);

Express Example to listen to webhooks events

💡

To make the webhook work on localhost, you'll have to use something like ngrok for port forwarding.

💡

The Content-Type is set to "Content-Type": "text/plain", attempting to parse it as JSON, multipart, or any other structured format will not work as expected. Please ensure that you handle the content as plain text.

import express, { Request, Response } from "express";
import bodyParser from "body-parser";
import { WebhookReceiver } from "@huddle01/server-sdk/webhooks";
 
const receiver = new WebhookReceiver({
  apiKey: "YOUR_API_KEY",
});
 
const app = express();
const port = 3002;
 
// Middleware to parse the body as text/plain
app.use(express.text({ type: '*/*' }));
 
app.post("/", async (req: Request, res: Response) => {
  try {
    const header = req.headers["huddle01-signature"] as string | undefined;
 
    const data = await receiver.receive(req.body, header);
 
    switch (receivedData.event) {
      case "meeting:started": {
        // !! Thie helper will give properly typed data in typescript
        const { data } = webhookReceiver.createTypedWebhookData(
          "meeting:started",
          receivedData.payload,
        );
 
        // ... logic as per requirment
        break;
      }
 
      case "meeting:ended": {
        const { data } = webhookReceiver.createTypedWebhookData(
          "meeting:ended",
          receivedData.payload,
        );
 
        // ... logic as per requirment
      }
 
      // ... cases for peer:joined, peer:left, recording:started, etc
    }
 
    res.status(200).send({ success: true });
  } catch (error) {
    console.error({ error });
  }
});
 
app.listen(port, () => {
  console.log(`🦊 Express server running at http://localhost:${port}`);
});
 

Events

You can listen to these event on your server side and perform various actions based on the events. Following is the list of events you can listen to:

meeting:started

type MeetingStarted = {
  sessionId: string;
  roomId: string;
  createdAt: number;
};

meeting:ended

type MeetingEnded = {
  sessionId: string;
  roomId: string;
  createdAt: number;
  endedAt: number;
  duration: number;
  participants: number;
  maxParticipants: number;
  audioMinutes: number;
  videoMinutes: number;
};

peer:joined

type PeerJoined = {
  id: string;
  sessionId: string;
  roomId: string;
  joinedAt: number;
  metadata?: string;
  role?: string;
  browser: {
    name?: string;
    version?: string;
  };
  geoData?: {
    region: string;
    country: string;
  };
  device: {
    model?: string;
    type?: string;
    vendor?: string;
  };
};

peer:left

type PeerLeft = {
  id: string;
  sessionId: string;
  roomId: string;
  leftAt: number;
  duration: number;
  metadata?: string;
  role?: string;
}

recording:started

type Recording = {
  id: string;
  roomId: string;
  sessionId: string;
  files: {
    duration: number;
    size: number;
    filename: string;
    location: string;
  }[];  // This array will be empty when we start the recording
  streams: {
    duration: number;
    url: string;
    startedAt: number;
    endedAt: number;
  }[];  // This array will be empty when we start the recording
  cid?: string;
  ipfsUrl?: string;
};

recording:stopped

type Recording = {
  id: string;
  roomId: string;
  sessionId: string;
  files: {
    duration: number;  // Duration is in seconds
    size: number;  // Size is in MB
    filename: string;
    location: string;
  }[];
  streams: {
    duration: number;  // Duration is in seconds
    url: string;
    startedAt: number;
    endedAt: number;
  }[];
  cid?: string;
  ipfsUrl?: string;
};

recording:updated

type Recording = {
  id: string;
  roomId: string;
  sessionId: string;
  files: {
    duration: number;  // Duration is in seconds
    size: number;  // Size is in MB
    filename: string;
    location: string;
  }[];
  streams: {
    duration: number;  // Duration is in seconds
    url: string;  // rtmp url, where you streamed to
    startedAt: number;
    endedAt: number;
  }[];
  cid?: string;
  ipfsUrl?: string;
};

How to manually verify and validate webhooks

This code ensures the messages from Huddle01 are genuine - like a secret handshake. It checks if the signature matches what we expect, using your unique key. This way, you know it's really Huddle01 contacting you. We use the received header and your API key to verify the payload's integrity. By hashing the payload with a timestamp, we protect against tampering and replay attacks. The same concept can be carried to any other language. If you are using any other language and facing difficulty, please let us know.

Example

import crypto from "node:crypto"
 
const verifyWebhook = (body: any, header?: string | null) => {
  // Huddle01-Signature: t=36285904404,sha256=88f3ff0fds9sf8a98vb0b096e81507cfd5c932fc17cf63a4a55566fd38da3a2d3d2
  const timestamp = header?.split(",")[0]?.split("=")?.[1];
  const signature = header?.split(",")[1];
 
  if (!timestamp || !signature) throw new Error("Invalid headers");
 
  let data: Record<any, any>;
 
  if (typeof body === "string") data = JSON.parse(body);
  else if (typeof body === "object") data = body;
  else throw new Error("Invalid body");
 
  if (!data.id) throw new Error("Invalid body");
 
  // Tolerance zone: 5 minutes ago
  const tolerance = Date.now() - 5 * 60 * 1000;
 
  if (Number(timestamp) < tolerance) {
    // The request timestamp is outside of the tolerance zone.
    throw new Error("Request expired");
  }
 
  const hashPayload = `${data.id}.${timestamp}.${JSON.stringify(data)}`;
 
  const signatureAlgorithm = signature.split("=")[0];
 
  if (!signatureAlgorithm) throw new Error("Invalid signature algorithm");
 
  const hmac = crypto.createHmac(signatureAlgorithm, "YOUR_API_KEY");
 
  const digest = Buffer.from(
    `${signatureAlgorithm}=${hmac.update(hashPayload).digest("hex")}`,
    "utf8",
  );
 
  const providerSig = Buffer.from(signature, "utf8");
 
  if (
    providerSig.length !== digest.length ||
    !crypto.timingSafeEqual(digest, providerSig)
  ) {
    throw new Error("Invalid signature");
  }
 
  return data;
};
Audio/Video Infrastructure designed for developers to empower them to ship simple yet powerful Audio/Video Apps.
support
company
Copyright © 2024 Graphene 01, Inc. All Rights Reserved.