API: Media & webhooks
Controllers: admin-media.controller.ts + admin-media-webhook.controller.ts. Mount admin/media.
Direct upload (video)
Section titled “Direct upload (video)”POST /v1/admin/media/uploads/direct
Section titled “POST /v1/admin/media/uploads/direct”Permission: catalog:write.
Request:
{ "kind": "video", "seriesId": "0193...", "episodeId": "0194...", "sizeBytes": 152841234, "meta": { "sourceFileName": "ep-01.mp4" }}Response 200:
{ "assetId": "0195...", "uploadUrl": "https://upload.videodelivery.net/tus/...", "providerId": "cf-stream-uid-abc"}The client then TUS-uploads to uploadUrl. On finish, CF calls the webhook.
Assets
Section titled “Assets”| Method | Path | Perm | Notes |
|---|---|---|---|
| GET | /admin/media/assets | catalog:read | List; filter by kind, status, seriesId, episodeId, lang. |
| GET | /admin/media/assets/:id | catalog:read | Detail. |
| POST | /admin/media/assets/:id/retry | catalog:write | Re-request transcode for a failed asset. |
| DELETE | /admin/media/assets/:id | catalog:delete | Soft-delete. |
Asset shape
Section titled “Asset shape”{ "id": "0195...", "kind": "video", "seriesId": "0193...", "episodeId": "0194...", "provider": "cf_stream", "providerId": "cf-uid", "playbackId": "cf-playback-id", "status": "ready", "durationS": 342, "width": 1920, "height": 1080, "lang": null, "createdAt": "2026-07-06T12:30:00Z"}Image upload
Section titled “Image upload”POST /v1/admin/media/uploads/image
Section titled “POST /v1/admin/media/uploads/image”Permission: catalog:write.
Multipart form: kind (poster|hero|thumb) + file. Uploads to CF Images (stubbed in v2: see Integrations status).
Captions
Section titled “Captions”POST /v1/admin/media/captions
Section titled “POST /v1/admin/media/captions”Permission: catalog:write.
Multipart form: episodeId, lang (en|ar|id), file (SRT or VTT). Creates a media_asset with kind='caption' and lang=<lang>.
Webhook (system)
Section titled “Webhook (system)”POST /v1/admin/media/webhook/cf-stream
Section titled “POST /v1/admin/media/webhook/cf-stream”Permission: none: HMAC-verified via a shared secret with Cloudflare.
Payload: CF Stream event schema (see CF docs). The handler:
- Verifies HMAC signature against
CF_STREAM_WEBHOOK_SECRET. - Looks up asset by
providerId. - Updates status (
processing→readyorfailed). - Stores
playbackId,durationS,width,height. - Extracts thumbnail candidates.
Response 204 on success. 401 on bad HMAC.
Media sync cron
Section titled “Media sync cron”Runs every minute (see Cron jobs). Polls CF for assets stuck > 10 min in uploading / processing and reconciles state. This exists to handle dropped webhooks.