Platform
Webhooks
Relay sends signed HTTP POST requests to your configured endpoint whenever platform events occur — no polling required. Every delivery is signed with HMAC-SHA256 so you can verify it genuinely came from Relay.
Configure your endpoint
Set your webhook URL in Dashboard → Settings → Webhooks. Relay will send all events to this single endpoint. Your endpoint must:
- Accept POST requests with a JSON body
- Return HTTP 2xx within 10 seconds
- Be publicly accessible (no localhost)
Event types
hall.createdA new hall was created.
hall.closedA hall was closed (manually or when all participants left).
participant.joinedA participant connected to a hall.
participant.leftA participant disconnected from a hall.
recording.startedRecording began for a hall.
recording.completeRecording finished and the file is in your S3 bucket.
recording.failedRecording failed. Check S3 credentials.
clip.readyA clip extraction job completed. File is in your S3 bucket.
moderation.flag_raisedAI moderation detected content in a session.
Payload structure
Every webhook delivery shares the same envelope:
{
"event": "hall.created",
"data": {
"hall_id": "h_01j9x2kp3n...",
"name": "engineering-standup",
"max_participants": 20,
"record": true,
"moderation": false
},
"organisation_id": "org_01j8...",
"timestamp": "2025-04-05T10:00:00.000Z"
}Signature verification
Every request includes a X-Relay-Signature header with an HMAC-SHA256 signature of the raw request body using your webhook signing secret. Always verify this before processing the event.
import crypto from "crypto";
import express from "express";
const app = express();
app.post("/webhooks/relay", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-relay-signature"] as string;
const secret = process.env.RELAY_WEBHOOK_SECRET!;
const expected = crypto
.createHmac("sha256", secret)
.update(req.body)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body.toString());
console.log("Received event:", event.event);
res.sendStatus(200);
});import hmac, hashlib
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
@app.post("/webhooks/relay")
async def relay_webhook(request: Request):
payload = await request.body()
sig = request.headers.get("x-relay-signature", "")
secret = os.environ["RELAY_WEBHOOK_SECRET"].encode()
expected = hmac.new(secret, payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
raise HTTPException(status_code=401, detail="Invalid signature")
event = json.loads(payload)
print(f"Received: {event['event']}")
return {"received": True}timingSafeEqual (or equivalent) when comparing signatures to prevent timing attacks.Retries & delivery
If your endpoint returns a non-2xx response or times out, Relay retries the delivery with exponential backoff — up to 5 attempts over ~2 hours. Delivery history is visible in Dashboard → Webhooks.