Appearance
Webhooks
GatherCloud POSTs signed JSON notifications to your HTTPS endpoints as things happen — an event goes live, a recording finishes, a question is asked.
Event types
| Type | Fires when |
|---|---|
event.published | A draft event is published |
event.started | An event goes live |
event.ended | An event ends |
broadcast.live | A broadcast phase starts streaming |
broadcast.paused | A broadcast is paused |
recording.ready | A merged recording is available |
transcript.ready | A transcript is available |
clip.ready | A requested clip is available |
question.created | A Q&A question is posted |
Register an endpoint
From the dashboard (Webhooks → Add webhook) or via the API:
sh
curl -X POST https://api.gathercloud.dev/v1/webhooks \
-H "x-api-key: gck_..." \
-H "content-type: application/json" \
-d '{
"url": "https://example.com/hooks/gathercloud",
"eventTypes": ["event.started", "recording.ready"]
}'Omit eventTypes (or pass []) to receive everything. The response includes a signing secret (whsec_…) — it is shown exactly once; store it where your receiver can use it for verification.
GET /v1/webhooks— list endpoints (without secrets).DELETE /v1/webhooks/:id— stop deliveries immediately.
Delivery
Each delivery is a POST with:
| Header | Contents |
|---|---|
x-gc-event-type | e.g. recording.ready |
x-gc-event-id | UUID — dedupe on this; retries reuse the same id |
x-gc-signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
The body is the event itself:
json
{
"id": "6e9c2c4e-…",
"type": "event.started",
"workspaceId": "…",
"eventId": "…",
"occurredAt": "2026-06-11T18:00:02.114Z"
}Respond with any 2xx within 10 seconds. On a 5xx or timeout the delivery is retried with backoff; 4xx responses are treated as rejected and not retried. Deliveries can arrive out of order and (rarely) more than once — make your handler idempotent on x-gc-event-id.
Verifying signatures
The signature is HMAC-SHA256("<t>.<raw body>", secret), hex-encoded. Always verify against the raw request body, before any JSON parsing:
ts
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(header: string, rawBody: string, secret: string): boolean {
const { t, v1 } = Object.fromEntries(
header.split(',').map((kv) => kv.split('=') as [string, string]),
);
if (!t || !v1) return false;
// Reject stale timestamps to prevent replay (5 min window).
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}