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 Quote → Insight 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.json → clusters[].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 (1–50, 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:
HealthResponseArtifactListResponse/ArtifactItemIndexStatsEnvelope/IndexStatsBody/IndexRebuildAcceptedCorpusSearchApiResponse/SearchHitModel(optionalliftedon transcript rows when lift applies; optionalsupporting_quoteson insight rows — quote speaker fields follow GI segment/diarization rules, issue #541)ExploreApiResponse(insightsupporting_quotesspeaker fields follow GI segment/diarization rules, issue #541)CilArcEpisodeBlock/CilPositionArcResponse/CilPersonProfileInsightRow/CilPersonProfileQuoteRow/CilPersonProfileResponse/CilTopicTimelineResponse/CilTopicTimelineMergeRequest/CilTopicTimelineMergedResponse/CilIdListResponseCorpusResolveEpisodesRequest/CorpusResolveEpisodesResponse/CorpusResolvedEpisodeArtifactCorpusFeedsResponse/CorpusFeedItemCorpusEpisodesResponse/CorpusEpisodeListItemCorpusEpisodeDetailResponseCorpusSimilarEpisodesResponse/CorpusSimilarEpisodeItemCorpusDigestResponse/CorpusDigestRow/CorpusDigestTopicBand/CorpusDigestTopicHit(digest rows:summary_bullet_graph_topic_idsparallel tosummary_bullets_preview; topic bands:graph_topic_id—topic:{slug}from the band label for Graph focus)CorpusStatsResponse/CorpusRunsSummaryResponse/CorpusRunSummaryItemRelatedNodeModel/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.