7.5 KiB
YouTube Sync API
Base path: /api/v1/youtube-sync
All endpoints require Authorization: Bearer <jwt>.
Related: 13 - Team Settings, 01 - Video Management, 05 - Queue System, 06 - Authentication
How channel connection works
There is no separate channel-connect endpoint. A YouTube channel is connected automatically during the user's first Google OAuth login:
- User hits
GET /auth/google— redirected to Google consent screen. - Google returns an OAuth callback with
accessTokenandrefreshToken. AuthService.upsertGoogleUserdetects that the user has no team memberships.- It calls the YouTube API (
channels.list?mine=true) using the login OAuth tokens to fetch the user's channel ID, name, and uploads playlist ID. - A
Teamrecord is created (user asOWNER), and aChannelrecord is created and linked — with the YouTube access and refresh tokens stored AES-256 encrypted (TOKEN_ENCRYPTION_KEY).
On subsequent logins, if the user already has a team, the access token is refreshed on all channels they own (connectedBy = user.id). A new refresh token is only stored if Google returns one (which only happens on first auth or after explicit re-consent).
Listing connected channels: GET /teams/:teamId/channels — see 09 - Teams API.
Channel disconnect: Not currently implemented. Removing a channel requires direct database access.
Important: The channel OAuth tokens are the same tokens obtained during user login — not a separate OAuth flow. This means one Google account = one YouTube channel connection at team-creation time. Adding a second channel to an existing team is not currently supported through the API.
POST /youtube-sync/channel-import
Pull all videos from a connected YouTube channel into the local database.
Minimum role: EDITOR
Request body:
{ "channelId": "cuid" }
Response: Import summary
{
"total": 57,
"created": 12,
"updated": 45,
"deleted": 0,
"deletedTitles": []
}
| Field | Description |
|---|---|
total |
Total number of video IDs processed from the YouTube playlist |
created |
New Video rows created |
updated |
Existing Video rows updated |
deleted |
Videos soft- or hard-deleted during this import (see channel-purge-deleted for the purge lifecycle) |
deletedTitles |
Titles of soft-deleted videos |
There is no skipped field. A video item returned by the YouTube API with no id field is silently dropped and counted in neither created nor updated — this is an edge case in malformed API responses and does not appear in the summary.
Behavior:
- Fetches the channel's uploads playlist from YouTube page by page.
- For each video: creates a new
Videorow (withVideoConfig) or updates the existing one. - Sets
lastSyncedHashon every upserted video so the YouTube state is the baseline (no false "push pending" after import). - Also syncs playlists and their video memberships — subject to the
itemCountoptimization described below. - Does not delete local videos that are missing from YouTube — use
channel-purge-deletedfor that.
Playlist itemCount optimization: During playlist membership sync, each playlist's current itemCount (from the YouTube API) is compared against the cached count stored in the local Playlist row from the previous import. If the counts match, that playlist's full video-membership sync is skipped entirely — no playlistItems.list API call is made for it. This saves quota on large channels where most playlists are unchanged. The cached itemCount is updated on every import regardless of whether the sync was skipped.
Known limitation: If videos are moved between playlists such that one playlist gains a video and another loses one in the same import run, both playlists may have unchanged itemCount values and their membership sync will be skipped, leaving the local VideoPlaylist join table stale. Use channel-full-refresh to force-sync all playlists.
Costs YouTube API quota. Each page of 50 videos costs 1
playlistItems.listunit; individual video detail fetches cost 1 unit each.
POST /youtube-sync/channel-full-refresh
Force-reimport all videos and playlists for a channel, bypassing the itemCount optimization — all playlists have their full video membership re-synced regardless of cached counts. Use this when a normal import appears to have missed playlist membership changes.
Minimum role: ADMIN
Request body:
{ "channelId": "cuid" }
Response: Same import summary shape as channel-import.
POST /youtube-sync/import-video-ids
Immediately import specific YouTube video IDs into the database, bypassing the normal playlist-based discovery.
Minimum role: ADMIN
Request body:
{
"channelId": "cuid",
"videoIds": ["dQw4w9WgXcQ", "abc123"]
}
Response:
{
"total": 2,
"created": 1,
"updated": 1,
"notFound": []
}
notFound lists any requested YouTube video IDs that the YouTube API did not return (private, deleted, or wrong ID). Unlike channel-import, this endpoint has no deleted/deletedTitles fields — it does not run the purge lifecycle.
Use this for videos that are unlisted or otherwise not reachable via the uploads playlist.
POST /youtube-sync/channels/:channelId/supplemental-ids
Add YouTube video IDs to the channel's supplemental list. Videos on this list are always imported during future channel-import runs, regardless of playlist membership.
Minimum role: ADMIN
Request body:
{ "videoIds": ["dQw4w9WgXcQ"] }
Response: Updated supplemental ID list.
POST /youtube-sync/channel-purge-deleted
Check each local video against YouTube and soft-delete any that no longer exist. Also restores soft-deleted videos that have reappeared.
Minimum role: ADMIN
Request body:
{ "channelId": "cuid" }
Response:
{
"checked": 200,
"softDeleted": 3,
"restored": 0,
"hardDeleted": 0,
"softDeletedTitles": ["Old Video Title", ...]
}
Each video follows a three-state lifecycle during purge:
- First miss → soft delete. If a video is not found on YouTube and
youtubeDeletedAtis null,youtubeDeletedAtis set to now. The video is hidden from normal list queries but remains in the database. - Grace period → no action. If
youtubeDeletedAtis already set but fewer than 30 days have passed, the video is left untouched. - 30-day expiry → hard delete. If
youtubeDeletedAtis set and is more than 30 days old, the video row (and its related records) is permanently deleted from the database. This is whathardDeletedcounts. - Reappearance → restore. If a previously soft-deleted video reappears on YouTube (e.g. it was temporarily private),
youtubeDeletedAtis cleared and the video is visible again.
GET /youtube-sync/queue-status
Inspect the current state of the YouTube sync (youtube-sync) BullMQ queue.
Minimum role: Any authenticated team member
Response:
{
"active": [
{ "jobId": "string", "videoId": "cuid", "videoTitle": "string", "addedAt": "ISO datetime" }
],
"waiting": [...],
"recentFailed": [
{ "jobId": "string", "videoId": "cuid", "videoTitle": "string", "addedAt": "ISO datetime", "failedReason": "string", "failedAt": "ISO datetime" }
],
"recentCompleted": [
{ "jobId": "string", "videoId": "cuid", "videoTitle": "string", "addedAt": "ISO datetime", "completedAt": "ISO datetime" }
]
}
Returns up to 10 recent failed jobs and 5 recent completed jobs.