Skip to content

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.

  1. Open System → Webhook.
  2. 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/16 are rejected.
  3. Select at least one event.
  4. Copy the generated signing secret to your receiver so it can verify requests.
  5. 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.

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.

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.

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.

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.

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.