API.md
JSON API
The JSON API is available under /api. The service does not enable CORS.
Authentication
Every /api request must include exactly one Authorization header. Its value must exactly equal the configured static token. Do not use a Bearer prefix.
Missing or repeated headers return 401:
{"error":"missing Authorization header"}
An incorrect token returns 401:
{"error":"invalid auth token"}
Invalid request input returns 400 with an error string. Handler failures return 500 with an error string.
Video object
Video listings contain these fields. Times are JSON timestamps in RFC 3339 format. Nullable fields are null when unset.
{
"video_id": "YouTube video ID",
"channel_name": "Channel name",
"title": "Video title",
"published_timestamp": "2026-01-02T15:04:05Z",
"watch_timestamp": null,
"duration": 1200,
"progress": 300,
"short": false,
"is_live": false,
"is_discarded": false,
"downloaded_at": null,
"download_status": null,
"download_error": null
}
duration and progress are seconds. channel_id and the server file path are not exposed.
Channel object
{
"channel_id": "YouTube channel ID",
"name": "Channel name",
"subscribed": true,
"unwatched_count": 3,
"image_url": "https://...",
"enable_shorts": true
}
MVP routes
POST /api/fetch
Fetches new videos from every subscribed channel. It has no request body.
Success response:
{"msg":"videos fetched successfully"}
POST /api/videos
Adds one custom video without subscribing to its channel. The request body accepts a YouTube video ID or URL:
{"video_id":"https://www.youtube.com/watch?v=..."}
A YouTube URL with ?t=N saves N seconds of watch progress. The created channel remains unsubscribed, so later fetches do not add its other videos.
Success response (201):
{"msg":"video added"}
GET /api/videos/new
Returns all unwatched, non-discarded videos:
{"videos":[/* Video objects */]}
Videos are ordered by published_timestamp newest first. Videos with the same timestamp are ordered by video_id descending.
GET /api/videos/watched?page=N
Returns watched, non-discarded videos in pages of 100:
{"videos":[/* Video objects */]}
page is a one-based positive integer and defaults to 1. Invalid, zero, and negative values use page 1. A response containing fewer than 100 videos is the final page.
Videos are ordered by watch_timestamp newest first. Videos with the same timestamp are ordered by video_id descending.
POST /api/videos/:video_id/watch
Marks the video as watched at the current server time. It has no request body.
Success response:
{"msg":"marked video as watched"}
POST /api/videos/:video_id/unwatch
Clears the video watch timestamp. It has no request body.
Success response:
{"msg":"cleared video from watch history"}
POST /api/videos/:video_id/progress
Updates the saved watch progress. The request body must contain a progress string in Go duration, mm:ss, or hh:mm:ss format:
{"progress":"2:30"}
Success response:
{"video":{/* Video object */}}
Missing or invalid progress returns 400.
GET /api/videos/:video_id
Returns one video, including its current download status:
{"video":{/* Video object */}}
A missing video returns 404.
POST /api/videos/:video_id/download
Starts an asynchronous server download. The request body must select one of the supported maximum video heights: 480, 720, 1080, 1440, or 2160.
{"format":720}
Success response (202):
{"msg":"download started"}
Poll GET /api/videos/:video_id to read download_status and download_error. Invalid or missing formats return 400.
GET /api/videos/:video_id/file
Returns the completed server download as an attachment. The endpoint requires the same Authorization header as every other API route. It returns 404 if the video has no completed file.
Channel routes
GET /api/channels
Returns subscribed channels ordered by name:
{"channels":[/* Channel objects */]}
POST /api/channels/subscribe
Subscribes to a YouTube channel. The request body accepts a raw channel ID, a channel handle (with or without @), or a YouTube channel URL:
{"channel_id":"https://m.youtube.com/@username"}
Success response:
{/* Channel object */}
POST /api/channels/:channel_id/unsubscribe
Stops future fetches for the channel. Existing videos remain available.
Success response:
{"msg":"unsubscribed from channel successfully"}
POST /api/channels/:channel_id/shorts?enable=true
Sets whether the channel includes Shorts. enable must be a boolean query parameter.
Success response:
{"msg":"shorts setting updated","enable_shorts":true}