FeedBro API

Use your FeedBro account from your own apps and scripts.

With the API you can read your feeds and articles as JSON, keep your reading state in step (read, saved, for later), pull the main text out of any web page, follow new sources and hear about changes on websites you watch. It is the same account you use in FeedBro: what you do through the API shows up in the app, and the other way round.

Quick start

  1. Create a key. Open the API tab in Settings, give the key a name (for example "My script") and copy it. You see the full key only once, so keep it somewhere safe.
  2. Send your first request. Every request carries the key in an Authorization header:
    curl -H "Authorization: Bearer YOUR_KEY" -H "Accept: application/json" \
      https://feedbro.com/api/v1/summary/unread-count
    FeedBro answers with JSON:
    {
      "unread_count": 142,
      "updated_at": "2026-10-20T12:47:13+00:00"
    }
  3. Explore. Use the playground below to try any call with your own key, or download the Postman collection.

Every address starts with https://feedbro.com/api/v1. Treat a key like a password: anyone who has it can read your account. If one leaks, revoke it in Settings and create a new one.

Try it

Paste a key, pick a call and press Send. The answer comes from your own account, and it counts toward your plan like any other request. Your key is not saved anywhere: it stays in the box until you leave the page. Prefer Postman? Download the Postman collection and set its apiKey variable.


        
    

How it works

Plans and limits

Every plan can read. Coffee Club can also change things: mark articles, run background jobs and receive webhooks.

FreeCoffee Club
Keys1up to 5
Requests500 a month100 a day
Speed30 a minute per key60 a minute per key
Readingyesyes
Marking articlesnoread, saved, for later
Background jobs and webhooksnoyes

Each answer tells you where you stand, in four headers:

X-Plan-Limit: 100          how many requests your plan includes
X-Plan-Remaining: 96       how many are left
X-Plan-Period: day         day (Coffee Club) or month (Free)
X-Plan-Resets: 2026-10-21T00:00:00Z   when the count starts again (00:00 UTC)

Most requests count as 1. A background job counts as more, because it does more work: fetching an article or extracting a page counts as 10, following a feed as 5, starting a watch as 25. When a job fails, it counts as 1 and the rest is given back.

Why some things are background jobs

Reading an article returns the text FeedBro already has, straight away. Fetching a page from another website can take from a second to a minute, so the API never makes you wait for it. Instead you start a job: FeedBro answers at once with 202 and a job id, does the work in the background, and then tells your webhook. You can also ask for the job's status whenever you like.

1. POST /extract  {"url": "https://www.example.com/story"}   →  202, job id "01m5cn…"
2. GET  /jobs/01m5cn…   →  "status": "queued", then "running", then "done" (or "failed")
3. or, without asking: your webhook receives a job.completed event with the result

Pages of results

Lists come 25 at a time, newest first. When there are more, the answer includes next_cursor. Send it back as cursor to get the next page. When next_cursor is null, you have everything.

Reading

Available on every plan.

RequestWhat you get
GET /feedsThe feeds you follow: id, title, url, site_url, unread_count and failing (true when FeedBro cannot fetch it right now). Optional since_hours counts only recent unread articles.
GET /articlesYour articles, 25 at a time, newest first. Filter with filter (all, unread, saved or read_later), feed_id, collection_id or since_hours. Page with cursor.
GET /articles/{id}One article with its full text, as FeedBro has it. Reading it through a key does not mark it read; send mark_read=1 if you want that.
GET /collectionsYour collections and the feeds in each.
GET /saved-pagesPages you saved with the browser extension.
GET /summary/unread-countOne number: unread articles across all your feeds.
GET /accountYour name, plan and reading preferences.

Example: your unread articles.

curl -H "Authorization: Bearer YOUR_KEY" -H "Accept: application/json" \
  "https://feedbro.com/api/v1/articles?filter=unread"
{
  "articles": [
    {
      "id": 1018350,
      "feed_id": 1278,
      "feed_title": "KING 5 News",
      "title": "Supreme Court agrees to hear a case about middle school admissions",
      "url": "https://www.king5.com/article/news/...",
      "author": "",
      "summary": "The Supreme Court will hear a case that has been...",
      "image_url": "https://media.tegna-media.com/...jpg",
      "published_at": "2026-10-20T12:19:43+00:00",
      "is_read": false,
      "is_saved": false,
      "is_read_later": false
    }
  ],
  "next_cursor": "eyJjYl9hcnRpY2xlcy5wdWJsaXNoZWRfYXQiOiIyMDI2LTEw…"
}

Marking articles Coffee Club

Each call switches the mark: a read article becomes unread, and the other way round. The answer gives the new state, so you always know where it ended up.

POST /articles/{id}/readRead / unread
POST /articles/{id}/saveSaved / not saved
POST /articles/{id}/read-laterRead later / not
POST /articles/1018350/read   →   {"read": true}

Background jobs Coffee Club

Each of these starts a job and answers 202 straight away. Usually the job is done within a minute. At most 20 of your jobs can wait at once.

RequestWhat it doesCounts as
POST /articles/{id}/fetchGets the full text of one of your articles when FeedBro only has a summary. A notice that is only a document (a PDF) gives you document_url instead.10
POST /extract
{"url": "…", "main_only": true}
The main content of any public web page, in result.page: title, author, published_at, image_url, site_name, language, is_article (false for a listing or home page), content_html, content_text and word_count. Send "main_only": false to also get full_text, every bit of visible text on the page. Each text field holds up to 200 KB; a longer one is cut, and its name is listed in truncated.10
POST /feeds/add
{"url": "…"}
Follows a feed. Give it a website and FeedBro finds its feed for you. A page with no feed fails with a hint to watch it instead (see Website Watch).5
GET /jobs/{id}The job's status (queued, running, done or failed) with its result or error.1

Starting a job:

curl -X POST -H "Authorization: Bearer YOUR_KEY" -H "Accept: application/json" \
  -H "Content-Type: application/json" -d '{"url": "https://www.example.com/story"}' \
  https://feedbro.com/api/v1/extract
202 Accepted
{
  "job": {
    "id": "01m5cncb17v11vg8gc498gycv4",
    "type": "page_extract",
    "status": "queued",
    "cost": 10,
    "result": null,
    "error": null,
    "created_at": "2026-10-20T12:43:02+00:00",
    "finished_at": null
  },
  "message": "Started. The result goes to your webhook, or check GET /api/v1/jobs/01m5cncb17v11vg8gc498gycv4."
}

A finished job, from GET /jobs/{id}:

{
  "job": {
    "id": "01m5cncb17v11vg8gc498gycv4",
    "type": "page_extract",
    "status": "done",
    "cost": 10,
    "result": {
      "page": {
        "url": "https://www.example.com/story",
        "title": "The story's headline",
        "author": "A. Writer",
        "published_at": "2026-10-20T08:00:00+00:00",
        "image_url": "https://www.example.com/lead.jpg",
        "site_name": "Example News",
        "language": "en",
        "is_article": true,
        "content_html": "<p>The first paragraph…</p>",
        "content_text": "The first paragraph…",
        "word_count": 812
      }
    },
    "error": null,
    "finished_at": "2026-10-20T12:43:11+00:00"
  }
}

A job that could not be done has "status": "failed" and says why in error, for example "This page has no RSS feed. To follow a page without a feed, watch it instead…".

Webhooks Coffee Club

A webhook is an address on your server that FeedBro calls when something happens, so you don't have to keep asking. Each key can have one. It must start with https://, use port 443 (the default) or 8443, and be reachable from the internet.

POST /webhook {"url": "https://…"}Sets the address. The answer includes a signing secret, shown only this once.
GET /webhookThe current address and the events it receives.
POST /webhook/testSends a ping event now, so you can check your side works.
POST /webhook/removeStops sending.

Events. job.completed and job.failed when a job ends; watch.item when a page you watch has a new post; watch.price when a store you watch has a new product, a lower price, a sale or an item back in stock; ping from the test call.

What arrives. A POST with a JSON body:

POST https://your-server.example/feedbro-hook
Content-Type: application/json
X-FeedBro-Event: job.completed
X-FeedBro-Timestamp: 1792500123
X-FeedBro-Signature: sha256=5d41402abc4b2a76b9719d911017c592...

{
  "id": "01m5cnf2k8w9…",
  "event": "job.completed",
  "created_at": "2026-10-20T12:43:11+00:00",
  "data": {
    "job": { "id": "01m5cncb17v11vg8gc498gycv4", "type": "page_extract", "status": "done", "result": { … } }
  }
}

Answer with any 2xx status to say you got it. If your server doesn't, FeedBro tries three more times over about 20 minutes.

Checking it came from FeedBro. The signature is an HMAC-SHA256, made with your secret, of the timestamp, a dot and the raw body. Compute the same on your side and compare. Also ignore deliveries more than five minutes old, so an old one can't be sent again.

// PHP
$ts = (int) ($_SERVER['HTTP_X_FEEDBRO_TIMESTAMP'] ?? 0);
$expected = 'sha256='.hash_hmac('sha256', $ts.'.'.$rawBody, $secret);
if (abs(time() - $ts) > 300 || ! hash_equals($expected, $_SERVER['HTTP_X_FEEDBRO_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}

// Node.js
const crypto = require('crypto');
const ts = Number(req.headers['x-feedbro-timestamp']);
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(ts + '.' + rawBody).digest('hex');
const ok = Math.abs(Date.now() / 1000 - ts) <= 300
  && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers['x-feedbro-signature'] || ''));

Website Watch Beta

Some websites have no feed. Website Watch reads the page itself and turns each new post, product or price change into an item, the same way it works in the app. Reading watches works on every plan; starting one is Coffee Club.

GET /watchesYour watched pages and stores: id, url, title, kind (page or store), items and failing.
GET /watches/{id}/itemsA watch's items, newest first, 25 at a time with next_cursor. A store's items also carry product: price, compare_at, currency and available.
POST /watches {"url": "…"}Starts watching a page, as a job (counts as 25). If the site already has its own feed, the result offers it instead. Send "page_anyway": true to watch the page anyway. Your plan's watch limit applies.
GET /watches
{
  "beta": true,
  "watches": [
    { "id": 1995, "url": "https://www.anthropic.com/news", "title": "Newsroom", "kind": "page",
      "last_checked_at": "2026-10-20T12:31:00+00:00", "failing": false, "items": 18 }
  ]
}

Errors

When something goes wrong, the status code tells you what kind of problem it is, and the JSON body says what happened in plain words, in message. Show that message to whoever needs to fix it. These are the answers you can get, exactly as the API sends them.

401 Unauthenticated

The key is missing, mistyped or revoked. Check the Authorization: Bearer YOUR_KEY header, or create a new key in Settings.

{
  "message": "Unauthenticated."
}

403 Not allowed

The key works, but it can't do this. Most often the call needs Coffee Club; then coffee_club_required is true, and you can show an upgrade offer.

Marking an article with a Free plan key:

{
  "message": "On the Free plan an API key can read only. Coffee Club adds marking articles read, saved or for later.",
  "coffee_club_required": true
}

Starting a job or setting a webhook without Coffee Club:

{
  "message": "Background jobs and webhooks are part of Coffee Club.",
  "coffee_club_required": true
}

An address the API offers only to FeedBro's own apps:

{
  "message": "An API key cannot use this endpoint."
}

404 Not found

The article, feed, watch or job id doesn't exist, or it belongs to someone else. Check the id: it should come from one of your own lists.

{
  "message": "Not found. It does not exist, or it is not in your account."
}

422 Something in the request is wrong

A field is missing or not valid. When a field is the problem, errors lists each one with what's wrong, so you can point the user at it.

{
  "message": "The url field must be a valid URL.",
  "errors": {
    "url": ["The url field must be a valid URL."]
  }
}

Other things the API refuses with 422:

{ "message": "That address cannot be fetched (it is not a public web page)." }
{ "message": "The webhook address must start with https:// and be under 500 characters." }

429 Too many requests

Either too many requests in one minute, or your plan's requests for the day (Coffee Club) or month (Free) are used up.

Too fast. Wait the number of seconds in Retry-After, then try again:

HTTP/1.1 429 Too Many Requests
Retry-After: 37

{
  "message": "Too many requests this minute. Wait a moment and try again."
}

The plan's requests are used up until resets:

HTTP/1.1 429 Too Many Requests
X-Plan-Remaining: 0
X-Plan-Resets: 2026-11-01T00:00:00Z

{
  "message": "Monthly limit reached: the Free plan includes 500 API requests a month. Coffee Club includes 100 a day.",
  "coffee_club_required": true,
  "resets": "2026-11-01T00:00:00Z"
}

Too many jobs waiting at once (at most 20):

{
  "message": "You already have 20 jobs waiting. Try again when some have finished."
}

500 Something went wrong on our side

Rare, and not something you did. Try again in a minute. If it keeps happening, tell us what you sent.

RSS instead of JSON

If an app only understands RSS, you don't need a key at all: use your private RSS links. Every plan has one for Saved articles, and Coffee Club adds one for each collection. You'll find them in Settings, on the Feeds tab.

Questions

Something missing, or not working the way you expected? Tell us. We read every message.