Docs
Feed subscriptions
Follow a site and its new posts become episodes. PodRead reads each feed every hour and converts up to 3 new posts per read. All endpoints need a bearer token, so read Authentication first. Requests and responses are JSON, and timestamps are UTC (ISO 8601 with a trailing Z).
Endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/feed_subscriptions | Follow a feed address or a site address |
| GET | /api/v1/feed_subscriptions | List the sites you follow, newest first |
| POST | /api/v1/feed_subscriptions/:id/pause | Pause a subscription |
| DELETE | /api/v1/feed_subscriptions/:id/pause | Resume a subscription |
| DELETE | /api/v1/feed_subscriptions/:id | Stop following a site |
Follow a site
POST /api/v1/feed_subscriptions with one field, feed_url. Send the feed address, or send the site address and let PodRead find the feed. PodRead reads the feed at once, converts the latest post, and checks the feed hourly after that.
curl -X POST https://podread.app/api/v1/feed_subscriptions \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"feed_url":"https://verynormal.info/rss/"}'
curl -X POST https://podread.app/api/v1/feed_subscriptions \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"feed_url":"https://verynormal.info"}'
Both calls return 201 Created with the same body:
{
"object": "feed_subscription",
"id": "feed_aB3xK9mQ2pL7",
"title": "Very Normal",
"feed_url": "https://verynormal.info/rss/",
"status": "active",
"last_polled_at": null,
"episode_count": 0
}
Fields
-
object string
Always
feed_subscription. -
id string
The
feed_-prefixed public identifier. Pass it to the pause and delete endpoints. -
title string | null
The title the feed states.
nullfor a feed with no title. -
feed_url string
The feed address PodRead polls. A site address you send is replaced by the feed address it declares.
-
status enum
One of
active,paused, orerroring. See Pause and resume. -
last_polled_at timestamp | null
When PodRead last read the feed.
nullbefore the first poll. -
episode_count integer
How many episodes this feed created.
Site address discovery
An agent usually holds the address of the page it just read, not the address of the feed. Send that page address in feed_url. PodRead reads the page, takes the first 3 feed links the page declares in its <link rel="alternate"> tags, and falls back to the common paths /feed, /rss, /feed.xml.
The response always carries the feed address, not the page address you sent. A page with no feed returns 422 with the message No feed found at that address. Try the site's RSS link.. PodRead reads the feed it finds during discovery, so it does not download the feed twice. Discovery adds up to 5 seconds to the call.
An address with no scheme works too. PodRead reads example.com as https://example.com.
Compare the feed_url in the response with the address you sent. The response shows you which feed you now follow. Discovery runs only when the address answers with a page. A typo in a feed path answers 404, and the call then returns 422 with Could not fetch feed. Send the site address when you do not know the feed path.
List subscriptions
curl https://podread.app/api/v1/feed_subscriptions \
-H "Authorization: Bearer sk_live_…"
{
"object": "list",
"data": [
{
"object": "feed_subscription",
"id": "feed_aB3xK9mQ2pL7",
"title": "Very Normal",
"feed_url": "https://verynormal.info/rss/",
"status": "active",
"last_polled_at": "2026-04-19T14:22:09Z",
"episode_count": 12
}
],
"has_more": false
}
The list is newest first and holds every subscription. It is not paginated, so has_more is always false. Each element of data is the object that POST /api/v1/feed_subscriptions returns.
Pause and resume
A paused subscription stays in the list, and PodRead stops reading its feed. Both calls return the subscription.
A resume reads the feed once, shortly after the call. That read converts up to 3 posts published during the pause, and it spends the credits those episodes cost. PodRead marks the older posts as seen and makes no episode for them. A resume of a subscription that is already active changes nothing and reads no feed.
curl -X POST https://podread.app/api/v1/feed_subscriptions/feed_aB3xK9mQ2pL7/pause \
-H "Authorization: Bearer sk_live_…"
curl -X DELETE https://podread.app/api/v1/feed_subscriptions/feed_aB3xK9mQ2pL7/pause \
-H "Authorization: Bearer sk_live_…"
{
"object": "feed_subscription",
"id": "feed_aB3xK9mQ2pL7",
"title": "Very Normal",
"feed_url": "https://verynormal.info/rss/",
"status": "paused",
"last_polled_at": "2026-04-19T14:22:09Z",
"episode_count": 12
}
PodRead sets status to erroring by itself after 5 failed reads in a row. A resume clears that state too.
Stop following
curl -X DELETE https://podread.app/api/v1/feed_subscriptions/feed_aB3xK9mQ2pL7 \
-H "Authorization: Bearer sk_live_…"
{ "object": "feed_subscription", "id": "feed_aB3xK9mQ2pL7", "deleted": true }
Episodes the feed already created stay in the podcast feed. Delete them one by one with DELETE /api/v1/episodes/:id.
Error responses
| Status | When | Body |
|---|---|---|
| 401 | Missing or invalid bearer token | {"error":{"type":"authentication_error","code":"invalid_token","message":"Unauthorized"}} |
| 404 | A pause or delete on an unknown id, or on one another user owns. PodRead does not leak existence across users. |
{"error":{"type":"invalid_request_error","code":"resource_missing","message":"Feed subscription not found","param":"id"}} |
| 422 | No feed at that address, a feed you already follow, an address that is not a URL, or a free account at its subscription limit. | {"error":{"type":"invalid_request_error","code":"feed_subscription_invalid","message":"No feed found at that address. Try the site's RSS link."}} |
| 429 | Over 10 saved subscriptions in the last rolling hour, or over 20 create attempts, or over 60 pause or resume calls. Back off and retry later. The attempt limits also send a Retry-After header. |
{"error":{"type":"rate_limit_error","code":"rate_limited","message":"You've reached your hourly feed subscription limit"}} or the same shape with the message Rate limit exceeded. Please try again later. |