Feeds
Podcast feed management
GET /feeds
Section titled “GET /feeds”List all feeds
Returns configured podcast feeds with metadata. Omitting page and limit still returns every feed, with pagination fields describing that unbounded page; passing either one switches to a paginated page. Set includeLatestEpisodes=true to add a bounded latestEpisodes projection to each feed, computed in one query for the whole page rather than one request per feed.
Parameters
Section titled “Parameters”page(query, integer): Page number (1-based). Only takes effect when page or limit is set; otherwise every feed is returned.limit(query, integer): Feeds per page. Only takes effect when page or limit is set.includeLatestEpisodes(query, boolean): When true, add a latestEpisodes projection to each feed.episodesPerFeed(query, integer): Maximum episodes per feed in latestEpisodes. Clamped to 1-20.sortBy(query, string, one ofrecent,title): Orders the whole feed list before it is sliced into a page, so a page is a slice of the sorted list rather than a sorted slice. recent orders by the feed’s newest episode, title alphabetically (case-insensitive). Any other value is a 400.sortDir(query, string, one ofasc,desc): Sort direction. Defaults to desc for recent and asc for title. Any other value is a 400.
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | List of feeds | object |
| 400 | Unknown sortBy or sortDir |
200 response body
Section titled “200 response body”feeds(array of Feed)total(integer): Total feed count.totalPages(integer)page(integer)limit(integer)offset(integer)lastRefreshCompletedAt(string or null (date-time)): When an all-feeds refresh pass last finished (15-minute scheduler or the manual Refresh All action). Null until the first pass completes.
POST /feeds
Section titled “POST /feeds”Add a new feed
Add a new podcast feed. Three shapes: a subscribed feed (default), which requires sourceUrl and fetches/parses it before saving; a local (archive-import) feed, set with feedType: “local”, which takes no sourceUrl and no upstream is ever fetched: metadata comes entirely from the request body; or the single combined recents feed, feedType: “recents”, served at /recents with a fixed slug, taking only title and description (409 when one already exists). The local branch still validates slug (or derives one from title) but skips URL/SSRF validation since there is no URL.
Request body (application/json, required)
Section titled “Request body (application/json, required)”downloadUserAgentOverride(string or null): Per-feed download User-Agent. Printable ASCII only. Null or blank inherits downloadUserAgent.feedUserAgentOverride(string or null): Per-feed RSS User-Agent. Printable ASCII only. Null or blank inherits feedUserAgent.sourceUrl(string (uri)): URL of the source RSS feed. Required unless feedType is “local”.slug(string): Optional custom slug (auto-generated if not provided). Immutable once the feed is created.feedType(string, one ofsubscribed,local,recents,youtube): “local” creates an archive-import feed with no upstream RSS: title is required, sourceUrl is ignored, and no fetch happens. “youtube” creates a feed from a YouTube playlist or channel: sourceKind, sourceId and title are required, sourceUrl is ignored, and the first listing runs before the response. Omit or “subscribed” for a normal feed. Defaultsubscribed.sourceKind(string, one ofplaylist,channel): YouTube feeds only. Kind of the source.sourceId(string): YouTube feeds only. Canonical playlist id or channel id, as returned by the preview.artworkUrl(string (uri)): YouTube feeds only. Cover image downloaded once at creation.includeShorts(boolean or null): YouTube feeds only. Whether videos of 180 seconds or less are listed; null follows the default.autoProcessOverride(boolean or null): Per-feed auto-process override applied at creation. Null inherits the global setting.title(string): Feed title. Required and editable when feedType is “local”; ignored for a subscribed feed, which takes its title from the upstream RSS on refresh.description(string): Feed description. Local feeds only; ignored for a subscribed feed.author(string or null): itunes:author. Local feeds only.explicit(boolean or null): itunes:explicit. Local feeds only; null means unset.categories(array of string): itunes:category list. Local feeds only; null clears.p20(LocalFeedP20)maxEpisodes(integer or null): Per-feed cap on served episode count. Subscribed feeds: clamped to 10-500; omit or set null to fall back to the global maxFeedEpisodes setting (default 300). Local feeds: 0, null, or omitted serves every episode (the default); a positive value up to 10000 serves only the newest N.onlyExposeProcessedEpisodes(boolean or null): Per-feed override. true hides non-processed episodes; false shows everything; null falls back to the global onlyExposeProcessedDefault setting.processingMode(string, one ofstandard,keep_content,skip_detection,passthrough): Processing mode applied before the first refresh. Subscribed and YouTube feeds only; local and recents feeds reject it with a 400. Omitted means standard. cue_only is rejected on create because it needs cue templates; set it from PATCH /feeds/{slug} afterwards.requireManualApproval(boolean or null): Per-feed override. true keeps newly processed episodes out of the served feed until approved; false publishes them at once; null falls back to the global requireManualApprovalDefault setting.retentionDaysOverride(integer or null): Per-feed retention override. Null falls back to the global retentionDays setting; 0 archives the feed so processed audio is never deleted, including by the force-all cleanup; 1-3650 keeps it for that many days.audioReplacementSoundOverride(boolean or null): Override replacement sound for this feed; null inherits the global choice.audioMp3StreamCopyOverride(boolean or null): Override MP3 stream copy for this feed; null inherits the global choice.keepOriginalAudioOverride(boolean or null): Per-feed override for retaining the pre-cut original audio. Null falls back to the global keepOriginalAudio setting; true always keeps it; false never does. Applies from the next episode processed.
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 201 | Feed created successfully | object |
| 400 | Bad request - invalid input | Error |
| 409 | Feed with slug already exists | Error |
201 response body
Section titled “201 response body”slug(string)sourceUrl(string)feedUrl(string)message(string)
POST /feeds/import-opml
Section titled “POST /feeds/import-opml”Import feeds from OPML file
Import podcast feeds from an OPML file upload. Accepts .opml or .xml files. Automatically generates slugs from feed titles and refreshes the first 5 imported feeds.
Request body (multipart/form-data, required)
Section titled “Request body (multipart/form-data, required)”opml(string (binary), required): OPML file (.opml or .xml)
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | No feeds were imported (all skipped or failed) | |
| 201 | Feeds imported successfully | object |
| 400 | Invalid OPML file or no file provided | Error |
201 response body
Section titled “201 response body”imported(integer): Number of feeds successfully importedskipped(integer): Number of feeds skipped (already exist)failed(integer): Number of feeds that failed to importfeeds(object)feeds.imported(array of object)feeds.imported[].url(string)feeds.imported[].slug(string)feeds.skipped(array of object)feeds.skipped[].url(string)feeds.skipped[].slug(string)feeds.skipped[].reason(string)feeds.failed(array of object)feeds.failed[].url(string)feeds.failed[].error(string)
GET /feeds/export-opml
Section titled “GET /feeds/export-opml”Export feeds as OPML file
Export all subscribed podcast feeds as an OPML XML file. Use mode=original for upstream source URLs (backup/migration). Use mode=modified for MinusPod-served ad-free feed URLs (import into podcast apps).
Parameters
Section titled “Parameters”mode(query, string, one oforiginal,modified): URL mode: “original” for upstream source URLs, “modified” for MinusPod ad-free feed URLsslugs(query, string): Optional comma-separated feed slugs to include (in the usual feed order). Unknown slugs are skipped. Omit to export every feed.
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | OPML file download | application/octet-stream |
| 400 | Invalid mode parameter, or slugs matched no feeds | Error |
POST /feeds/export-opml
Section titled “POST /feeds/export-opml”Export selected feeds as an OPML file
Export selected feeds in the usual feed order. Unknown slugs are skipped; the request fails if none match.
Parameters
Section titled “Parameters”mode(query, string, one oforiginal,modified): URL mode: “original” for upstream source URLs, “modified” for MinusPod ad-free feed URLs
Request body (application/json, required)
Section titled “Request body (application/json, required)”slugs(array of string, required)
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | OPML file download | application/octet-stream |
| 400 | Invalid mode or slug list, or slugs matched no feeds | Error |
GET /opml/{mode}.opml
Section titled “GET /opml/{mode}.opml”OPML feed list for podcast apps’ import-from-URL
App-level, key-gated OPML export served from the public feed domain so podcast apps can import feeds directly by URL. Requires authenticated feeds to be enabled and a valid feed key; returns 404 when feed auth is disabled (the modified list is a bundle of keyed feed URLs and is never served unauthenticated). The copyable URLs are surfaced by GET /settings as opmlModifiedUrl / opmlOriginalUrl.
Parameters
Section titled “Parameters”mode(path, string, one ofmodified,original, required): modified = MinusPod ad-free feed URLs; original = upstream source URLskey(query, string, required): The global feed auth key
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | OPML document | text/xml |
| 401 | Missing or invalid feed key | |
| 404 | Feed auth disabled, or unknown mode |
GET /feeds/{slug}
Section titled “GET /feeds/{slug}”Get a single feed
Returns details for a specific podcast feed
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifier
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Feed details | Feed |
| 404 | Resource not found | Error |
PATCH /feeds/{slug}
Section titled “PATCH /feeds/{slug}”Update feed settings
Update podcast feed settings like network override, DAI platform, processing overrides, and the source RSS URL. The recents feed accepts only title and description; any other field returns 400.
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifier
Request body (application/json, required)
Section titled “Request body (application/json, required)”sourceUrl(string (uri)): Replacement source RSS URL for this feed. The server fetches and parses the URL before persisting; a URL that does not return a parseable RSS feed is rejected with 400 and nothing is saved. On success the stored ETag/Last-Modified validators are cleared and an immediate forced refresh runs. Existing episodes are kept (matched by GUID). Cannot be null or empty.networkIdOverride(string or null): Network to assign this feed to: a known network id, or a custom name that links feeds sharing the same value (null uses the auto-detected network). The effective network is the override when set, else the auto-detected one.daiPlatform(string): DAI platform identifier (e.g., megaphone, acast)autoProcessOverride(boolean or null): Override auto-process setting (null uses global)autoProcessMaxAgeHoursOverride(integer or null): Per-feed auto-process window in hours (null uses the global autoProcessMaxAgeHours)downloadUserAgentOverride(string or null): Audio, artwork, chapters, and transcript User-Agent for this feed. Printable ASCII only. Null or blank inherits downloadUserAgent. Cross-fetch uses a different podcast client.feedUserAgentOverride(string or null): RSS fetch User-Agent for this feed. Printable ASCII only. Null or blank inherits feedUserAgent.languageOverride(string or null): Per-feed transcription language. ‘auto’ pins auto-detect for this feed; a language code (e.g. ‘en’, ‘de’) forces that language; null falls back to the global whisperLanguage setting.titleOverride(string or null): Per-feed display title shown to subscribers in the generated RSS feed (issue 375). Survives RSS refreshes. Null or empty clears it and falls back to the source feed title.descriptionOverride(string or null): Per-feed description shown to subscribers in the generated RSS feed. Survives RSS refreshes. Line breaks are kept. Null or empty clears it and falls back to the source feed description.detectionNotes(string or null): Operator hints about this show appended to the LLM ad-detection and review prompts (issue 709). Survives RSS refreshes. Null or empty clears it.chapterNotes(string or null): Per-feed notes sent as a final Feed notes paragraph with every chapter generation request. Null or blank clears it.detectionMode(string or null, one ofblacklist,keep_content,null): Per-feed ad-detection mode. ‘keep_content’ (experimental) has the LLM label show content and removes everything else; null or ‘blacklist’ is the default, which labels and removes ads. Prefer processingMode; kept for per-field compatibility.chaptersMode(string or null, one ofauto,generate,off,null): Per-feed chapters mode. ‘auto’ preserves publisher-embedded chapters when enough survive the cut, otherwise generates them; ‘generate’ always generates new chapters; ‘off’ skips the chapter step entirely; null follows the global chaptersMode setting.chaptersInNotes(string or null, one ofon,off,null): Per-feed override for listing generated chapters in served episode descriptions. null follows the global chaptersInNotes setting.reviewLinksInNotes(string or null, one ofon,off,null): Per-feed override for review and rate links in served episode descriptions. null follows the global reviewLinksInNotes setting.adChaptersEnabled(boolean or string or null, one oftrue,false,on,off,null): Deprecated. Translates into segmentCategoryActions; false (or ‘off’) demotes every mark to keep. true, ‘on’, and null are accepted and ignored (null is a no-op, matching the old per-feed override contract).adChapterCategories(map of boolean): Deprecated. Translates into segmentCategoryActions; true promotes keep to mark, false demotes mark to keep; remove and beep stay unchanged. null is a no-op.queuePriority(string or null, one ofhigh,normal,low,null): Per-feed auto-process queue priority (issue 625). ‘high’ processes this feed’s episodes before other queued episodes; ‘low’ runs only when nothing else is waiting; ‘normal’ or null uses the default ordering. Changing it re-stamps this feed’s already-queued pending episodes immediately.lowAdYieldAction(string or null, one ofnothing,redetect,reprocess,full,null): Per-feed override of the global lowAdYieldAction setting; null uses the global value. Automatic response when a pipeline-initiated run removes far less ad time than the feed’s recent average: ‘nothing’ (default), ‘redetect’ (LLM-only rerun from the stored transcript), ‘reprocess’, or ‘full’. Fires at most once per episode.episodeLogs(string or null, one ofon,off,null): Per-feed run log storage (issue 660): ‘on’ stores each run’s pipeline log, ‘off’ never does, null follows the global setting. Nothing is stored while episodeLogRetentionDays is 0.titleSkipPatterns(array of string): Per-feed episode title blacklist. Each pattern is matched against the whole episode title, case-insensitively (fnmatch glob); use ‘*’ as a wildcard, e.g. ‘Bonus Episode ’ matches any title starting with ‘Bonus Episode’. Without a wildcard the whole title must match, so a plain substring needs surrounding ‘’ wildcards. A matching episode is never queued from RSS refresh or JIT-processed; requests for a skipped episode’s audio redirect to the upstream enclosure instead. Null or an empty array clears the blacklist.descriptionTrimPattern(string or null): Chapter title glob for the chapter trim. Matched against the title of each timestamp line in the episode description and against the episode’s chapters (upstream or MinusPod-generated), case-insensitively, with ‘*’ as a wildcard. A match cuts from that timestamp per descriptionTrimMode. Null clears the rule.descriptionTrimMode(string, one ofto_end,to_next): to_end (default) cuts to the end of the episode; to_next cuts until the next timestamp line.descriptionSkipPatterns(array of string): Per-feed episode description blacklist (issue 835). Each pattern is matched against the plain-text description (HTML tags removed, whitespace collapsed), case-insensitively (fnmatch glob); use ‘’ as a wildcard, e.g. ‘This is a preview. To hear the entire episode’. Without a wildcard the whole description must match, so a plain substring needs surrounding ‘’ wildcards. A matching episode is never queued from RSS refresh or JIT-processed; requests for a skipped episode’s audio redirect to the upstream enclosure instead. Null or an empty array clears the blacklist.titleSkipAction(string or null, one ofserve_original,hide,null): Served-RSS handling for an episode skipped by title, description, or duration. ‘serve_original’ (default, null) keeps original audio available; ‘hide’ removes the entry.minDurationSeconds(number or null): Minimum publisher RSS duration in seconds, inclusive. Null clears this limit; omission preserves it. The effective minimum cannot exceed the maximum. Unknown or zero durations remain eligible; manual reprocessing overrides the filter.maxDurationSeconds(number or null): Maximum publisher RSS duration in seconds, inclusive. Null clears this limit; omission preserves it. The effective maximum cannot be below the minimum. Filtering uses RSS duration before ad removal.cueTemplateScoreOverride(number or null): Per-feed ZNCC match threshold for audio cue template matching. Overrides the global audioCueTemplateScore setting for this feed. null or omitted clears the override and falls back to the global setting.cueCreateFromPairsOverride(boolean or null): Per-feed override for cue-pair ad synthesis. true forces on, false forces off, null inherits the global audio_cue_create_from_pairs setting.cueCutMatchesOverride(boolean or null): Per-feed override for cutting matched cue audio. true forces on, false forces off, null inherits the global audio_cue_cut_matches setting.cuePairMinBreakOverride(number or null): Per-feed minimum break duration (seconds) for cue-pair synthesis. null inherits the global setting.cuePairMaxBreakOverride(number or null): Per-feed maximum break duration (seconds) for cue-pair synthesis. null inherits the global setting.cuePairMaxBreakFractionOverride(number or null): Per-feed maximum break fraction of episode duration for cue-pair synthesis. null inherits the global setting.cueSnapConfidenceOverride(number or null): Per-feed minimum cue confidence to snap an ad edge. null inherits the global audio_cue_snap_confidence setting.cueSnapLeadOverride(number or null): Per-feed snap lead window (seconds) before an ad edge. null inherits the global audio_cue_snap_lead_seconds setting.cueSnapLagOverride(number or null): Per-feed snap lag window (seconds) after an ad edge. null inherits the global audio_cue_snap_lag_seconds setting.silenceSnapEnabled(boolean or null): Per-feed opt-in for snapping cut boundaries to nearby silence. Simple flag, not tri-state; null and false both read as off.transitionSnapEnabled(boolean or null): Per-feed opt-in for snapping cut boundaries to content-transition cues. Simple flag, not tri-state; null and false both read as off.spliceVetoEnabled(boolean or null): Per-feed splice check. Tri-state: null inherits the global spliceVetoEnabled setting, true forces the check on, false lets long cuts through without splice evidence.maxAdDurationOverride(number or null): Per-feed max ad duration cap in seconds. Ads longer than this are held for review instead of cut. null means no cap (global MAX_AD_DURATION constants apply). Also raises the longest single detection the model may report for this feed (the global Longest single LLM detection, default 7 minutes).adDetectionExcludeStartOverride(number or null): Per-feed opening window in seconds during which ad markers are ignored. null inherits adDetectionExcludeStartSeconds, 0 disables it for the feed, 1 to 600 sets the feed’s own window.maxAdDurationRejectOverride(number or null): Per-feed length past which an ad must name a recognized sponsor to be cut; one that does not is held for review. Must not exceed the global maxAdDurationConfirmedSeconds. null falls back to the global maxAdDurationSeconds setting. Also raises the longest single detection the model may report for this feed (the global maxLlmWindowAdSeconds, default 7 minutes), so set it to the length of the show’s longest continuous ad block.cueGatedApproval(boolean or null): Per-feed cue-gated approval. When true, only ads with audio cue evidence auto-cut; others are held for review. null and false both read as off.differentialFetchEnabled(boolean or null): Legacy per-feed cross-fetch flag. Use differentialFetchMode for new clients; null selects automatic DAI detection.processingMode(string, one ofpassthrough,skip_detection,keep_content,standard,cue_only): Writable processing-mode preset. Sets passthroughEnabled, skipAdDetection, and detectionMode together. Cannot be combined with those fields in one request. cue_only requires at least one enabled ad_break_start and one enabled ad_break_end template on the feed; rejected with a 400 otherwise, since a boundary-only cue cannot pair safely without the LLM pass.passthroughEnabled(boolean or null): When true, episodes on this feed are downloaded and served exactly as published, with no transcription, detection, or cutting. Null and false both mean normal processing. Prefer processingMode; kept for per-field compatibility.skipAdDetection(boolean or null): When true, episodes on this feed are transcribed and get chapters and a transcript, but ad detection is skipped and nothing is cut. For ad-free shows. Null and false both mean normal processing. Prefer processingMode; kept for per-field compatibility.cueOnlySafety(string or null, one ofhold_new,auto_cut,null): Per-feed safety policy for cue_only mode. ‘hold_new’ (default) holds a template’s synthesized cuts for review until it has matched a paired start/end cue on 3 episodes. ‘auto_cut’ cuts immediately but still holds any pair scoring below 0.90 confidence. null resets to the default ‘hold_new’ behavior. Ignored outside cue_only mode.skipTranscription(boolean or null): Per-feed opt-out from transcription. Only valid on a feed already in cue_only mode; rejected with a 400 otherwise. Generated chapters stop, and transcript search and VTT subtitles become unavailable for those episodes, but publisher chapters (embedded ID3 or linked Podcasting 2.0 JSON) still get remapped onto the cut audio. Null and false both mean transcription still runs.maxEpisodes(integer or null): Per-feed cap on served episode count. Subscribed feeds: clamped to 10-500; set null to fall back to the global maxFeedEpisodes setting. Local feeds: 0 or null serves every episode (the default); a positive value up to 10000 serves only the newest N.onlyExposeProcessedEpisodes(boolean or null): Per-feed override. true hides non-processed episodes; false shows everything; null falls back to the global onlyExposeProcessedDefault setting.requireManualApproval(boolean or null): Per-feed override. true keeps newly processed episodes out of the served feed until approved; false publishes them at once; null falls back to the global requireManualApprovalDefault setting.retentionDaysOverride(integer or null): Per-feed retention override. Null falls back to the global retentionDays setting; 0 archives the feed so processed audio is never deleted, including by the force-all cleanup; 1-3650 keeps it for that many days.audioReplacementSoundOverride(boolean or null): Override replacement sound for this feed; null inherits the global choice.audioMp3StreamCopyOverride(boolean or null): Override MP3 stream copy for this feed; null inherits the global choice.keepOriginalAudioOverride(boolean or null): Per-feed override for retaining the pre-cut original audio. Null falls back to the global keepOriginalAudio setting; true always keeps it; false never does. Applies from the next episode processed.ytProcessDelayMinutesOverride(integer or null): YouTube feeds only. Minutes to wait after a video is published before processing. Null follows the global setting.ytProbeAttemptsOverride(integer or null): YouTube feeds only. SponsorBlock lookup attempts. Null follows the global setting.ytProbeIntervalMinutesOverride(integer or null): YouTube feeds only. Minutes between SponsorBlock lookup attempts. Null follows the global setting.ytLlmFallbackOverride(boolean or null): YouTube feeds only. Run LLM detection when SponsorBlock has no data. Null follows the global setting.ytLlmOnTopOverride(boolean or null): YouTube feeds only. Run LLM detection on top of SponsorBlock segments. Null follows the global setting.ytCaptionsOverride(boolean or null): YouTube feeds only. Use YouTube captions as the transcript. Null follows the global setting.ytHidePrivateOverride(boolean or null): YouTube feeds only. Hide private and deleted episodes from the listing. Null follows the global setting.ytTranscribeOverride(boolean or null): YouTube feeds only. Transcribe with the configured transcriber when captions are missing. Null follows the global setting.youtubeIncludeShorts(boolean or null): YouTube feeds only. Whether videos of 180 seconds or less are listed. Null follows the default.segmentCategoryActions(map of string, one ofremove,beep,keep,mark): Per-feed segment-category action overrides (issuedetectShowSegments(boolean or null): Per-feed opt-in to detect intro, outro, and recap/housekeeping segments in addition to the always-detected ad-side categories. true/false override the global detectShowSegments setting for this feed; null falls back to it.skipSecondPass(boolean or null): When true, the pass-2 verification scan of the cut audio is skipped. Null follows the global skipSecondPass setting.transcriptDifferential(boolean or null): Per-feed override for upstream-transcript differential. True enables and false disables it for this feed; null follows the global transcriptDifferentialEnabled setting.differentialFetchMode(string or null, one ofinherit,auto,on,off,null): Per-feed cross-fetch mode. ‘inherit’ follows the global default, ‘auto’ follows DAI detection, and ‘on’ or ‘off’ forces the stage.ownEpisodeGuids(boolean or null): When true, the served RSS uses MinusPod episode ids as item GUIDs instead of passing upstream GUIDs through (issue 598). Switching it on a feed with subscribers makes podcast apps treat every episode as new once. Null and false both mean upstream GUIDs. New feeds are created with true.description(string or null): Feed description, rewritten into the served channel <description>. Editable on any feed, local or subscribed, unlike title and p20 below, which are local-feed only (author, explicit and categories also work on YouTube feeds).title(string): Local feeds only (issue #625); 400 on a subscribed feed, whose title is RSS-managed and would be overwritten on the next refresh anyway; use titleOverride there instead. Must be non-empty.author(string or null): itunes:author. Local and YouTube feeds; 400 on a subscribed feed.explicit(boolean or null): itunes:explicit. Local and YouTube feeds; 400 on a subscribed feed. Null clears.categories(array of string): itunes:category list. Local and YouTube feeds; 400 on a subscribed feed. Null clears.p20(LocalFeedP20): Local feeds only; 400 on a subscribed feed. Merged onto the feed’s existing stored p20 object, so an unspecified tag is left untouched, except sending the literal JSON null, which clears the six list tags and any stored locked_owner in one shot (see LocalFeedP20). guid can never be set here.
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Feed updated successfully | Feed |
| 400 | Bad request - invalid input | Error |
| 404 | Resource not found | Error |
DELETE /feeds/{slug}
Section titled “DELETE /feeds/{slug}”Delete a feed
Delete a podcast feed and all associated data
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifier
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Feed deleted successfully | object |
| 404 | Resource not found | Error |
200 response body
Section titled “200 response body”message(string)slug(string)
POST /feeds/{slug}/refresh
Section titled “POST /feeds/{slug}/refresh”Refresh a single feed
Fetch the latest RSS feed and update the episode list. By default
this sends a conditional GET. If upstream returns 304 Not
Modified, the refresh is a no-op. Pass {"force": true} in the
body to bypass the stored ETag / Last-Modified headers and do a
full fetch, useful when an upstream CDN is falsely returning 304
against its own new content.
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifier
Request body (application/json)
Section titled “Request body (application/json)”Optional force-refresh flag. Omit for default conditional GET.
force(boolean): When true, clears the podcast’s stored ETag + Last-Modified values before fetching so upstream cannot short-circuit with 304 Not Modified. Defaultfalse.
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Feed refreshed successfully | object |
| 400 | Feed has no upstream source | Error |
| 404 | Resource not found | Error |
| 500 | Internal server error | Error |
| 502 | Upstream fetch failed or returned content that could not be parsed as RSS | Error |
200 response body
Section titled “200 response body”slug(string, required)message(string, required)outcome(RefreshOutcome, required)episodeCount(integer, required)lastRefreshed(string or null (date-time), required)lastPodpingAt(string or null (date-time), required)
POST /feeds/refresh
Section titled “POST /feeds/refresh”Refresh all feeds
Refresh RSS feeds for all configured podcasts. By default this
sends a conditional GET per feed. If upstream returns 304 Not
Modified, that feed is a no-op. Pass {"force": true} in the
body to clear every podcast’s stored ETag / Last-Modified before
fetching so upstream cannot short-circuit with 304.
Request body (application/json)
Section titled “Request body (application/json)”Optional force-refresh flag. Omit for default conditional GET per feed.
force(boolean): When true, clears every podcast’s stored ETag + Last-Modified values before fetching so upstream cannot short-circuit with 304 Not Modified. Defaultfalse.
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | All feeds refreshed | RefreshAllResult |
| 207 | Refresh pass completed with at least one feed failure | RefreshAllResult |
| 500 | Internal server error | Error |
POST /feeds/refresh-artwork
Section titled “POST /feeds/refresh-artwork”Refresh cover art for all feeds
Re-pull each feed’s cover art and rebuild its served RSS so the cover-art badge setting (artworkWatermarkEnabled, issue #420) takes effect. Unlike a feed refresh, this does not re-discover or queue episodes, so it never triggers processing. Podcast apps still re-fetch the feed on their own schedule before the change is visible.
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Artwork refreshed | object |
200 response body
Section titled “200 response body”message(string)feedCount(integer)
POST /feeds/regenerate
Section titled “POST /feeds/regenerate”Rebuild every served RSS with current URL settings (feed auth key, cover badge, base URL)
Re-fetch each feed’s upstream source and re-render its served RSS so URL-affecting settings (feed auth key, cover-art badge, base URL) take effect. Unlike a feed refresh, this never re-discovers or queues episodes, so it cannot trigger processing. Podcast apps still re-fetch the feed on their own schedule before the change is visible.
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Feeds regenerated | object |
200 response body
Section titled “200 response body”message(string)feedCount(integer)
GET /feeds/{slug}/artwork
Section titled “GET /feeds/{slug}/artwork”Get feed artwork
Returns cached podcast artwork without admin authentication. Safe legacy slugs are accepted up to 200 characters.
No session needed.
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifier
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Artwork image | image/jpeg, image/png |
| 404 | Resource not found | Error |
GET /feeds/{slug}/ad-distribution
Section titled “GET /feeds/{slug}/ad-distribution”Get a feed’s ad-position distribution
Returns where this feed’s ad cuts have historically landed across an episode, as a normalized-position histogram plus the learned prior zones. Setting-independent: returns data even when the positional-prior experiment is disabled.
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifier
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Ad distribution for the feed | AdDistribution |
| 404 | Resource not found | Error |
GET /networks
Section titled “GET /networks”List known podcast networks
Returns list of known podcast networks for network-level pattern matching
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | List of networks | object |
200 response body
Section titled “200 response body”networks(array of Network)
GET /feeds/subscriber-keys
Section titled “GET /feeds/subscriber-keys”List subscriber keys across all feeds
Labels and timestamps without secret values, sorted by feed title, label, then creation time.
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Subscriber keys with their feed | object |
200 response body
Section titled “200 response body”keys(array of FeedSubscriberKeyWithFeed, required)
GET /feeds/{slug}/subscriber-keys
Section titled “GET /feeds/{slug}/subscriber-keys”List feed subscriber keys
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifier
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Labels and timestamps without secret values | object |
| 404 | Resource not found | Error |
200 response body
Section titled “200 response body”keys(array of FeedSubscriberKeyRecord, required)
POST /feeds/{slug}/subscriber-keys
Section titled “POST /feeds/{slug}/subscriber-keys”Create a feed subscriber key
Returns the scoped token and feed URL once.
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifier
Request body (application/json)
Section titled “Request body (application/json)”label(string)
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 201 | Scoped token and feed URL | FeedSubscriberKeyCreated |
| 400 | Bad request - invalid input | Error |
| 404 | Resource not found | Error |
DELETE /feeds/{slug}/subscriber-keys/{key_id}
Section titled “DELETE /feeds/{slug}/subscriber-keys/{key_id}”Revoke a feed subscriber key
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifierkey_id(path, string, required)
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Key revoked | object |
| 404 | Resource not found | Error |
200 response body
Section titled “200 response body”revoked(constanttrue, required)
DELETE /feeds/{slug}/subscriber-keys/{key_id}/record
Section titled “DELETE /feeds/{slug}/subscriber-keys/{key_id}/record”Delete a revoked subscriber key record
Removes only a record that was already revoked. The deleted token stays invalid because no matching credential remains.
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifierkey_id(path, string, required)
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Revoked key record deleted | object |
| 404 | Subscriber key not found | |
| 409 | Subscriber key is still active |
200 response body
Section titled “200 response body”deleted(constanttrue, required)
GET /podping/hosts
Section titled “GET /podping/hosts”List domains seen sending Podpings on the Hive chain
Diagnostic view of what the Podping listener has observed. The listener records the domain of every feed URL announced on the chain, including ones that match no local feed, which is what lets a feed report whether its host sends Podpings at all. Counts are aggregated per domain as the listener runs, so there is no per-notification history to return. Rows recorded earlier stay visible when the listener is disabled.
Parameters
Section titled “Parameters”limit(query, integer): Rows to return, clamped to 1-500. Does not affect the totals.
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Observed Podping host domains, most recently active first | object |
200 response body
Section titled “200 response body”hosts(array of object)hosts[].domain(string)hosts[].firstSeenAt(string (date-time))hosts[].lastSeenAt(string (date-time))hosts[].pingCount(integer)hosts[].active(boolean): Seen within activeWindowDays.limit(integer)totalDomains(integer)activeDomains(integer)activeWindowDays(integer)listenerEnabled(boolean)allNodesDown(boolean): Non-fatal: every configured Hive RPC node is currently failing. RSS polling remains the fallback, so this never gates /health or feed refresh.degradedSince(string or null (date-time)): When allNodesDown last became true, or null.nodes(array of object): Durable per-node health, one entry per configured RPC node.nodes[].node(string)nodes[].selected(boolean): Last node to complete a successful RPC call.nodes[].consecutiveFailures(integer)nodes[].lastSuccessAt(string or null (date-time)): Last successful RPC response, shown as Last seen; null before success.nodes[].nextRetryAt(string or null (date-time))nodes[].lastFailureReason(string or null)nodes[].httpStatus(integer or null): Latest HTTP status, including 200 with invalid_response; null if none arrived.nodes[].outcome(string or null, one ofhealthy,http_error,invalid_response,unreachable,null): Latest RPC outcome; null before the first check.
GET /feeds/{slug}/tags
Section titled “GET /feeds/{slug}/tags”Get a feed’s tag breakdown (rss / episode / user / effective)
Parameters
Section titled “Parameters”slug(path, string, required)
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Tag breakdown | object |
| 404 | Feed not found |
200 response body
Section titled “200 response body”effective(array of string)rss(array of string)episode(array of string)user(array of string)
PUT /feeds/{slug}/tags
Section titled “PUT /feeds/{slug}/tags”Update a feed’s user-added tags
Parameters
Section titled “Parameters”slug(path, string, required)
Request body (application/json, required)
Section titled “Request body (application/json, required)”user_tags(array of string, required)
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Updated tag breakdown | object |
| 400 | Unknown or invalid tag(s) |
200 response body
Section titled “200 response body”effective(array of string)rss(array of string)episode(array of string)user(array of string)
POST /feeds/{slug}/rerender-segments
Section titled “POST /feeds/{slug}/rerender-segments”Re-cut every processed episode of a feed against the current segment-category action maps
Queues the existing per-episode recut (reprocess_mode=recut) for every processed episode of the feed that still has a retained original, saved transcript segments, and ad detections. Each recut re-resolves segment-category actions against the current per-feed/global maps, so this is the bulk trigger for applying a changed segment-category setting to already-processed episodes.
Parameters
Section titled “Parameters”slug(path, string, required)
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Recut queued for qualifying episodes | object |
| 404 | Feed not found |
200 response body
Section titled “200 response body”queued(integer)skipped(integer)