Try a call

Webhooks

Join, leave, call ended, and recording.stopped.

Rill POSTs JSON to RILL_WEBHOOK_URL with X-Rill-Timestamp (unix seconds) and X-Rill-Signature (hex HMAC-SHA256 of timestamp + "." + raw body using RILL_API_SECRET). Rill does not follow HTTP redirects — set RILL_WEBHOOK_URL and RILL_RECORD_WEBHOOK_URL to the final HTTPS endpoint.

The call never waits on your URL. Same person replacing a connection does not emit leave then join.

Recording completion is a sidecar POST (RILL_RECORD_WEBHOOK_URL / RILL_RECORD_WEBHOOK_SECRET). Same HMAC headers. Join/leave events never include recording.

Verify with verifyWebhook from @rill/server on the raw POST body. Default skew is five minutes (maxSkewSeconds: 300). After HMAC succeeds, JSON.parse the body and persist idrecording.stopped uses {callId}-{startedAt}; treat duplicate id as idempotent. See examples/webhook-receiver/ for a tiny Node sample.

ts
import { verifyWebhook } from "@rill/server";

export function handleWebhook(
  rawBody: string,
  headers: { timestamp?: string; signature?: string },
) {
  const result = verifyWebhook({
    secret: process.env.RILL_API_SECRET!,
    timestamp: headers.timestamp ?? "",
    signature: headers.signature ?? "",
    body: rawBody,
  });
  if (!result.ok) {
    throw new Error(result.code);
  }
  const event = JSON.parse(rawBody) as { id: string; type: string };
  // Persist event.id. recording.stopped uses {callId}-{startedAt}.
  return event;
}

Other languages

Signature is hex HMAC-SHA256 of "{timestamp}.{raw_body}". Compare with a constant-time check. Reject if |now - timestamp| exceeds 300 seconds.

Go

go
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp))
mac.Write([]byte("."))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
hmac.Equal([]byte(expected), []byte(strings.ToLower(signature)))

Python

python
import hashlib, hmac

expected = hmac.new(
    secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256
).hexdigest()
hmac.compare_digest(expected, signature.lower())

Rust

rust
use hmac::{Hmac, Mac};
use sha2::Sha256;

let mut mac = Hmac::<Sha256>::new_from_slice(secret)?;
mac.update(timestamp.as_bytes());
mac.update(b".");
mac.update(body);
let expected = hex::encode(mac.finalize().into_bytes());

Gold vector (secret secret, timestamp 1700000000):

{"id":"01ARZ3NDEKTSV4RRFFQ69G5FAV","type":"participant.left","createdAt":"2023-11-14T22:13:20Z","callId":"01ARZ3NDEKTSV4RRFFQ69G5FAV","participantId":"user-123"}

Signature: 1bd40059accd4f28c5366b0f9647533d3b6d02276792f17c8f94eaf7c7977343

participant.joined

A new participant id is in the call.

json
{
  "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "type": "participant.joined",
  "createdAt": "2023-11-14T22:13:20Z",
  "callId": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "participantId": "alice"
}

participant.left

That id is gone (including kick). Last-writer-wins replace does not emit leave then join.

json
{
  "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "type": "participant.left",
  "createdAt": "2023-11-14T22:13:20Z",
  "callId": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "participantId": "alice"
}

call.ended

Call destroyed. reason is empty, join_timeout, lifetime, ended, or shutdown.

json
{
  "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "type": "call.ended",
  "createdAt": "2023-11-14T22:13:20Z",
  "callId": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "reason": "empty"
}

recording.stopped

Sidecar completion POST. Same HMAC headers. Configure on rill-record, not the SFU. The sidecar retries like SFU join/leave webhooks; id is {callId}-{startedAt} — treat duplicate id as idempotent.

json
{
  "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV-20260820T120000Z",
  "type": "recording.stopped",
  "createdAt": "2026-08-20T12:15:00Z",
  "callId": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "startedAt": "20260820T120000Z",
  "stoppedAt": "20260820T121500Z",
  "files": [
    {
      "participantId": "alice",
      "kind": "av",
      "contentType": "video/mp4",
      "bucket": "rill",
      "key": "rill/01ARZ3NDEKTSV4RRFFQ69G5FAV/20260820T120000Z/alice.mp4",
      "url": "http://127.0.0.1:3900/rill/..."
    }
  ]
}

Type to search…

↑↓ navigate open esc close