← Docs

API Reference

Kairos exposes the same REST API that Hades itself is built on — sources, libraries, channels, scheduling, scrapers, discovery, chapters, and more. This is the surface to build against if you want to script Pantheon or write an alternate client. It's a large, actively-growing API; if something here looks off, the source of truth is always kairos/src/api/services/ in the repo.

💡 Base URL & auth. Every path below is relative to your Kairos host — direct (default :8081) or through Hermes' gateway (default :8000), which transparently proxies the entire /api/* surface to Kairos. Authenticate once with POST /api/auth/login, then send the returned token as Authorization: Bearer <token> on every subsequent request (a ?token= query param also works, for contexts that can't set headers — e.g. an <img> tag or an EventSource stream).
# Log in, grab a token, list channels
curl -sX POST http://your-server:8081/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"••••••••"}'
# → {"token":"...", "user": {"role":"admin", ...}}

curl -s http://your-server:8081/api/channels \
  -H 'Authorization: Bearer <token>'
GET read
POST create / action
PATCH partial update
DELETE remove
public no token required
auth any logged-in user
admin admin role required

Auth & Users #

MethodPathDescriptionAuth
GET/api/auth/setupWhether initial admin setup is still neededpublic
POST/api/auth/setupCreate the first admin accountbody: username, passwordpublic
POST/api/auth/loginBody: username, password → tokenpublic
POST/api/auth/logoutNo-op without a tokenpublic
GET/api/auth/meCurrent user profileauth
GET/api/usersList all usersadmin
POST/api/usersCreate a userbody: username, password, roleadmin
PATCH/api/users/:idChange password/rolebody: password, roleadmin
DELETE/api/users/:idBlocked for self-delete or the last remaining adminadmin
PATCH/api/users/:id/restrictionSet parental-control ceilingsbody: restricted, max_tv_rating, max_movie_rating, max_channel_ratingadmin
GET/api/users/:id/overridesPer-title allow/block overrides for a useradmin
POST/api/users/:id/overridesBody: entity_type (show/movie/channel), entity_id, mode (allow/block)admin
DELETE/api/users/:id/overrides/:entity_type/:entity_idRemove an overrideadmin
POST/api/auth/cast-tokenMint a viewer-capped token for Cast/relay handoffauth
GET/api/auth/sessionsList the caller's own device/Cast sessionsquery: purpose (default cast)auth
DELETE/api/auth/sessions/:idRevoke one of the caller's own sessionsauth

Sources & Libraries #

MethodPathDescriptionAuth
GET/api/sources/typesSupported source types (plex, jellyfin, emby, local)admin
GET/api/sourcesList configured sourcesadmin (GET also public, for internal service use)
POST/api/sourcesAdd a sourcebody: source_id, source_type, display_name, base_urladmin
POST/api/sources/testTest a connection before savingbody: source_type, base_url, token, user_idadmin
DELETE/api/sources/:idRemove a sourceadmin
GET/api/sources/:id/libraries/availableLibraries discoverable on the remote serveradmin
GET/api/sources/:id/fsBrowse local filesystem subdirectories (local source only)query: pathadmin
GET/api/sources/:id/librariesLibraries registered on this sourceadmin
POST/api/sources/:id/librariesRegister a librarybody: external_lib_id, display_name, library_type (show/movie/mixed), preferred_scraper, preferred_language, include_anidbadmin
PATCH/api/sources/:id/libraries/:lidUpdate a librarybody: display_name, library_type, preferred_scraper, preferred_language, include_anidb, skip_scraping, show_on_homeadmin
GET/api/sources/:id/libraries/:lid/scraper-priorityScraper priority orderquery: item_typeadmin
PUT/api/sources/:id/libraries/:lid/scraper-priorityBody: item_type, order[]admin
DELETE/api/sources/:id/libraries/:lidRemove a libraryadmin
POST/api/sources/:id/syncTrigger an incremental syncadmin
POST/api/sources/:id/hard-syncWipe source_mapping and resync from scratchadmin
GET/api/sources/:id/browse/playlistsRemote playlists visible on this sourceadmin
GET/api/sources/:id/browse/collectionsRemote collectionsquery: library_idadmin
GET/api/sources/:id/browse/playlists/:eidItems inside a remote playlistadmin
GET/api/sources/:id/browse/collections/:eidItems inside a remote collectionadmin
GET/api/sources/:id/sample-pathOne sample file path, for the Path Map UIadmin
GET/api/sync/statusWhether any sync is currently runningadmin

Config & Settings #

MethodPathDescriptionAuth
GET/api/config/settingsFull runtime settingsadmin
GET/api/config/public-settingsSubset of settings needed by Hephaestus/Hadespublic
PATCH/api/config/settingsBody: epg_debug, sync_debug, sync_threads, stream_buffer_size, image_cache_ttl_hours, verbose_transcode_logs, cast_app_idadmin
POST/api/config/epg/clear-allWipe all materialized schedule dataadmin
POST/api/logs/clientFrontend error reportingbody: level, messagepublic (internal: Hades)
POST/api/config/library/resetWipe the entire media index (keeps sources/channels/users)admin
GET/api/config/debug-dumpDownload a sanitized SQLite snapshot of library/matching tablesadmin
GET/api/config/credentialsPer-source token/user-id presence flags (not the values)admin
GET/api/config/credentials/:source_idSame, for one sourceadmin
PUT/api/config/credentials/:source_idBody: token, user_idadmin
DELETE/api/config/credentials/:source_idClear stored credentialsadmin
GET/api/config/path-maps/:source_idPath maps for a sourceadmin
PUT/api/config/path-maps/:source_idBody: maps: [{from, to}]admin
GET/api/tv/manifestLayout contract for native TV/mobile renderers (pantheon-android, Hades' own /tv route) — nav structure, filter-pills fields, and a theme block of style tokens generated from Hades' own design system so native clients stay visually in sync without duplicating hand-written token valuespublic

Arr Integration — Sonarr / Radarr #

MethodPathDescriptionAuth
GET/api/config/arrSonarr/Radarr connection configadmin
PATCH/api/config/arrBody: sonarr_url, sonarr_api_key, radarr_url, radarr_api_keyadmin
POST/api/arr/lookupSearch Sonarr/Radarr by title or external idbody: type (show/movie), title | tvdb_id | tmdb_id | imdb_idadmin
GET/api/arr/options/:typeQuality profiles + root foldersadmin
POST/api/arr/addPush a title to Sonarr/Radarrbody: type, add_dataadmin

Content Requests #

MethodPathDescriptionAuth
GET/api/requestsAdmin sees every request, viewer sees only their ownauth
POST/api/requestsRequest a piece of contentbody: content_type, source, external_id, title, year, poster_urlauth
PATCH/api/requests/:idApprove/reject, optionally auto-push to arrbody: status (approved/rejected), arr_add {type, add_data}admin
DELETE/api/requests/:idRemove a requestauth (owner or admin)

Channels #

MethodPathDescriptionAuth
GET/api/channelsList channels (parental-filtered)public (GET)
GET/api/channels/:id/access-checkParental-control gate before live playbackauth
POST/api/channelsBody: name, number, timezone, advance_modeadmin
PATCH/api/channels/:idBody: name, timezone, offline media, stream_resolution/bitrates, seed, content_tag, anchor_hashes, and moreadmin
DELETE/api/channels/:idRemove a channeladmin
GET/api/channels/:id/logoServes local file or redirects to a remote logo URLpublic
GET/api/channels/:id/exportExport channel config as JSONquery: depth (deep/shallow)admin
POST/api/channels/import/previewPreview an exported channel JSON before importingadmin
POST/api/channels/importImport a previously exported channeladmin
POST/api/channels/:id/fillerAdd a channel-level filler entrybody: content_type, content_id, advancement, weight, season_filteradmin
PATCH/api/channels/:id/filler/:eidBody: advancement, weightadmin
DELETE/api/channels/:id/filler/:eidRemove a filler entryadmin

Blocks, Content & Bumpers #

A block owns a recurring time window on a channel; content and filler/bumper entries hang off it.

MethodPathDescriptionAuth
GET/api/channels/:id/blocksList blocks on a channeladmin
POST/api/channels/:id/blocksFull block object: block_type, day_mask, start_time, priority, advancement, etc.admin
PATCH/api/channels/:id/blocks/:bid~25 optional fields — scheduling/filler/bumper tuningadmin
DELETE/api/channels/:id/blocks/:bidRemove a blockadmin
POST/api/channels/:id/blocks/:bid/contentAdd a show/movie/list to a blockadmin
PATCH/api/channels/:id/blocks/:bid/content/:cidBody: season_filter, position, weight, run_count, include_specials, episode_orderadmin
DELETE/api/channels/:id/blocks/:bid/content/:cidRemove a content entryadmin
DELETE/api/channels/:id/blocks/:bid/content/:cid/cursorReset a content item's playback cursoradmin
POST/api/channels/:id/blocks/:bid/playlistMaterialize block content into a Kairos playlist, optionally push to Plexbody: title, source_idadmin
POST/api/channels/:id/blocks/:bid/fillerBody: content_id, content_type, advancement, weight, season_filteradmin
PATCH/api/channels/:id/blocks/:bid/filler/:eidUpdate a block-level filler entryadmin
DELETE/api/channels/:id/blocks/:bid/filler/:eidRemove itadmin
GET/api/channels/:id/bumpersList bumpers on a channeladmin
POST/api/channels/:id/bumpersBody: content_type, content_id, mode (between/...), every_n, season_filteradmin
PATCH/api/channels/:id/bumpers/:bidUpdate a bumperadmin
DELETE/api/channels/:id/bumpers/:bidRemove a bumperadmin

Episode Groups & Show Specials Linking #

MethodPathDescriptionAuth
GET/api/shows/:id/groupsMulti-part episode groupingsauth
POST/api/shows/:id/groupsBody: name, group_typeadmin
DELETE/api/shows/:id/groups/:gidRemove a groupingadmin
POST/api/shows/:id/groups/:gid/membersBody: episode_id, part_numadmin
DELETE/api/shows/:id/groups/:gid/members/:midRemove a memberadmin
GET/api/shows/:id/grouping-candidatesAuto-detected multi-part groupings for one showauth
GET/api/grouping-candidatesSame, across every show with an unconfirmed candidateauth
POST/api/shows/:id/specials/scanScan the movie library for this show's OVA/specialadmin
GET/api/shows/:id/specials/candidatesCandidate matches awaiting confirmationadmin
GET/api/shows/:id/specialsSpecials already linked via episode.linked_movie_idadmin
POST/api/shows/:id/specials/candidates/:cid/acceptConfirm a linkadmin
POST/api/shows/:id/specials/candidates/:cid/rejectDismiss a candidateadmin

Shows, Movies & Episodes #

The largest single resource group — library browsing, metadata editing, matching state, and writeback.

MethodPathDescriptionAuth
GET/api/content/:type/:id/access-checkParental gate used before VOD starttype: movie | episode | showauth
GET/api/images/proxyFetch + cache any external image URLquery: urlpublic
POST/api/shows/:id/refresh-metadataRe-pull metadata from the confirmed scraper matchadmin
POST/api/movies/:id/refresh-metadataSame, for a movieadmin
PATCH/api/shows/:id/skip-scrapingBody: skip_scrapingadmin
PATCH/api/movies/:id/skip-scrapingBody: skip_scrapingadmin
PATCH/api/shows/:id/find-specialsOpt in/out of auto specials scanningbody: find_specialsadmin
PATCH/api/shows/:id/episode-display-orderBody: episode_display_order (season/aired)admin
GET/api/librariesList librariesauth
PATCH/api/libraries/:id/home-visibilityBody: show_on_homeadmin
GET/api/metadata/valuesDistinct values for a metadata field, for filter autocompletequery: field, type, library_idauth
GET/api/showsSearch/browse, paginated + parental-filteredquery: q, genre, year, content_rating, label, network, actor, country, collection, studio, sort, library_id, limit, offsetauth
GET/api/shows/:id/episodesQuery: seasonauth
GET/api/shows/:id/seasonsSeason listauth
GET/api/shows/:id/languagesProbed audio/subtitle languagesauth
GET/api/shows/:id/videoinfoProbed codec/resolution/bit-depthauth
GET/api/episodesSearchquery: show_id, q, season, limit, offsetauth
GET/api/moviesSearch/browse — same filter family as /api/showsauth
GET/api/shows/:idFull detail incl. every linked source (404 instead of 403 if restricted)auth
PATCH/api/shows/:idManual metadata edit, records a per-field overridebody: title, overview, studio, status, content_rating, genres, tags, labels, network, actors, countries, collections, year, imdb_id, tvdb_id, tmdb_id, thumb, artadmin
POST/api/shows/:id/writebackPush metadata back to Plex/Jellyfin — 403 unless the match is human-confirmedadmin
POST/api/shows/:id/mergeMerge a duplicate show into this onebody: duplicate_id, confirmadmin
GET/api/shows/:id/thumb, /artProxied poster/artauth
GET/api/episodes/:id/thumbProxied episode thumbnailauth
GET/api/movies/:idFull detailauth
PATCH/api/movies/:idSimilar field set to shows (incl. tags), minus season/networkadmin
POST/api/movies/:id/writebackSame confirmed-match gate as showsadmin
POST/api/movies/:id/mergeBody: duplicate_id, confirmadmin
GET/api/movies/:id/thumb, /artProxied poster/artauth
GET/api/movies/:id/languages, /videoinfoProbed language/video infoauth

Chapters #

Ad-break / intro-outro detection and manual chapter markers. Not yet consumed by the scheduler.

MethodPathDescriptionAuth
GET/api/chapters/reviewBrowse detected chapters for QA against the actual mediaquery: media_type, chapter_type, q, limit, offsetadmin
GET/api/episodes/:id/chapters, /api/movies/:id/chaptersChapters for an itemauth
POST/api/episodes/:id/chapters, /api/movies/:id/chaptersAdd a chapter manuallybody: type, start, end (see ChapterRepository)admin
PATCH/api/chapters/:idEdit a chapteradmin
DELETE/api/chapters/:idRemove a chapteradmin
POST/api/episodes/:id/chapters/sync, /api/movies/:id/chapters/syncRe-probe existing chapter markers from the source fileadmin
POST/api/shows/:id/chapters/syncSame, looped over every episodeadmin
POST/api/shows/:id/chapters/detect, /api/movies/:id/chapters/detectTrigger async ffmpeg-based scene/ad-break detection (202 accepted)admin
GET/api/chapters/detect/statusDetection job statusadmin
POST/api/chapters/:id/writebackStub — currently returns 501 Not Implementedadmin

Playlists #

Named, orderable item lists. Three independent axes on the same resource: static (explicit item list) vs smart (items periodically recomputed from a stored filter_expr — the same filter language as /api/shows//api/movies); linked-to-a-remote-list or not (Plex playlist/collection, or a generic Jellyfin/Emby playlist via source-sync); and shown as a Home page shelf or not (a shelf is just a smart playlist with show_on_home=1 — there's no separate shelf table). Distinct from the M3U/XMLTV IPTV feed (see Scheduler below).

MethodPathDescriptionAuth
GET/api/playlistsList playlists (admin — full editing internals)admin
GET/api/playlists/browseLightweight tile listing for the Library's Playlists section — title, mode, item_count, total_ms, poster_source, first few items for a client-side poster-collage fallbackauth
GET/api/home-playlistsEvery smart playlist with show_on_home=1 whose active window (home_active_start/end, MM-DD, empty = always) includes today, ordered by home_order — the extra Home-page shelves layered alongside the built-in onespublic
POST/api/playlists/plex-sync-all, /source-sync-all, /refresh-smart-allBulk: pull every Plex-linked list, every source-linked list, or recompute every smart playlist's items from its filter_expradmin
POST/api/playlistsBody: title, modeadmin
GET/api/playlists/:idDetail (admin — full editing internals incl. filter_expr)admin
GET/api/playlists/:id/itemsResolved item list — any authenticated viewerauth
GET/api/playlists/:id/resolve-play-targetResolves Play/Restart/Shuffle to a concrete playable item, honoring watch progressauth
POST/api/playlists/:id/plex-syncPull from a linked Plex listbody: source_id, external_id, plex_type (playlist/collection)admin
POST/api/playlists/:id/source-syncPull from a linked Jellyfin/Emby (or Plex) listbody: source_id, external_id, list_kindadmin
POST/api/playlists/:id/pushWriteback — create or reconcile the linked remote list to match this playlist's current items (adds/removes both directions)admin
POST/api/playlists/:id/refresh-smartRecompute this smart playlist's items from filter_expr now, instead of waiting for the regular sync-cycle passadmin
DELETE/api/playlists/:id/plex-link, /source-linkUnlink from the remote list (items are kept locally)admin
PATCH/api/playlists/:idBody: title, mode (sequential/show_collection), membership (static/smart), filter_expr, smart_type (show/movie), smart_sort, smart_limit, poster_source, show_on_home, home_order, home_tile_limit, home_active_start, home_active_endadmin
DELETE/api/playlists/:idRemove a playlistadmin
GET/api/playlists/:id/exportPortable JSON export of a playlist's definition + itemsadmin
POST/api/playlists/import, /import/previewImport a previously-exported playlist; preview first shows what would be created/matched without writingadmin
POST/api/playlists/:id/itemsBody: item_type, item_id (static playlists only)admin
POST/api/playlists/:id/items/bulkBody: items: [{item_type, item_id}]admin
DELETE/api/playlists/:id/items/:iidRemove an itemadmin
PATCH/api/playlists/:id/items/:iidReorderbody: positionadmin

Filler Lists #

Same CRUD shape as Playlists, as a separate resource used for channel gap-filling. All routes are admin-gated with the same body shapes as their Playlists equivalents.

MethodPathDescription
GET/api/filler-listsList filler lists
POST/api/filler-lists/plex-sync-all, /source-sync-allSync all linked lists
POST/api/filler-listsCreate a list
GET/api/filler-lists/:idDetail + items
POST/api/filler-lists/:id/plex-sync, /source-syncSync one list
DELETE/api/filler-lists/:id/plex-link, /source-linkUnlink from remote list
PATCH/api/filler-lists/:idUpdate
DELETE/api/filler-lists/:idRemove
POST/api/filler-lists/:id/itemsAdd an item
POST/api/filler-lists/:id/items/bulkAdd items in bulk
DELETE/api/filler-lists/:id/items/:iidRemove an item

Activity, Logs & Metrics #

MethodPathDescriptionAuth
POST/api/sync/allTrigger sync on every source (no admin check — any logged-in user)auth
POST/api/sync/all-hardForce a hard sync on every sourceadmin
GET/api/logs/streamServer-Sent-Events log tailpublic
GET/api/system/metricsCPU/RAM for this process + hostpublic

Downloads — yt-dlp #

MethodPathDescriptionAuth
GET/api/config/downloadConfigured download destination pathadmin
PUT/api/config/downloadBody: pathadmin
POST/api/download/jobsStart a yt-dlp jobbody: url, pathadmin
GET/api/download/jobsList jobs with progress/logadmin

Scrapers & Matching #

MethodPathDescriptionAuth
GET/api/scrapers/configScraper configurationadmin
PATCH/api/scrapers/configBody: match_threshold, configs: [{source, api_key, language, enabled, language_weight, pin}]admin
POST/api/scrapers/matchTrigger a match pass (202 async, or already_running)body: target_id, item_typeadmin
GET/api/scrapers/match/statusMatch job statusadmin
GET/api/scrapers/statsCounts by match_statusadmin
GET/api/scrapers/queueReview queue with candidatesquery: status, limit, offsetadmin
POST/api/scrapers/queue/:kairos_id/manual-matchBody: item_type, source, external_id, title, year, poster_url, overviewadmin
POST/api/scrapers/queue/:id/acceptConfirm a candidate matchadmin
POST/api/scrapers/queue/:id/rejectReject a candidate matchadmin
GET/api/scrapers/anidb/poster/:aidRedirects to the image proxypublic
GET/api/scrapers/searchDirect scraper search — powers the Discovery featurequery: q, typepublic
GET/api/scrapers/metadata/:item_type/:kairos_idExternal IDs + alternate titlesauth
POST/api/scrapers/metadata/:item_type/:kairos_idBody: external_ids[], alternate_titles[]admin
POST/api/scrapers/metadata/:item_type/:kairos_id/refreshRe-fetch alternate titles/external idsadmin

Scheduler, EPG & IPTV Feed #

The endpoints DVR clients (XTeve, TiViMate, Plex Live TV) actually consume, alongside the EPG preview/materialization API Hades uses.

MethodPathDescriptionAuth
GET/playlist.m3uThe IPTV M3U playlistpublic
GET/api/media/languagesLibrary-wide distinct audio/subtitle languages (sampled, cached 1h)public
GET/epg.xml, /api/epg.xml, /api/xmltv.xml, /api/channels.xmlAll four alias the same XMLTV generatorquery: hours (max 72)public
GET/api/channels/:id/nowWhat's playing right now, with filler/offline fallbackquery: atpublic
GET/api/channels/:id/nextWhat plays nextpublic
POST/api/channels/:id/playedReport playback completion, advances the cursorbody: item_type, item_id, block_id, duration_actual_mspublic
POST/api/channels/:id/epg/previewPreview a schedule without committing — supports draft blocks + a seed overridebody: hours, seed, blocks[]auth
POST/api/channels/:id/epg/clearClear cached preview/materializationauth
GET/api/channels/:id/epgMaterialized EPG gridquery: hours, frompublic

Timeslots #

Fixed-time rotating slots inside a block (e.g. a 7pm "Toonami" slot). Every route here relies solely on the global Bearer-token requirement — none currently add an explicit admin-role check, unlike the rest of the scheduling API.

MethodPathDescriptionAuth
POST/api/blocks/:bid/slotsCreate a timeslotauth
PATCH/api/blocks/:bid/slots/:sidBody: slot_offset_mins, slot_duration_mins, overflow, late_start_mins, early_start_secs, align_to_mins, start_scopeauth
DELETE/api/blocks/:bid/slots/:sidRemove a slotauth
POST/api/blocks/:bid/slots/reorderBody: [{slot_id}]auth
POST/api/blocks/:bid/slots/:sid/cursor/resetReset the slot's rotation cursorauth
POST/api/blocks/:bid/slots/:sid/queueAdd a rotating show/movie to the slotauth
PATCH/api/blocks/:bid/slots/:sid/queue/:eidBody: premiere_date, pre_premiere_behaviorauth
DELETE/api/blocks/:bid/slots/:sid/queue/:eidRemove from the rotationauth
POST/api/blocks/:bid/slots/:sid/queue/reorderBody: [{entry_id}]auth

Roku Devices #

Pairing and management for the native pantheon-roku channel.

MethodPathDescriptionAuth
GET/api/roku-devicesCaller's own paired devicesauth
POST/api/roku-devicesCreate + start an ECP-launch pairing flowbody: name, ip_addressauth
GET/api/roku-devices/:idOwn devices onlyauth
DELETE/api/roku-devices/:idUnpair a deviceauth
POST/api/roku-devices/:id/confirm-pairingConfirm pairingbody: pairing_tokenauth (internal: relayed by Hermes from the Roku device itself)

Playback & Watch Progress #

MethodPathDescriptionAuth
GET/api/playback/:content_type/:idResolve a library item to a playable file path — also resolves linked specials through their moviepublic (internal: called by Hephaestus)
GET/api/watch-progressCaller's continue-watching listquery: limitauth
PUT/api/watch-progress/:content_type/:idUpsert position, auto-clears at ≥95% watchedbody: position_ms, duration_msauth
DELETE/api/watch-progress/:content_type/:idClear saved progressauth