Docs

Public API
Copy page as Markdown

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.

feed address
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/"}'
site address
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:

201 Created
{
  "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. null for 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, or erroring. See Pause and resume.

  • last_polled_at timestamp | null

    When PodRead last read the feed. null before 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_…"
200 OK
{
  "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_…"
200 OK
{
  "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_…"
200 OK
{ "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.

Start listening free

2 episodes/month, no credit card required

By signing up, you agree to our Terms and Privacy Policy.