OP3, the Open Podcast Prefix Project: a free service that counts the downloads of episodes through a prefix of their URLs, and its API.
A host puts the prefix of OP3 in front of the URL of each enclosure. OP3 then counts each request, and redirects it to the file.
The API gives the downloads of a show, the counts of the show and of its episodes, and the apps of the listeners. Each call needs the token of an API key from https://op3.dev/api/keys.
A download of OP3 has the keys of the downloads of dk.simongray.podcast.analytics where OP3 tells the same fact. They're the :time, the :url, the :listener, the agent, and the :ip, which OP3 keeps as a hash. OP3 counts its downloads by its own rules, which are close to those of the IAB.
OP3, the Open Podcast Prefix Project: a free service that counts the downloads of episodes through a prefix of their URLs, and its API. A host puts the prefix of OP3 in front of the URL of each enclosure. OP3 then counts each request, and redirects it to the file. The API gives the downloads of a show, the counts of the show and of its episodes, and the apps of the listeners. Each call needs the token of an API key from https://op3.dev/api/keys. A download of OP3 has the keys of the downloads of dk.simongray.podcast.analytics where OP3 tells the same fact. They're the :time, the :url, the :listener, the agent, and the :ip, which OP3 keeps as a hash. OP3 counts its downloads by its own rules, which are close to those of the IAB.
(accepts? url)Whether OP3 takes the URL url behind its prefix, as its server did on
11 October 2026.
OP3 answers a URL that it doesn't take with an error, so the episode behind the prefix doesn't play. It takes an http or https URL with a path and without a user name. It doesn't take a domain that's an IPv6 address or localhost, or that has characters other than ASCII letters, digits, dots and hyphens.
Whether OP3 takes the URL `url` behind its prefix, as its server did on 11 October 2026. OP3 answers a URL that it doesn't take with an error, so the episode behind the prefix doesn't play. It takes an http or https URL with a path and without a user name. It doesn't take a domain that's an IPv6 address or localhost, or that has characters other than ASCII letters, digits, dots and hyphens.
(call! token call params)(call! token call params opts)Send the call call with params and opts to OP3 with the bearer
token, and read the answer. On the JVM it gives the data, and in
ClojureScript a promise of it.
It gives nil when OP3 has no such show or no data of it. Any other
answer that isn't a success throws ::failed with its :status and
:message, such as 401 for an unknown token or 429 when a caller asks too
often. A success that holds no JSON object is ::failed too. The opts
are these:
Send the call `call` with `params` and `opts` to OP3 with the bearer `token`, and read the answer. On the JVM it gives the data, and in ClojureScript a promise of it. It gives nil when OP3 has no such show or no data of it. Any other answer that isn't a success throws ::failed with its :status and :message, such as 401 for an unknown token or 429 when a caller asks too often. A success that holds no JSON object is ::failed too. The `opts` are these: - :origin, as request takes it - :timeout-seconds, for each request - :user-agent, as http/send! takes it - :send, a function that sends a request map in place of client/send!
The calls of the API, by the names of OP3, with their parameters:
:show {:show :episodes?}
:downloads {:show :start :end :episode :bots? :limit
:continuation-token}
:show-download-counts {:show} ; one show UUID, or several
:episode-download-counts {:show :limit}
:top-apps-for-show {:show}
The :show parameter names a show by its show UUID. For :show and :top-apps-for-show, it can also be its podcast guid or the URL of its feed. The :start and :end are instants, or text that OP3 reads: a date such as 2026-09 or 2026-09-26, or a time before now, such as -24h.
The calls of the API, by the names of OP3, with their parameters:
:show {:show :episodes?}
:downloads {:show :start :end :episode :bots? :limit
:continuation-token}
:show-download-counts {:show} ; one show UUID, or several
:episode-download-counts {:show :limit}
:top-apps-for-show {:show}
The :show parameter names a show by its show UUID. For :show and
:top-apps-for-show, it can also be its podcast guid or the URL of its
feed. The :start and :end are instants, or text that OP3 reads: a date
such as 2026-09 or 2026-09-26, or a time before now, such as -24h.(fetch-downloads! token params)(fetch-downloads! token params opts)Fetch the downloads of the show that params name, page after page,
with the bearer token and opts. On the JVM it gives the result, and
in ClojureScript a promise of it.
The result is a map of the :downloads and the :continuation-token. The
call :downloads gives the pages. A page holds at most :limit downloads,
20,000 unless params say. The fetch stops after :max-pages pages, five
unless opts say. When it stops before the end, the continuation token
names the next page, and params with it read on from there. The other
opts are as call! takes them.
Nothing that OP3 or the network does throws. When the call for a page fails, the result has its :error, a diagnostic, with the downloads of the pages before that page. The continuation token then names the page that failed.
Fetch the downloads of the show that `params` name, page after page, with the bearer `token` and `opts`. On the JVM it gives the result, and in ClojureScript a promise of it. The result is a map of the :downloads and the :continuation-token. The call :downloads gives the pages. A page holds at most :limit downloads, 20,000 unless `params` say. The fetch stops after :max-pages pages, five unless `opts` say. When it stops before the end, the continuation token names the next page, and `params` with it read on from there. The other `opts` are as call! takes them. Nothing that OP3 or the network does throws. When the call for a page fails, the result has its :error, a diagnostic, with the downloads of the pages before that page. The continuation token then names the page that failed.
The origin of OP3, unless the options give the origin of another instance of OP3 as :origin.
The origin of OP3, unless the options give the origin of another instance of OP3 as :origin.
(prefix url)(prefix url {:keys [podcast-guid] :as opts})The URL url with the prefix of OP3 in front, by opts, so that OP3
counts the requests for it. It's nil for a URL that OP3 doesn't take.
Such a URL is https://op3.dev/e/cdn.example.com/episode.mp3. A URL that
goes through OP3 already stays as it is. The opts are these:
The URL `url` with the prefix of OP3 in front, by `opts`, so that OP3 counts the requests for it. It's nil for a URL that OP3 doesn't take. Such a URL is https://op3.dev/e/cdn.example.com/episode.mp3. A URL that goes through OP3 already stays as it is. The `opts` are these: - :podcast-guid, the podcast:guid of the show, which helps OP3 find the show - :origin, for another instance of OP3 - :accepts-pred, a predicate of a URL, when that instance takes other URLs
(prefixed? url)(prefixed? url opts)Whether the URL url goes through OP3, at the :origin of opts. Its
prefix is in the URL before the query, also behind other prefixes.
Whether the URL `url` goes through OP3, at the :origin of `opts`. Its prefix is in the URL before the query, also behind other prefixes.
(request token call params)(request token call params opts)The request map of the call call with params, as calls lists
them, and the bearer token of an API key. The :origin of opts names
another instance of OP3. A nil token gives no Authorization, for a
request that sender signs on your server.
The request map of the call `call` with `params`, as calls lists them, and the bearer `token` of an API key. The :origin of `opts` names another instance of OP3. A nil `token` gives no Authorization, for a request that sender signs on your server.
(response call body)(response call body opts)The data in the JSON body of the answer to the call call. The
fields of OP3 become keywords in kebab-case, and its times instants:
:show {:show-uuid :title :podcast-guid :stats-page-url
:episodes [{:id :title :pubdate :item-guid}]}
:downloads {:downloads [download] :continuation-token}
:show-download-counts {:asof :show-download-counts {show-uuid counts}}
:episode-download-counts {:show-uuid :show-title :episodes [counts] …}
:top-apps-for-show {:show-uuid :app-downloads {app downloads}}
A download has the keys of an IAB download where OP3 tells the same
fact. So hashedIpAddress is :ip, and audienceId is the :listener, and
the :url is that of the file behind the prefix, by the :origin of
opts, as target gives it. The keys of a map by show or by app stay
text.
It throws ::failed for a body that holds no JSON object.
The data in the JSON `body` of the answer to the call `call`. The
fields of OP3 become keywords in kebab-case, and its times instants:
:show {:show-uuid :title :podcast-guid :stats-page-url
:episodes [{:id :title :pubdate :item-guid}]}
:downloads {:downloads [download] :continuation-token}
:show-download-counts {:asof :show-download-counts {show-uuid counts}}
:episode-download-counts {:show-uuid :show-title :episodes [counts] …}
:top-apps-for-show {:show-uuid :app-downloads {app downloads}}
A download has the keys of an IAB download where OP3 tells the same
fact. So hashedIpAddress is :ip, and audienceId is the :listener, and
the :url is that of the file behind the prefix, by the :origin of
`opts`, as target gives it. The keys of a map by show or by app stay
text.
It throws ::failed for a body that holds no JSON object.(sender token)(sender token {:keys [send] :or {send client/send!} :as opts})A function to give as :send, which adds the bearer token to each
request to the API of OP3 and sends it with the :send of opts.
Give it to the proxy on your server, e.g. (proxy/handler {:send (sender
token)}), so that the token stays there, and call with a nil token in
the page. The token replaces any Authorization that the request has. A
request to another URL than those of the API at the :origin of opts
goes as it is. The :send is client/send! by default.
A function to give as :send, which adds the bearer `token` to each
request to the API of OP3 and sends it with the :send of `opts`.
Give it to the proxy on your server, e.g. (proxy/handler {:send (sender
token)}), so that the token stays there, and call with a nil token in
the page. The token replaces any Authorization that the request has. A
request to another URL than those of the API at the :origin of `opts`
goes as it is. The :send is client/send! by default.(target url)(target url opts)The URL url without the prefix of OP3 at the :origin of opts, which
is the URL of the file that OP3 redirects to, e.g.
https://cdn.example.com/episode.mp3 for
https://op3.dev/e/cdn.example.com/episode.mp3, so that OP3's downloads
and a host's own count together. A prefix in front of OP3's goes too.
A URL that doesn't go through OP3 stays as it is.
The URL `url` without the prefix of OP3 at the :origin of `opts`, which is the URL of the file that OP3 redirects to, e.g. https://cdn.example.com/episode.mp3 for https://op3.dev/e/cdn.example.com/episode.mp3, so that OP3's downloads and a host's own count together. A prefix in front of OP3's goes too. A URL that doesn't go through OP3 stays as it is.
(with-prefix feed)(with-prefix feed opts)The feed with the prefix of OP3 in front of the URL of each enclosure,
and of each source of an alternate enclosure, by opts.
OP3 counts the requests for both. A prefix goes only where OP3 takes the
URL. The :podcast-guid is that of the feed, unless opts give one.
The `feed` with the prefix of OP3 in front of the URL of each enclosure, and of each source of an alternate enclosure, by `opts`. OP3 counts the requests for both. A prefix goes only where OP3 takes the URL. The :podcast-guid is that of the feed, unless `opts` give one.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |