# Videos API Base path: `/api/v1/videos` All endpoints require `Authorization: Bearer `. Related: [[01 - Video Management]], [[04 - Database Schema]], [[11 - Bulk Jobs API]], [[07 - Playlists API]] --- ## GET /videos List videos for the current team with pagination, sorting, and filtering. **Query parameters:** | Param | Type | Description | |---|---|---| | `page` | number | Page number (default: `1`) | | `pageSize` | number | Items per page (default: `20`) | | `sortBy` | string | Field to sort by | | `sortOrder` | `asc\|desc` | Sort direction | | `search` | string | Title search (case-insensitive) | | `lintStatus` | `OK\|WARNING\|ERROR` | Exact lint status match | | `hasLintIssues` | boolean | Match `ERROR` or `WARNING` | | `privacyStatus` | `PUBLIC\|PRIVATE\|UNLISTED` | Privacy filter | | `scheduled` | boolean | Videos with a future `scheduledAt` | | `notScheduled` | boolean | Videos without a `scheduledAt` value | | `remoteConflict` | boolean | Videos with a remote conflict flag | | `pendingSync` | boolean | Videos where `hasPendingChanges` is `true` | | `deletedOnYouTube` | boolean | Videos with `youtubeDeletedAt` set | **Response:** ```json { "data": [ "VideoSummary[]" ], "total": 142, "page": 1, "pageSize": 20 } ``` `hasPendingChanges` is computed per video by comparing the current state hash against `lastSyncedHash`. See [[01 - System Overview]] for details on the sync status logic. --- ## GET /videos/:id Get full video detail including lint results, playlists, and collaborators. **Response:** `VideoDetail` object --- ## PATCH /videos/:id Update video metadata fields. **Request body** (all fields optional): ```json { "title": "string", "tags": ["string"], "privacyStatus": "PUBLIC|PRIVATE|UNLISTED", "scheduledAt": "ISO datetime", "categoryId": "string", "selfDeclaredMadeForKids": false, "embeddable": true, "license": "youtube|creativeCommon", "defaultLanguage": "en", "defaultAudioLanguage": "en", "recordingDate": "YYYY-MM-DD", "gameTitle": "string", "collaboratorIds": ["cuid"], "templateId": "cuid" } ``` **Notes:** - `recordingDate` is not returned by YouTube during import and is always `null` after a channel sync. Setting it locally will correctly mark the video as push-pending. - Providing `collaboratorIds` updates `Video.collaboratorIds` (a JSON string array on the `Video` row). This is the single field the render engine reads to resolve `{collab.*}` tokens. - Providing `templateId` applies the template's block configuration to the video's `VideoConfig`. --- ## POST /videos/:id/sync Enqueue a YouTube sync job for the video. Returns immediately; processing is asynchronous via BullMQ. The processor renders the description, computes a metadata hash, skips if unchanged, then pushes all editable fields to YouTube. After a successful push, `renderedDescription` and `lastSyncedHash` are updated on the video row. **Response:** ```json { "queued": true } ``` **Cost:** 50 YouTube API quota units per successful push. See [[10 - Quota API]]. --- ## POST /videos/:id/refresh Pull latest metadata from YouTube and update the local record. This is a synchronous operation. > Re-importing after making local changes will reset the `lastSyncedHash` baseline. **Response:** Updated `VideoDetail` --- ## POST /videos/:id/accept-remote Resolve a detected remote conflict by adopting the pending remote snapshot as the new local state. Requires **EDITOR** role. **Zero YouTube API calls** — operates entirely on the snapshot captured by `detectConflict()` when the conflict was first detected. **Behavior:** 1. Copies `pendingRemoteSnapshot` fields into the live Video columns (`title`, `tags`, `categoryId`, `privacyStatus`, `defaultLanguage`, `defaultAudioLanguage`, `selfDeclaredMadeForKids`, `embeddable`, `license`, `recordingDate`) 2. Sets `youtubeDescription = renderedDescription = pendingRemoteDescription` 3. Promotes `pendingRemoteSnapshot` to `youtubeSnapshot` 4. Recomputes `lastSyncedHash` from the accepted state, sets `lastSyncedAt = now` 5. Clears `pendingRemoteSnapshot`, `pendingRemoteDescription`, `remoteConflict` 6. Writes an audit log entry with `action: 'accept-remote'` **Errors:** - `400` if there is no `pendingRemoteSnapshot` to accept - `404` if the video is not in the caller's team **Response:** Updated `Video` row. For the opposite direction ("keep local, overwrite remote"), just call `POST /videos/:id/sync` — the sync processor overwrites remote and clears `remoteConflict` on success. --- ## POST /videos/:id/render Render the video's description using the current `VideoConfig` and return the result without saving or pushing. **Response:** ```json { "rendered": "string" } ``` --- ## DELETE /videos/:id Soft-delete the video (marks as deleted locally). Does not delete the video from YouTube. --- ## GET /youtube-sync/channels List connected YouTube channels for the current team. Base path: `/api/v1/youtube-sync/channels` **Response:** `Channel[]`