Skip to content

API: Media & webhooks

Controllers: admin-media.controller.ts + admin-media-webhook.controller.ts. Mount admin/media.

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.

MethodPathPermNotes
GET/admin/media/assetscatalog:readList; filter by kind, status, seriesId, episodeId, lang.
GET/admin/media/assets/:idcatalog:readDetail.
POST/admin/media/assets/:id/retrycatalog:writeRe-request transcode for a failed asset.
DELETE/admin/media/assets/:idcatalog:deleteSoft-delete.
{
"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"
}

Permission: catalog:write.

Multipart form: kind (poster|hero|thumb) + file. Uploads to CF Images (stubbed in v2: see Integrations status).

Permission: catalog:write.

Multipart form: episodeId, lang (en|ar|id), file (SRT or VTT). Creates a media_asset with kind='caption' and lang=<lang>.

Permission: none: HMAC-verified via a shared secret with Cloudflare.

Payload: CF Stream event schema (see CF docs). The handler:

  1. Verifies HMAC signature against CF_STREAM_WEBHOOK_SECRET.
  2. Looks up asset by providerId.
  3. Updates status (processingready or failed).
  4. Stores playbackId, durationS, width, height.
  5. Extracts thumbnail candidates.

Response 204 on success. 401 on bad HMAC.

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.