Recording webhook
The recording webhook is ReplayVOD's outbound "react to something" primitive: when a recording reaches a terminal state, the recorder POSTs a signed JSON event to an endpoint you choose. Point it at a notifier, a media-server refresh, a NAS, or a post-processing/upload script, with zero ReplayVOD-specific code on the other end. The server never knows or cares what is on the receiving side.
It is owner-configured (the config holds a signing secret and a server-side egress URL) and delivered from a durable, at-least-once outbox, so a terminal event survives a restart and a brief receiver outage.
Configure
Section titled “Configure”Use System → Webhook in the owner dashboard:
- Enable the webhook and set the endpoint URL. Use
httpsfor a public host;httpis allowed forlocalhostor a private/LAN address (e.g.http://192.168.1.50:8096). Link-local / cloud-metadata addresses (169.254.0.0/16) are rejected. - Choose which events fire (at least one is required while enabled).
- Copy the signing secret (auto-generated on first enable; rotate it with a confirm dialog). Configure it on your receiver to verify deliveries.
- Send test posts a one-off signed
recording.testdelivery so you can confirm connectivity and signature before a real recording.
The page also shows a recent delivery log (outcome, HTTP status, attempts) and a per-delivery retry for failed/rejected ones.
Events
Section titled “Events”| Event | When |
| --------------------- | ---- |
| recording.completed | A recording finalized to DONE. |
| recording.failed | A recording failed or was cancelled (not a shutdown interrupt, which stays RUNNING for resume). |
| recording.test | Sent only by the dashboard "Send test" button ("test": true). |
Request
Section titled “Request”POST <your-url> with Content-Type: application/json and:
| Header | Meaning |
| ----------------------------- | ------- |
| Replayvod-Webhook-Id | Unique per delivery. Use it for idempotency. |
| Replayvod-Webhook-Timestamp | RFC3339 (nanosecond) send time. |
| Replayvod-Webhook-Event | The event id (also in the body; the body is authoritative). |
| Replayvod-Webhook-Signature | sha256=<hex HMAC> (see Verifying). |
| User-Agent | ReplayVOD-Webhook/1 |
{ "version": 1, "event": "recording.completed", "video_id": 42, "status": "DONE", // videos.status: DONE | FAILED "completion_kind": "complete", // complete | partial | cancelled "truncated": false, // stopped before the broadcast ended "broadcaster_id": "12345", "broadcaster_login": "speedy", "broadcaster_name": "Speedy", "title": "Any% PB attempts", "category": "Celeste", "started_at": "2026-05-30T12:00:00Z", "ended_at": "2026-05-30T13:00:00Z", // omitted for a failure that never finalized "duration_seconds": 3600.0, "total_size_bytes": 1048576, "error": "auth failed", // present only on recording.failed "parts": [ { "part_index": 1, "path": "videos/vod-42-01.mp4", // storage-relative; useful to a co-located consumer "size_bytes": 600, "duration_seconds": 1800.0, "download_url": "https://your-host/api/v1/videos/42/parts/1/download?exp=...&sig=..." } ]}parts is always a JSON array (never null), one entry per recorded part, each
with its own download_url. version is the payload schema version; branch on
it if it ever changes.
Verifying the signature
Section titled “Verifying the signature”expected = "sha256=" + lowercase_hex(HMAC_SHA256(secret, id + timestamp + body))secret is the dashboard signing secret; id and timestamp are the header
values; body is the raw request body bytes (verify before parsing).
Compare in constant time. Example (Python):
import hashlib, hmac
def verify(secret: str, headers, raw_body: bytes) -> bool: msg = (headers["Replayvod-Webhook-Id"].encode() + headers["Replayvod-Webhook-Timestamp"].encode() + raw_body) expected = "sha256=" + hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, headers["Replayvod-Webhook-Signature"])Reject deliveries whose timestamp is too old to blunt replay, and de-duplicate
on Replayvod-Webhook-Id.
Fetching the files
Section titled “Fetching the files”Each part of a completed, non-retained recording carries a download_url: an
absolute, signed, unauthenticated link that streams that part's bytes (HTTP
range supported) with no dashboard session required. Its maximum lifetime is the
operator's download.signed_url_ttl_hours
(default 7 days; 0 disables signed URLs, leaving only path). When the
recording auto-delete task is enabled, retention caps the URL at the recording's
retention deadline; after a recording has been retained, retry payloads keep the
part metadata but omit download_url.
The origin is taken from PUBLIC_BASE_URL when set, otherwise from the direct
EventSub callback origin when direct mode is active, otherwise from the OAuth
callback origin. Set PUBLIC_BASE_URL for normal deployments. A consumer that
shares ReplayVOD's storage volume can use path directly.
Delivery semantics
Section titled “Delivery semantics”- Respond
2xxto acknowledge.429and5xxare retried with exponential backoff; other4xx/3xxare permanent rejections (redirects are not followed). Deliveries are persisted in a durable outbox and survive restarts, so delivery is at-least-once — keep handlers idempotent onReplayvod-Webhook-Id. - The dashboard's delivery log is the recent (durable) history; old terminal
rows are pruned by
recording_webhook_delivery_retention_days. - Beyond the URL rules above the target is not IP-restricted, because it is owner-configured. Don't expose the config surface to anyone you wouldn't trust with server egress.