Skip to content

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

TypeFires when
event.publishedA draft event is published
event.startedAn event goes live
event.endedAn event ends
broadcast.liveA broadcast phase starts streaming
broadcast.pausedA broadcast is paused
recording.readyA merged recording is available
transcript.readyA transcript is available
clip.readyA requested clip is available
question.createdA 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:

HeaderContents
x-gc-event-typee.g. recording.ready
x-gc-event-idUUID — dedupe on this; retries reuse the same id
x-gc-signaturet=<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));
}