Liking cljdoc? Tell your friends :D

Services and their defaults

This page lists what podcast-clj knows about other services and apps, such as the podcast directories, Podping, OP3, gpodder servers and Nostr relays, and the option that replaces each default. Principles explains why they're defaults, and How to build a podcast app shows the calls. Namespaces are relative to dk.simongray.podcast.

The rule

  • A fact that a spec defines is a constant in the code, e.g. the well-known paths of LNURL-pay, the methods of the Hive API, and the calls of the Mastodon API.
  • A fact about one service is a default: a public var holds it as data, and an option of the same name replaces it for one call.
  • Where an option takes the name of an entry in a table, it also takes an entry as data, in the same shape.

The defaults, as of September 2026

WhatVarOptionDefault
The directoriesdirectory/sourcesa source, as a name or an entryApple Podcasts, fyyd, gpodder.net and the Podcast Index
The root of the Podcast Index APIdirectory.podcastindex/base-url:base-urlhttps://api.podcastindex.org/api/1.0/
The Hive nodes that Podping readspodping/nodes:nodes, or one :nodepublic nodes
The service that writes a podping for a hostpodping/gateway:gatewayhttps://podping.cloud/
The genres of Apple Podcasts, for the chart of a categorydirectory/apple-genresthe :genres of a sourcethe genres of Apple
The forms of links to the appslinks/apps:appsthe table below
The paths that find-feeds! tries on the domain of a pagefeed.discovery/guessed-paths:guessed-paths/feed/ of WordPress, /rss of Libsyn and Ghost, /feed.xml of Jekyll and /index.xml of Hugo
The relays of Nostrcomments.nostr/relays:relaysthe relays below
The instance of OP3analytics.op3/origin:originhttps://op3.dev
The URLs that OP3 accepts behind its prefixanalytics.op3/accepts?:accepts-pred, a predicate of a URLthe rules of op3.dev
The paths of the gpodder APIgpodder.api/apis:api, as a name or a map of paths:gpodder
The limits of a gpodder servergpodder/default-limits:page-size, :max-pages, :batch-size, :max-pending1000, 10, 100 and 500
The paths of statuses, for the Mastodon APIcomments.activitypub/status-paths:status-pathsthe paths of four server programs
The requirements of a directory for a feedfeed.validate/requirements:requirements, as a name or a mapnone
The categories of Apple Podcasts, which the validator checks a podcast's againstfeed.spec/categories and feed.spec/renamed-categories:categories and :renamed-categoriesthe categories below
The requirements of a directory for artwork and media filesfeed.check.rules/requirements:requirements, as a name or a mapnone
The folder that an app writes around a whole OPML listprofile.opml/wrappers:wrappers, a set of names or a predicate of a namefeeds

There's no default for these, so you give them:

  • the AppView that reads AT Protocol threads, as :appview. As of September 2026, Bluesky's https://public.api.bsky.app and Blacksky's https://api.blacksky.community answer without an account.
  • the root of a gpodder server, some of which Sync reference lists, and the URL of a WebDAV collection, some of which the providers page of like-minded lists
  • the OPAWG list of user agents, which analytics/user-agents reads from files that you keep

The directories

An entry of directory/sources says which API the directory speaks, which :calls it answers, and has the URLs of the service:

Name:apiURLs
:apple:apple:search and :lookup, the URLs of the two calls, :trending and :genre-charts, the roots of the charts, and :genre-names, the URL of the genre service
:fyyd:fyyd:root
:gpodder:gpodder:root
:podcastindex:podcastindex:root, and :pages, the URL of the pages of the shows

The functions request, response and call! take a source as a name or an entry, and the :sources of search! and fetch-chart! can hold both. The :id of an entry names the source in :directory/source, :directory/sources and the :source of a diagnostic, and an entry without an :id takes the name of its API.

The operators of gpodder.net said on 6 September 2026 that it's now a sync service, and that it hasn't indexed podcasts since 2023. Its search, its tag charts and its lookup still answer, but from that old index.

The chart of a category

Each directory names a category its own way. These facts come from the answers of the services on 29 September 2026:

DirectoryThe chart of a categoryThe category
Apple PodcastsThe feed {country}/rss/toppodcasts/limit={n}/genre={id}/json, below the :genre-charts of the entry, in the form of Apple's older feeds. It names no feed, and the chart of one show has an object in place of a list.The id of a genre. directory/apple-genres has the ids by name, from Apple's genre service.
Podcast IndexThe call podcasts/trending with the parameter catA name or an id from categories.json of the podcast namespace, or several, with commas. The index gives the chart of all categories for a name that it doesn't know, without an error.
gpodder.netThe call api/2/tag/{tag}/{n}.jsonA tag, a word that users of gpodder.net gave a show
fyydNone. The call feature/podcast/hot ignores a category.

The Podcast Index uses the words of Apple's names for its categories, e.g. Apple's Society & Culture is Society and Culture there. So the names of Apple's top categories work for both, and podcastindex/category-filter reads a name with an ampersand as its parts. A second-level category of Apple, such as Tech News, has no name in the Podcast Index, so give a map with a category for each source.

The names of the categories in other languages

Apple's genre service returns the names of its genres in the language of a store, e.g. Danish for ?id=26&cc=dk&l=da. Fetch them with directory/fetch-category-names!. Apple doesn't document the service, but it answered on 14 October 2026. The categories of the Podcast Index and of the podcast namespace are in English only, so the English lists stay as the fallback.

The gpodder API

The paths of the calls are in gpodder.api/apis, where {user} is the user name and {device} is the id of the device.

Call:gpodder:nextcloud
:login/api/2/auth/{user}/login.jsonnone
:register/api/2/devices/{user}/{device}.jsonnone
:subscriptions, :update-subscriptions/api/2/subscriptions/{user}/{device}.json/index.php/apps/gpoddersync/subscriptions, /index.php/apps/gpoddersync/subscription_change/create
:episodes, :update-episodes/api/2/episodes/{user}.json/index.php/apps/gpoddersync/episode_action, /index.php/apps/gpoddersync/episode_action/create

A backend only logs in when its API has a path for :login, and only registers its device when it has one for :register.

LimitDefaultMeaning
:page-size1000A page of episode actions with this many actions is full, so a pull asks for one more page. gpodder.net gives 1000 actions at most.
:max-pages10The pages of episode actions that one pull reads
:batch-size100The episode actions in one request of a push. gpodder.net accepts a longer list in the background, and reports no errors in it.
:max-pending500The actions for unknown feeds that the cursor keeps

The statuses of the Mastodon API

When the server of the root post refuses the reader, comments/fetch! reads the thread through the Mastodon API, which needs the id of the status from the URL of the root post. Each entry of comments.activitypub/status-paths is a regular expression for the whole path of a URL, whose one group is the id.

PathServer program
/@USER/IDMastodon
/@USER/statuses/IDGoToSocial
/users/USER/statuses/IDThe ActivityPub id of a status of Mastodon and GoToSocial
/ap/users/ACCOUNT/statuses/IDThe numeric ActivityPub id of a status on newer Mastodon servers, e.g. infosec.exchange on 14 October 2026
/notice/IDPleroma and Akkoma

OP3

OP3 answers a URL that it doesn't accept with an error, so an episode with such a URL wouldn't play, and analytics.op3/accepts? checks a URL first. As of 11 October 2026, the URL must be http or https, with a path and without a user name, and its domain can't be:

  • an IPv6 address
  • localhost
  • a name with characters other than ASCII letters, digits, dots and hyphens

Another instance of OP3 may accept other URLs, so give its :origin, and its rules as :accepts-pred.

To try the API before you have a key of your own, OP3's API docs give the sample bearer token preview07ce, as of October 2026. They don't say what it's limited to.

The links of the apps

The forms of the links to a show and to an episode in each app are in links/apps. They come from Nathan Gathright's list Podcast Platform Links, as it was on 29 September 2026. The table only has the forms built from ids that a directory gives, and leaves out the forms that need an id that only the app knows, such as Spotify's id of a show. Apple's id of an episode comes from the :episodes call of directory.

A key in braces is a key of links/ids, and a word before the key is an encoding:

  • encoded: percent-encoded for a query
  • base64url: the base64 of URLs
  • hex: hexadecimal
  • bare: the URL without its scheme

The apps ignore {slug}, so links/default-slug fills it. An entry of type page is a web page that leads a listener to the apps.

NameTypeShowEpisodeSubscribe
1tunerapphttps://1tuner.com/podcast/{slug}/{base64url feed-url}https://1tuner.com/podcast/{slug}/{base64url feed-url}/{encoded episode-guid}
AntennaPodapphttps://antennapod.org/deeplink/subscribe?url={feed-url} antennapod-subscribe://{bare feed-url}
Anytime Playerapphttps://anytimeplayer.app/subscribe?url={feed-url}
Apple Podcastsapphttps://podcasts.apple.com/podcast/id{apple-id}https://podcasts.apple.com/podcast/id{apple-id}?i={apple-episode-id}podcast://{bare feed-url}
Breezapphttps://breez.link/p?feedURL={encoded feed-url}https://breez.link/p?feedURL={encoded feed-url}&episodeID={encoded episode-guid}
Castamaticapphttps://castamatic.com/guid/{podcast-guid} castamatic://{bare feed-url}
Castboxapphttps://castbox.fm/vic/{apple-id}
Castroapphttps://castro.fm/itunes/{apple-id} castros://subscribe/{bare feed-url}
CurioCasterapphttps://curiocaster.com/podcast/pi{podcastindex-id}
Downcastapp downcast://{bare feed-url}
Fountainapphttps://fountain.fm/show/{podcastindex-id}https://fountain.fm/episode/{podcastindex-episode-id}
Goodpodsapphttps://www.goodpods.com/podcasts-aid/{apple-id}
gPodderapphttps://gpodder.net/subscribe?url={feed-url} gpodder://{bare feed-url}
Great Podspagehttps://www.greatpods.co/podcasts?podcastIndexId={podcastindex-id}
https://www.greatpods.co/podcasts?itunesId={apple-id}
iCatcher!app icatcher://{bare feed-url}
Metacastapphttps://metacast.app/itunes/{apple-id}
https://open.metacast.app/podcasts/{podcast-guid}
Moon FMapphttps://moon.fm/itunes/{apple-id}
Odeslipagehttps://pods.link/i/{apple-id}
Overcastapphttps://overcast.fm/itunes{apple-id} overcast://x-callback-url/add?url={feed-url}
Player FMapphttps://player.fm/subscribe?id={encoded feed-url} playerfm://{bare feed-url}
Plinkpagehttps://plinkhq.com/i/{apple-id}?to=page
Pocket Castsapphttps://pca.st/itunes/{apple-id} pktc://subscribe/{bare feed-url}
Pod Enginepagehttps://www.podengine.ai/podcasts/id/{apple-id}
pod.linkpagehttps://pod.link/{apple-id}
https://pod.link/{base64url feed-url}
Podbeanapphttps://www.podbean.com/itunes/{apple-id}
Podcast Addictapphttps://podcastaddict.com/feed/{encoded feed-url}https://podcastaddict.com/episode/{encoded enclosure-url}podcastaddict://{bare feed-url}
Podcast Detailspagehttps://podcastdetails.com/?url={feed-url}
Podcast Guruapphttps://app.podcastguru.io/podcast/{apple-id}
https://app.podcastguru.io/podcast/X{hex feed-url}
podcastguru://{bare feed-url}
Podcast Indexpagehttps://podcastindex.org/podcast/{podcastindex-id}
https://podcastindex.org/podcast/{podcast-guid}
Podcast Parrotapphttps://podcastparrot.app/show?url={encoded feed-url}
Podcast Republicapphttps://www.podcastrepublic.net/podcast/{apple-id}
Podcast X-Raypagehttps://podcastxray.com/podcast/{apple-id}
Podcatapp podcat://{bare feed-url}
Podchaserpagehttps://podchaser.com/f/pod/{apple-id}
podfollowpagehttps://podfollow.com/{apple-id}
Podfriendapp https://podfriend.com/podcast/{apple-id}/{encoded episode-guid}
Podkickerapp podkicker://subscribe/{bare feed-url}
PodLPapphttps://link.podlp.app/{apple-id}
https://link.podlp.app/{podcast-guid}
Podnewspagehttps://podnews.net/podcast/{apple-id}
https://podnews.net/podcast/pi{podcastindex-id}
https://podnews.net/podcast/{podcast-guid}
podStationapphttps://podstation.github.io/subscribe-ext/?feedURL={feed-url}
Poduramaapphttps://podurama.com/podcast/{slug}-i{apple-id}
Podverseapphttps://api.podverse.fm/api/v1/podcast/podcastindex/{podcastindex-id}
RSSRadioapp rssradio://{bare feed-url}
Sonnetapphttps://sonnet.fm/p/{apple-id}
Steno.fmapphttps://steno.fm/show/{podcast-guid}https://steno.fm/show/{podcast-guid}/episode/{base64url episode-guid}
The Podcast Appapphttps://thepodcastapp.dev/p/itunes{apple-id}
https://thepodcastapp.dev/p/{podcastindex-id}
TrueFansapphttps://truefans.fm/{podcast-guid}
YouTube Musicapphttps://music.youtube.com/library/podcasts?addrssfeed={base64url feed-url}

No app has a form for a link to a time. The apps that share an episode at a time put the time on a link with their own id of the episode, and none of them documents the form. A form is data, so you can add your own to the table, with a key such as :time in the ids. For a media file, links/at-time writes the media fragment of the W3C, which a browser plays from that time.

The relays of Nostr

The default relays for comments on Nostr are in comments.nostr/relays, which comments/fetch! asks when the options give no :relays. A thread whose root names relays asks those first.

The table shows what each relay answered on 11 October 2026, when asked for the notes about one episode by its NIP-73 id. Fountain wrote three notes about it on 29 September.

RelayIn comments.nostr/relaysThe answer
wss://relay.fountain.fmyesThe three notes about the episode. It closes a filter that names kind 1111 as "kinds not supported", and answers a filter on the tags I and E with "invalid filters".
wss://relay.damus.ioyesNo note about the episode, but notes about other episodes
wss://relay.primal.netyesNo note about the episode, but notes about other episodes, and a comment of kind 1111 on one
wss://nos.lolnoNo note about the episode, but notes about other episodes
wss://relay.nostr.bandnoNo connection within ten seconds

On that day, the relays had notes of kind 1 about episodes, with the id of the episode in an i tag as Fountain writes them. Only relay.primal.net had a comment of kind 1111 on an episode, the form that NIP-22 defines, written on 8 October. The reader asks for both.

Choose your own relays with :relays:

(comments/fetch! thread {:relays ["wss://relay.example"
                                  "wss://relay.fountain.fm"]})

The wallets of the listeners

The wallet namespaces follow NIP-47 in value.nwc and the WebLN guide in value.webln, as they read on 29 September 2026. No test used a wallet with real money, so the table says what the specs say, not what a wallet answered.

SpecWhereWhat podcast-clj implements
NIP-47, Nostr Wallet Connectgithub.com/nostr-protocol/nips, the file 47.mdThe connection URI, the info event, requests and responses, and the commands pay_invoice, make_invoice, lookup_invoice, get_balance and get_info
NWC-04, Keysend Paymentsgithub.com/nostr-wallet-connect/nwc, the file 04.mdThe command pay_keysend, with custom records in hexadecimal
NWC-06, Metadata ConventionsThe same repository, the file 06.mdNothing of its own: pay-invoice takes :metadata as the app gives it
NIP-44, Encrypted Payloadsgithub.com/nostr-protocol/nips, the file 44.mdVersion 2, including payloads of 65,536 bytes and more
WebLNwebln.guideenable, getInfo, sendPayment and keysend

On that day, NIP-47 had a core of five commands, and the rest were extensions in the repository nostr-wallet-connect/nwc. An older NIP-47 had pay_keysend in its own text.

The options replace these choices:

ChoiceDefaultOption
How long ask! waits, after which a request expires60 seconds, nwc/default-limits:timeout-seconds
The relays that a request can go toThe public internet, nostr/public?:allow-pred
What sends a request to a relaynostr/send!:send
The most bytes of an encrypted text65,535, nip44/default-limits:max-bytes
The WebLN providerThat of the page, webln/provider:provider

The rules of the apps for downloads

No standard defines how an app downloads episodes and deletes files. The namespace downloads has the kinds of rule that these apps have, and the table gives the rules of each app as its help pages described them on 29 September 2026, with the matching rule in downloads.

AppThe rule of the appIn downloads
Apple PodcastsAutomatic downloads of a number of episodes, or of the episodes of a time:limit of :episodes, or :age in its :where
Apple PodcastsNo automatic downloads for one show:download? of the rule of the show
Apple PodcastsA played episode goes 24 hours after it's played:played
Pocket CastsA played episode goes when it's played, or some time later:played
Pocket CastsAn episode goes when the listener hasn't played or downloaded it for some time:inactive
Pocket CastsStarred episodes stay, if the listener wants:keep
Pocket CastsA show keeps its newest 1, 2, 5 or 10 episodes:limit of :episodes, with :trim?
AntennaPodAutomatic downloads of the episodes in the inbox, for each show:download?, and :inbox? in the :where of :episodes
AntennaPodA filter for a show: a minimum length, and words to include or exclude:duration, :text and :not in the :where of :episodes
AntennaPod50 episodes at most, unless the listener sets another number:max-episodes
AntennaPodA file goes when its episode is played:played of 0
AntennaPodOld files go when a download needs room, except those in the queue and the favourites:make-room?, and :keep

The defaults are in downloads/default-rule and downloads/default-rules. Until the listener sets a rule, they download nothing, and only delete the files of a show that the listener unsubscribed from.

The folder around an OPML list

Some apps write the whole subscription list inside one outline, a wrapper rather than a folder of the listener's own. OPML can't tell the two apart, so profile.opml/from-opml goes by the name: a folder is a wrapper when it holds every feed of the list and its name is in profile.opml/wrappers, whatever its case.

NameApp that writes itSeen in September 2026
feedsPocket Castsan export file
feedsOvercastan export file

A folder with another name stays a folder, so a listener's own folder that holds all the shows survives, and an export from profile.opml/to-opml comes back as it went out. For an app that writes another name, give the names as :wrappers, or (constantly true) to treat every folder that holds every feed as a wrapper.

The requirements of directories

An entry of feed.validate/requirements holds the elements that a directory requires and recommends, for the channel and for each item. An entry of feed.check.rules/requirements holds what it requires of artwork and of the media files. Both hold the levels of the diagnostics that the directory changes:

KeyMeaning
:nameThe name of the directory or the standard
:artwork:sizes, the smallest and largest width in pixels, :square?, for a square of any size, :types, the media types, :colors, the colour models, and :extension?, :alpha? and :last-modified?
:media-type-predA predicate of the media type that the server gives a media file
:levelsThe level of a diagnostic, by its id

Both have these profiles:

ProfileSourceAs of
:psp-1PSP-1 of the Podcast Standards Project30 September 2026
:appleApple's A Podcaster's Guide to RSS30 September 2026
:spotifySpotify's Podcast Delivery Specification 1.1013 October 2026
:youtubeYouTube's Deliver podcasts using an RSS feed, with the limits on a video's title and description13 October 2026

Spotify's specification also asks for titles of at most 20 characters where they show, which it truncates rather than refuses, so the profile leaves that out. YouTube publishes no size for the artwork that it makes its videos from.

The categories of Apple Podcasts

The var feed.spec/categories holds the categories and subcategories of Apple's list, as of 29 September 2026, which PSP-1 requires too. The validator checks the categories of a podcast by it, but not those of a feed of music or of another medium, which the list doesn't cover.

In 2019, Apple renamed some categories and dropped some subcategories, and it still reads the old names. The var feed.spec/renamed-categories has each with the one that stands for it now, from Apple's email as Blubrry republished it on 17 July 2019, and the list of changes that Podnews updated on 27 November 2019.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close