Recording webhook
ReplayVOD can send a signed HTTP POST to an endpoint you choose when a recording finishes or fails. Use it to trigger a notification, refresh a media library, or start your own processing script.
Only the owner can configure the webhook. Deliveries are saved in the database so they survive server restarts and temporary receiver outages.
Configure
Section titled “Configure”- Open System → Webhook.
- Enable the webhook and enter the endpoint URL. Public endpoints must use HTTPS. HTTP is allowed for localhost and private network addresses. Link-local addresses such as
169.254.0.0/16are rejected. - Select at least one event.
- Copy the generated signing secret to your receiver so it can verify requests.
- Select Send test and check that your receiver accepts the signed request.
The page shows recent deliveries, their HTTP status, and attempt counts. You can retry failed or rejected deliveries. If you rotate the signing secret, update the receiver too.
Events
Section titled “Events”| Event | Sent when |
|---|---|
recording.completed |
A recording finishes with status DONE. |
recording.failed |
A recording fails or is cancelled. A shutdown interruption awaiting resume doesn’t trigger this event. |
recording.test |
The owner selects Send test. The payload includes "test": true. |
Request
Section titled “Request”ReplayVOD sends a POST with Content-Type: application/json and these headers:
| Header | Meaning |
|---|---|
Replayvod-Webhook-Id |
Delivery ID. Use it to detect duplicate deliveries. |
Replayvod-Webhook-Timestamp |
RFC3339 send time with nanosecond precision. |
Replayvod-Webhook-Event |
Event name, also included in the body. Treat the body as authoritative. |
Replayvod-Webhook-Signature |
sha256= followed by the hexadecimal HMAC. |
User-Agent |
ReplayVOD-Webhook/1 |
Example completion payload:
{ "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, "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 an array, with one entry per recorded part. It can be empty. version identifies the payload format. Failed events can include an error message and omit download_url, even if some footage was saved. The signed download route only serves completed recordings.
Verifying the signature
Section titled “Verifying the signature”The signature uses the same construction as Twitch EventSub:
expected = "sha256=" + lowercase_hex(HMAC_SHA256(secret, id + timestamp + body))Use the signing secret from the dashboard, the ID and timestamp header values, and the raw request body bytes. Verify before parsing the JSON, and compare signatures in constant time.
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 timestamps outside your accepted delivery window to prevent replay. Track Replayvod-Webhook-Id so retries don’t repeat your processing.
Fetching the files
Section titled “Fetching the files”Completed recordings include a signed download_url for each part while their media is available. The link supports HTTP range requests and doesn’t require a dashboard session.
Anyone with the link can download the part until it expires. Keep full webhook payloads private if you log them.
[download].signed_url_ttl_hours sets the maximum lifetime, with a default of 168 hours (seven days). Set it to 0 to send storage paths without signed links. When automatic deletion is enabled, links also expire by the recording’s deletion deadline. Retries after deletion keep the part details but omit download links.
Set PUBLIC_BASE_URL so links use the address your receiver can reach. If it is unset, the server uses the direct EventSub callback’s origin when available, then the OAuth callback’s origin. A receiver that shares the recording storage can use each part’s relative path instead.
Delivery semantics
Section titled “Delivery semantics”Return a 2xx status to acknowledge a delivery. ReplayVOD retries 429 and 5xx responses with increasing delays. Other 4xx responses and redirects are treated as permanent rejections; redirects aren’t followed.
A delivery can arrive more than once, including after a restart. Make your handler safe to retry using the delivery ID.
[scheduler].recording_webhook_delivery_retention_days controls how long completed delivery history is kept. Queued and active deliveries remain in the database. See the config.toml reference.