Skip to content

HTTP API Reference

The optional FastAPI viewer server exposes a JSON HTTP API under the /api prefix, consumed by the Vue 3 GI/KG viewer. It is not part of the programmatic podcast_scraper library surface (see API index); install the [dev] extra and run it with make serve (see the Server Guide for running, architecture, and extension).

This page is the source-of-truth catalogue of endpoints and response models. The live, always-current spec is the server's own OpenAPI at /docs (Swagger UI) and /openapi.json.

Authentication

Local dev server: no auth. Treat production deployments as out-of-scope for this guide unless you add your own reverse proxy or middleware.

Endpoints

Method Path Tag Description Key query params
GET /api/health health Liveness and capability flags: status; core viewer artifacts_api, search_api, explore_api, index_routes_api, corpus_metrics_api, cil_queries_api (default true when mounted); catalog corpus_library_api, corpus_digest_api, corpus_binary_api. RFC-077 (default false): feeds_api, operator_config_api, jobs_api when those route groups are mounted. Omit digest flag on older builds → clients treat digest as unavailable.
GET, PUT /api/feeds feeds Read/write structured feeds.spec.yaml under the resolved corpus root (JSON { "feeds": [...] } on PUT). path
GET, PUT /api/operator-config operator_config Read/write viewer-safe operator YAML at the server-resolved path. GET returns content, operator_config_path, and available_profiles (union of packaged config/profiles/*.yaml names from cwd and repo roots, same as Config preset resolution; excluding *.example.yaml). When the file is missing or whitespace-only and the packaged preset cloud_balanced exists, GET writes a minimal profile: cloud_balanced file first (idempotent if already that content). PUT rejects forbidden secret keys and top-level feed keys (rss, rss_url, rss_urls, feeds) — use /api/feeds / feeds.spec.yaml for feeds. See RFC-077 (Phase 1b) and PRD-030. path
GET, POST /api/jobs jobs List (GET) or enqueue (POST) pipeline jobs for the corpus; GET /api/jobs/{id}, POST /api/jobs/{id}/cancel, POST /api/jobs/reconcile. POST supports per-feed scoping (P1.4): feed (RSS URL or feed-workspace-dirname slug; omit for full batch — resolved via feeds.spec.yaml, 404 on unknown slug), skip_existing (bool; skip episodes already present, keyed on stable episode_id/GUID), append (bool; episode_id-validated resume into a stable run_append_* dir), max_episodes (int ≥ 1; cap episodes this run), episode_offset (int ≥ 0; skip the newest N before selecting), episode_order (newest or oldest; 400 on any other value). All per-feed knobs require feed to be set. path, feed, skip_existing, append, max_episodes, episode_offset, episode_order
GET /api/scheduled-jobs scheduled-jobs List in-process cron schedules from viewer_operator.yaml scheduled_jobs: (#708). Each row carries name, cron, enabled, next_run_at (UTC ISO; null when disabled or invalid cron). Mounts only when enable_jobs_api is on. Operators add/remove schedules by editing the YAML via PUT /api/operator-config, which triggers a scheduler reload in-process. path
GET /api/artifacts artifacts List *.gi.json, *.kg.json, and *.bridge.json (recursive); each item includes mtime_utc (#507) and publish_date (YYYY-MM-DD from episode metadata when present, else UTC calendar day from file mtime). path (required) — corpus output directory
GET /api/artifacts/{path} artifacts Load and return a single artifact JSON by relative path. path (required) — corpus root for the relative lookup
GET /api/index/stats index LanceDB index stats, staleness heuristics, and rebuild job flags (rebuild_in_progress, rebuild_last_error; #507). path, embedding_model (optional; compare index to this id, else Config default)
POST /api/index/rebuild index Queue background index_corpus (202); mutex per corpus. Poll GET /api/index/stats. path, rebuild, embedding_model, vector_index_path, vector_index_types (comma-separated)
GET /api/search search Corpus search — hybrid (two-tier BM25 + dense via RRF over LanceDB, with compound results) when a search/lance_index exists (RFC-090), with sentence embeddings; reports no_index when no usable index is present (FAISS retired in #995, ADR-099). Transcript hits may include optional lifted: overlapping QuoteInsight plus speaker / topic display names from bridge.json when present (Semantic Search Guide — lift). Insight hits may include supporting_quotes (from indexer enrichment): quote speaker_id / speaker_name mirror .gi.json — often null / absent when segments lack diarization labels (GitHub #541; canonical rules: Development Guide — GI quote speaker_id). Successful responses include optional lift_stats: transcript_hits_returned and lift_applied for the returned page (after top_k). Each hit carries source_tier (insight / segment / aux, derived from doc_type — PRD-033 FR1.1); the response carries query_type (detected intent: entity_lookup / raw_evidence / temporal_tracking / cross_show_synthesis / semantic, from the RFC-090 rules router — FR1.4). Each call also appends a privacy-light event (timestamp + query_type, no raw text) to search/query_log.jsonl for the Dashboard activity chart. q (required), path, type, feed, since, speaker, grounded_only, top_k, embedding_model, dedupe_kg_surfaces (default true: merge same-text kg_entity / kg_topic rows)
GET /api/explore explore GI cross-episode explore (filter mode) or UC4 natural-language query. Insight rows may include supporting_quotes with speaker_id / speaker_name mirroring .gi.json (often absent without diarization — GitHub #541; Development Guide — GI quote speaker_id). path, question / q, topic, speaker, grounded_only, min_confidence, sort_by, limit, strict
GET /api/relational/* relational Relational-query layer (RFC-094 / #882) — read-only traversals over the typed cross-layer CorpusGraph (#874) that ground the PRD-033 Detail/Digest/Dashboard surfaces. All return a RelatedNode projection ({id, type, text, show_id, episode_id}); never raise (empty results on a missing id). Flat lists: /positions (person → insights they STATED, FR4.1), /insights-about (entity → insights that MENTION it), /entities-in (insight → entities it mentions), /topic-entities (topic → mentioned entities, ranked by frequency, FR4.2), /episodes (podcast → its episodes, HAS_EPISODE), /related-insights (insight → topic/entity siblings), /episode-insights (episode → siblings of its insights, FR4.3). Grouped ({key: [RelatedNode]}): /who-said (topic → insights per person, FR4.2), /cross-show (topic → top insight per distinct show — the corpus differentiator, FR3.2). positions / insights-about / related-insights re-rank results by an entity-scoped hybrid query over structural order (best-effort; degrades to structural order without an index). path; one of person / entity / insight / topic / podcast / episode (per route); k (cap); cross-show also per_show
GET /api/corpus/query-activity dashboard Daily search-volume counts (zero-filled, oldest→newest) from the append-only search/query_log.jsonl that /api/search writes (timestamp + intent only, no raw query text). Powers the Dashboard search-activity chart (PRD-033 FR6.2; honest scope — volume over time, not query-by-topic). path, days (1–365, default 30)
GET /api/persons/{person_id}/positions cil Position arc — chronological insights for a person and topic across episodes. Scans **/*.bridge.json with sibling GI/KG. Each insight carries sentiment: {compound, label} (joined from the insight_sentiment enricher by insight_id; absent when the sidecar is missing — best-effort, never errors). topic (required), path, insight_types (comma-separated; omit → claim only; all / * → no filter)
GET /api/persons/{person_id}/brief cil Person profile — insights grouped by topic plus quotes for that person. path
GET /api/persons/{person_id}/topics cil Distinct topic ids for that person (from brief keys). path
GET /api/topics/{topic_id}/timeline cil Topic timeline — insights about the topic per episode. Each episode may include episode_title, feed_title, episode_number, and artwork fields (episode_image_url, episode_image_local_relpath, feed_image_url, feed_image_local_relpath) from sibling *.metadata.json. Each insight carries sentiment: {compound, label} (from insight_sentiment, best-effort). path, insight_types (omit → all types; all / * → all)
GET /api/topics/{topic_id}/conversation-arc cil Topic conversation arc (ADR-108) — the aggregate-first "shape" of a topic's conversation over time: ISO-week buckets of insight volume × VADER sentiment mix ({week, volume, negative, neutral, positive, avg_compound}, oldest first). Rolls the topic timeline up by week to drive the weekly-bar surfaces; empty weeks when the topic has no dated insights. path, insight_types (comma-separated; omit → all types; all / * → all)
POST /api/topics/timeline cil Merged topic timeline — same Pattern C rules as GET, but one corpus scan for multiple topic_ids in the JSON body (cluster scope). Response includes topic_ids (deduped, canonical order) and episodes (merged per episode_id, insight nodes deduped). JSON body: topic_ids (required), path (optional if default output dir), insight_types (optional; same semantics as GET)
GET /api/topics/{topic_id}/persons cil Distinct person: ids that discuss the topic via grounded quotes. path
POST /api/corpus/resolve-episode-artifacts corpus Map logical episode_id values (from metadata) to corpus-relative gi_relative_path / kg_relative_path / bridge_relative_path from one catalog scan. Rows without GI on disk are listed in missing_episode_ids. The GI/KG viewer’s topic-cluster sibling auto-load builds candidate ids from topic_clusters.jsonclusters[].members[].episode_ids (union per touched cluster). JSON body: episode_ids (required), path (optional if server default set)
POST /api/corpus/node-episodes corpus Progressive graph expansion (cross-episode): for a canonical node_id (person:, org:, or topic:), scan *.bridge.json bundles (no full triple-read), then resolve sibling *.gi.json / *.kg.json by stem. Returns episodes (metadata-relative paths when present, optional episode_id, GI/KG flags) and truncated when the cap is hit. The GI/KG viewer uses this on eligible nodes (double-tap expand). JSON body: node_id (required), path (optional if server default set), limit (optional cap)
GET /api/corpus/feeds corpus Aggregate feeds from episode metadata under the corpus root. path (optional if server default set)
GET /api/corpus/episodes corpus Paginated episode list (newest-first scan); optional filters. Each item includes cil_digest_topics as an empty array (CIL pills are returned on digest rows and episode detail only, to avoid per-row bridge reads in the list). GI/KG path fields support graph loads after opening an episode. When topic_cluster_only=true, the server keeps rows whose bridge.json topic appears on a multi-member topic-cluster member that lists this episode's episode_id in members[].episode_ids (reads search/topic_clusters.json per request; pipeline-built clusters include those ids). path, feed_id, q (title substring), topic_q, since (YYYY-MM-DD), topic_cluster_only (boolean), limit (1–200), cursor
GET /api/corpus/episodes/detail corpus Episode row + summary bullets + GI/KG/bridge paths + cil_digest_topics (same shape as list / digest). path, metadata_relpath (required)
GET /api/corpus/episodes/similar corpus LanceDB semantic peers for an episode; 200 with error when index missing. path, metadata_relpath (required), top_k (1–25)
GET /api/corpus/digest corpus Feed-diverse recent episodes (metadata + GI/KG flags), optional cil_digest_topics per row (bridge + topic-cluster order; omitted when compact=true), and optional semantic topic bands. compact=true forces 24h, smaller cap, no topic bands and no CIL pill enrichment. path, window (24h / 7d / 1mo / since), since (required if window=since; 1mo = previous calendar month, UTC), compact, include_topics, max_rows
GET /api/corpus/topic-clusters corpus Topic clustering artifact: returns search/topic_clusters.json when present (404 with available: false when missing). path (optional if server default set)
POST /api/corpus/topic-clusters/rebuild operator Operator-gated (require_viewer_access). (Re)build search/topic_clusters.json off the current index without a full reindex; reuses the per-corpus rebuild gate. 202 accepted / 409 a rebuild is already running / 503 LanceDB unavailable. Poll GET /api/corpus/topic-clusters. path, threshold (default 0.75)
DELETE /api/corpus/runs/{run_id} operator Operator-gated rollback (fails closed without a credential). Move a whole run's episodes to <corpus>/.trash/<ts>/ (recoverable), re-aggregate corpus_manifest.json cost, then full index_corpus(rebuild=True) via the shared gate (topic-clusters recompute after). 202 with a summary + /api/index/stats poll pointer / 400 invalid run_id or missing confirm / 404 unknown run_id / 409 a rebuild is already running. path, confirm (must equal run_id), dry_run
DELETE /api/corpus/episodes/{episode_id} operator Operator-gated finer-grained rollback (for --append runs that share one run_append_<hash> dir): move just that episode's files (transcript/metadata/media/sidecars, all subdirs) to .trash/, re-aggregate, reindex. Same status codes as the run rollback. Note: GET /api/corpus/episodes (the consumer Library list) stays open — only the DELETE is operator-gated. path, confirm (must equal episode_id), dry_run
GET /api/corpus/text-file corpus Inline file under the corpus root for browser viewing (.txt, .md, .vtt, .srt, .json). Graph Quote node detail uses this for the in-app transcript viewer and the Open raw transcript in new tab header link. Pipeline runs that direct-download WebVTT/SubRip normalize to transcripts/… .txt (plus *.segments.json for GI timing); metadata usually points at that .txt. If relpath ends with .txt (and is not already *.cleaned.txt) and that file is missing, the server tries the sibling stem.cleaned.txt (metadata often still references the raw Whisper path). Post-#974, GI transcript_ref points at the ad-free base (stem.adfree.txt); when that is requested but absent (pre-#974 corpus), the server degrades to the raw stem.txt. path (optional if server default set), relpath (required)
GET /api/corpus/stats corpus Publish-month histogram (YYYY-MM → episode count) from one catalog scan; GI/KG Dashboard. path
GET /api/corpus/documents/manifest corpus Return corpus_manifest.json at corpus root (404 if missing). path
GET /api/corpus/documents/run-summary corpus Return corpus_run_summary.json at corpus root (404 if missing). path
GET /api/corpus/runs/summary corpus Discover run.json under the tree (capped), compact metrics per file for Dashboard. path
GET /api/corpus/coverage corpus GI/KG sibling-file presence per episode; aggregates by publish month and feed (Dashboard). path
GET /api/corpus/persons/top corpus Top speakers by grounded insight count (scans *.gi.json under catalog). path, limit
GET /api/app/artwork app Consumer platform: serve locally-stored podcast art (downloaded at ingest; never re-fetched from origin). size=large (original) or thumb (≤320px, generated on first request + cached); content-addressed → Cache-Control: immutable. 400 off the corpus-art store, 404 missing. #1078. ref (required), size
GET /api/app/episodes app Consumer platform: catalog list across the corpus, newest-first, via a pluggable ContentSource (LocalCorpusSource MVP; DiscoverySource extends it for #1069). {items[{slug, title, feed_id, podcast_title, publish_date, duration_seconds, episode_image_url, feed_image_url, artwork_url, status, summary_preview, summary_bullets[], topics[], has_*}], page, page_size, total, has_more}. summary_preview is a short clean lede (the summary title / first sentence — not the bullets joined); the full bullets are in summary_bullets[] (card expand-on-demand). RFC-099, #1078. page, page_size, status, feed_id
GET /api/app/podcasts/{feed_id}/episodes app Consumer platform: same shape as /api/app/episodes, scoped to one feed. #1078. page, page_size, status
GET /api/app/episodes/{slug} app Consumer platform: episode detail — slug, title, podcast title, publish date, duration, summary, and artifact-availability flags (has_transcript/summary/gi/kg/bridge). RFC-098, #1068. 404 for unknown slug.
GET /api/app/episodes/{slug}/insights app Consumer platform: grounded GIL insights with supporting quotes for one episode (RFC-049 projection: {id, text, grounded, insight_type?, confidence?, position_hint?, quotes[]}). Empty list (200) when the episode has no GI. #1068.
GET /api/app/episodes/{slug}/entities app Consumer platform: KG {persons[], orgs[], topics[]} for one episode (typed v2 or legacy Entity+kind). Empty when no KG. #1068.
GET /api/app/episodes/{slug}/related app Consumer platform: "more like this" semantic peer episodes via the vector index (reuses run_similar_episodes), projected to the catalog card shape. 200 + empty when the index is unavailable. #1084. top_k
GET /api/app/episodes/{slug}/segments app Consumer platform (read-only; access becomes auth-gated in later Epic-1 tasks): transcript segments.json contract {version, episode_slug, segments[{id, start, end, text, speaker?}]} for one episode addressed by stable slug (RFC-098 §4–§5, #1067). Reads *.segments.json (ad-free base preferred). 404 for unknown slug or missing transcript/segments. Serves the single shared corpus at output_dir; no path override.
GET /api/app/episodes/{slug}/audio-source app Consumer platform: resolve the origin enclosure URL the client plays directly — bridge, never rehost (RFC-100, #1070). Returns {episode_slug, url, mime?, duration_seconds?, media_id?, strategy:"direct", resolved_url?, verified?, content_length?} from content.media_url. 404 when no origin URL. With validate=true a HEAD follows redirects and reports resolved_url/verified/content_length (adds a network call); the no-store proxy stays deferred. validate
GET /api/app/episodes/{slug}/search app Consumer platform: episode-scoped grounded search — hybrid retrieval (RFC-090) over-fetched by feed and narrowed to this episode; extractive, no request-time LLM (D6). #1068. Same hit shape as /api/search. q (required), top_k
GET /api/app/search app Consumer platform: library-wide grounded search — whole shared corpus for now (scoped to the user's library once auth lands), extractive, no request-time LLM. Hit metadata enriched with episode_slug/episode_title/podcast_title for jump-to-moment, plus episode_artwork (thumb, remote-image fallback) so results render like library cards (Home, #1090). #1068. q (required), top_k, grounded_only
GET /api/app/podcasts app Consumer platform: distinct shows in the corpus for Home "Your shows" + the show page header (aggregate_feeds): {items[{feed_id, title, artwork_url, image_url, description, episode_count}]}. #1090.
GET /api/app/playback app Consumer platform (auth): all saved playback positions, newest-updated first, for Home "Continue listening". #1090.
GET /api/app/auth/login app Consumer platform auth (#1063, RFC-098 §2): begin the single-provider OAuth code flow — 307 redirect to the provider with a signed CSRF state cookie. 503 when auth is unconfigured (no APP_SESSION_SECRET / OAuth creds).
GET /api/app/auth/callback app Consumer platform auth: complete OAuth — verify state, exchange code, upsert the user (plain per-user files), set an HMAC-signed session cookie, 307 to /. 400 invalid state; 403 account not on the sign-in allowlist (#1064); 502 exchange failure. code, state
POST /api/app/auth/logout app Consumer platform auth: clear the session cookie (204).
GET /api/app/me app Consumer platform auth: the signed-in user's {user_id, email, name}; 401 when not authenticated. Gate dependency get_current_user for per-user routes.
GET, PUT /api/app/playback/{slug} app Consumer platform (auth, per-user files): playback position {slug, position_seconds, updated_at?}; GET returns 0 when unset (#1065).
GET, PUT /api/app/queue app Consumer platform (auth): the user's play queue — ordered episode slugs {items[]} (#1065).
GET, POST, DELETE /api/app/library (+ /{feed_id}) app Consumer platform (auth): subscribed podcasts — list / subscribe (idempotent on feed_id) / unsubscribe (#1065).
GET, PUT, DELETE /api/app/favorites (+ /{kind}/{ref}) app Consumer platform (auth): polymorphic saved items grouped by kind — {episodes[], insights[{ref, text, episode_slug?, podcast_title?, start_ms?}]} (AppFavoritesResponse). PUT saves {kind: episode\|insight\|person\|topic, ref, …} (idempotent on kind+ref); DELETE /{kind}/{ref} removes (ref URL-encoded).
GET, PUT /api/app/interests app Consumer platform (auth): the user's interest token list {items[]} (InterestsResponse) — a mixed token set (tc: clusters / topic: / person:); PUT replaces it (InterestsUpdate). Drives flag-gated personalized discovery (rank_discover). PRD-043 / RFC-102, #1098.
POST, DELETE /api/app/interests/{token} app Consumer platform (auth): follow / unfollow a single interest token (cluster tc: / topic topic: / person person:) — the entity-card Follow toggle. Idempotent; DELETE is a no-op when absent. Returns {items[]}. PRD-043 / RFC-102, #1097.
GET /api/app/clusters app Consumer platform: top interest clusters by corpus prevalence for the picker — {items[{id, label, size}]} (AppInterestClustersResponse). PRD-043 / RFC-102, #1098. limit (150, default 12)
POST /api/app/listen/{slug} app Consumer platform (auth): append one "episode opened" event to the user's append-only listen log (listen_events.jsonl) for analytics — 204, best-effort, never blocks playback. PRD-043 / RFC-102.
GET /api/app/me/stats app Consumer platform (auth): the signed-in user's own listening summary — {episodes, shows, listening_seconds, active_days, day_streak, daily[{date, count}]} (UserStatsResponse); daily is a 14-day opens sparkline. Computed from per-user playback + listen log (no DB, no LLM). PRD-043 / RFC-102.
GET /api/app/episodes/{slug}/stats app Consumer platform (public, no auth): anonymous cross-user reach — {slug, listeners, opens, insights, daily[{date, count}]} (EpisodeStatsResponse). Aggregate counts only (never identities), from scanning every user's listen log; zeroed when no APP_DATA_DIR. PRD-043 / RFC-102.

Design and response field semantics: Corpus Digest. Topic strings: repo config/digest_topics.yaml.

Response models

Pydantic response schemas are defined in schemas.py:

  • HealthResponse
  • ArtifactListResponse / ArtifactItem
  • IndexStatsEnvelope / IndexStatsBody / IndexRebuildAccepted
  • CorpusSearchApiResponse / SearchHitModel (optional lifted on transcript rows when lift applies; optional supporting_quotes on insight rows — quote speaker fields follow GI segment/diarization rules, issue #541)
  • ExploreApiResponse (insight supporting_quotes speaker fields follow GI segment/diarization rules, issue #541)
  • CilArcEpisodeBlock / CilPositionArcResponse / CilPersonProfileInsightRow / CilPersonProfileQuoteRow / CilPersonProfileResponse / CilTopicTimelineResponse / CilTopicTimelineMergeRequest / CilTopicTimelineMergedResponse / CilIdListResponse
  • CorpusResolveEpisodesRequest / CorpusResolveEpisodesResponse / CorpusResolvedEpisodeArtifact
  • CorpusFeedsResponse / CorpusFeedItem
  • CorpusEpisodesResponse / CorpusEpisodeListItem
  • CorpusEpisodeDetailResponse
  • CorpusSimilarEpisodesResponse / CorpusSimilarEpisodeItem
  • CorpusDigestResponse / CorpusDigestRow / CorpusDigestTopicBand / CorpusDigestTopicHit (digest rows: summary_bullet_graph_topic_ids parallel to summary_bullets_preview; topic bands: graph_topic_idtopic:{slug} from the band label for Graph focus)
  • CorpusStatsResponse / CorpusRunsSummaryResponse / CorpusRunSummaryItem
  • RelatedNodeModel / RelationalListResponse / RelationalGroupedResponse (relational-query layer, RFC-094 / #882)
  • QueryActivityResponse / QueryActivityBucket (search-activity, PRD-033 FR6.2)

Additional shipped endpoints

Rich catalog with response models, auth, and params — extracted from handler decorators and function signatures. Response-model class names resolve against server/schemas.py.

Method Path Response model Auth Params Purpose
GET /api/corpus/binary binary stream open path, relpath Serve a corpus-relative binary artifact (audio-thumbnail bytes etc.) with the right Content-Type.
GET /api/corpus/enrichments envelope list open path List available corpus-scope enricher envelopes (id + status metadata).
GET /api/corpus/enrichments/{enricher_id} envelope open path: enricher_id; query: path Full envelope for one corpus-scope enricher.
GET /api/corpus/episode/enrichments/{enricher_id} envelope open path: enricher_id; query: metadata_relpath, path Per-episode enricher output; identify the episode by its .metadata.json relative path.
GET /api/corpus/feed-signals CorpusFeedSignalsResponse open path, feed_id, top_k, max_episodes Feed-level signals for one show (topic distribution, cadence).
GET /api/corpus/media binary stream open path, relpath Locally-persisted audio bytes (RFC-096).
GET /api/corpus/theme-clusters envelope open path Co-occurrence theme clusters (topics discussed together).
GET /api/corpus/trending AppCorpusTrendingResponse open limit_per_kind Corpus-scope trending entities (RFC-103 momentum) — operator counterpart to /api/app/trending.
GET /api/index/timeseries IndexTimeseriesResponse open path Historical index-rebuild + staleness stats over time.
GET /api/jobs/subprocess-log plain text open job_id, path Full stdout+stderr of a pipeline job's subprocess.
GET /api/jobs/subprocess-log-tail PipelineJobLogTailResponse open job_id, path, max_bytes Tail-slice + optional live polling of a running job's subprocess log.
GET /api/jobs/{job_id}/log plain text open path: job_id; query: path Full log of the parent job (wraps subprocess log with job envelope).
GET /api/jobs/{job_id}/log-tail PipelineJobLogTailResponse open path: job_id; query: path, max_bytes Tail-slice of the parent job log.
GET /api/operator-config/profiles OperatorProfilesResponse open List packaged config/profiles/*.yaml presets available for the profile: key in viewer_operator.yaml.
GET /api/ops/summary ops summary JSON open Ops dashboard summary — costs, model spend, provider health.
POST /api/ops/resilience/reset 200 OK open scope (query) Force-reset a resilience breaker before its cooldown (ADR-113).
GET /api/resilience resilience-state JSON open Current resilience-layer state per provider (open breakers, blown fuses).
GET /api/topics/perspective-leaders CilTopicPerspectiveLeadersResponse open path, limit Corpus-wide "who talks most about X" leaderboard across topics.
GET /api/topics/{topic_id}/perspectives topic-perspectives JSON open path: topic_id; query: path, scope CIL topic-perspectives (operator equivalent of /api/app/topics/…/perspectives).
POST /api/search/compare SearchCompareResponse open JSON body: SearchCompareRequest Two-subject briefing pack (Search v3 compare operator, RFC-093).
GET /api/usage usage rollup JSON open window, provider Provider usage rollup — token / spend counters over the current window.

Relational sub-routes (the /api/relational/* row above is the catch-all; each is real and used): /positions, /insights-about, /topic-entities, /entities-in, /episodes, /related-insights, /insight-detail, /episode-insights, /who-said, /cross-show, /topics, /co-speakers, /related-topics. All return a RelatedNode projection (RelationalListResponse for flat lists, RelationalGroupedResponse for grouped shapes); each takes path + its scope key (person / entity / insight / topic / podcast / episode) plus an optional k cap.