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.created

A new hall was created.

hall.closed

A hall was closed (manually or when all participants left).

participant.joined

A participant connected to a hall.

participant.left

A participant disconnected from a hall.

recording.started

Recording began for a hall.

recording.complete

Recording finished and the file is in your S3 bucket.

recording.failed

Recording failed. Check S3 credentials.

clip.ready

A clip extraction job completed. File is in your S3 bucket.

moderation.flag_raised

AI moderation detected content in a session.

Payload structure

Every webhook delivery shares the same envelope:

json
1
2
3
4
5
6
7
8
9
10
11
12
{
  "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.

TypeScript — Express
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
});
Python — FastAPI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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}
Use 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.

1st attemptImmediate
2nd attempt1 minute
3rd attempt5 minutes
4th attempt30 minutes
5th attempt2 hours