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
meeting:started
type MeetingStarted = {
sessionId: string;
roomId: string;
createdAt: number;
};
meeting:ended
meeting:ended
type MeetingEnded = {
sessionId: string;
roomId: string;
createdAt: number;
endedAt: number;
duration: number;
participants: number;
maxParticipants: number;
audioMinutes: number;
videoMinutes: number;
};
peer:joined
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
peer:left
type PeerLeft = {
id: string;
sessionId: string;
roomId: string;
leftAt: number;
duration: number;
metadata?: string;
role?: string;
}
recording:started
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
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
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;
};