Webhooks
Signed, retried HTTPS callbacks for meeting activity.
Add an endpoint in Developers → Webhooks (or with the API). Deewan POSTs a JSON event to it whenever something you subscribed to happens.
Events
| Type | When |
|---|---|
room.created / room.updated / room.deleted | A room changes through the API |
room.started | The first person joins a meeting |
room.finished | The meeting has emptied and closed |
participant.joined / participant.left | Someone enters or leaves |
participant.stage_changed | A webinar attendee is invited to speak, or sent back to the audience |
meeting.scheduled / meeting.updated / meeting.cancelled | A calendar meeting changes through the API |
poll.created / poll.closed | A poll opens or closes |
breakout.started / breakout.ended | Breakout rooms open or close |
recording.ready | A recording is complete and can be fetched |
Payload
{
"id": "evt_Vd9sX2…",
"type": "participant.joined",
"created_at": "2026-09-17T09:20:11.482Z",
"data": {
"room": { "id": "5f0c2b8e-…", "code": "k7pq-3mzt-9xwe", "name": "Onboarding call" },
"participant": { "identity": "api_Qm9v….k2d9x0aa", "name": "Omar", "role": "participant" },
"occurred_at": "2026-09-17T09:20:11.000Z"
}
}Verify every request
Each request carries Deewan-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of <t>.<raw body> with your endpoint's secret. Reject anything that doesn't match or is older than five minutes.
Next.js route with the SDK
import { verifyWebhook } from "@deewan/sdk";
export async function POST(request: Request) {
const raw = await request.text(); // the raw body, not parsed JSON
try {
const event = await verifyWebhook(raw, request.headers.get("deewan-signature"), process.env.DEEWAN_WEBHOOK_SECRET!);
if (event.type === "room.finished") await markSessionComplete(event.data);
return new Response(null, { status: 204 });
} catch {
return new Response("invalid signature", { status: 400 });
}
}Python, without the SDK
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts["t"])
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Delivery
- Respond with any 2xx within 10 seconds. Do slow work after responding.
- Failed deliveries are retried from a durable queue after about 10 seconds, 1 minute, 5 minutes, 30 minutes and 2 hours, so they survive our restarts.
4xxanswers (except 408 and 429) are not retried. - Delivery is at-least-once: use the event
idto ignore duplicates. - Events can arrive out of order; use
occurred_at. - Endpoints must be public
https://URLs. Private, local and cloud-metadata addresses are refused, and redirects are not followed. - See each endpoint's recent deliveries in the dashboard or at
GET /webhooks/{id}/deliveries.