commafeed-api

SKILL.md

CommaFeed REST API Skill

Interact with a self-hosted CommaFeed RSS reader instance via its REST API.

Environment Variables (REQUIRED)

Before making any API calls, ensure these environment variables are set:

Variable Description Example
COMMAFEED_HOST CommaFeed instance URL (with protocol, no trailing slash) https://commafeed.example.com
COMMAFEED_USER CommaFeed username admin
COMMAFEED_PASS CommaFeed password secretpass
export COMMAFEED_HOST="https://commafeed.example.com"
export COMMAFEED_USER="admin"
export COMMAFEED_PASS="your-password"

If these are not set, ask the user to provide them before proceeding.

Authentication

All requests use HTTP Basic Auth:

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/category/get" | jq .

Every request pattern below assumes:

  • Base URL: $COMMAFEED_HOST/rest
  • Auth: -u "$COMMAFEED_USER:$COMMAFEED_PASS"
  • Content-Type: application/json (for POST requests with JSON body)

Helper alias for examples:

CF="curl -s -u $COMMAFEED_USER:$COMMAFEED_PASS"

API Endpoints

Categories

Method Path Description
GET /rest/category/get Get root category tree (all categories + subscriptions)
POST /rest/category/add Add a new category
POST /rest/category/modify Rename or move a category
POST /rest/category/delete Delete a category
POST /rest/category/collapse Collapse/expand a category in the UI
GET /rest/category/entries Get entries for a category
GET /rest/category/entriesAsFeed Get category entries as RSS/Atom XML
POST /rest/category/mark Mark all entries in a category as read
GET /rest/category/unreadCount Get unread count per subscription

Get category tree (all subscriptions)

Returns the full tree: root category with nested children and feed subscriptions.

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/category/get" | jq .

Response — Category object:

{
  "id": "all",
  "name": "All",
  "children": [
    {
      "id": "10",
      "parentId": "all",
      "name": "Tech",
      "children": [],
      "feeds": [
        {
          "id": 42,
          "name": "Hacker News",
          "feedUrl": "https://news.ycombinator.com/rss",
          "feedLink": "https://news.ycombinator.com",
          "iconUrl": "/rest/feed/favicon/42",
          "unread": 15,
          "categoryId": "10",
          "position": 0,
          "newestItemTime": "2026-03-07T10:30:00Z",
          "errorCount": 0,
          "lastRefresh": "2026-03-07T10:00:00Z",
          "nextRefresh": "2026-03-07T10:15:00Z"
        }
      ],
      "expanded": true,
      "position": 0
    }
  ],
  "feeds": [],
  "expanded": true,
  "position": 0
}

Add a category

curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d '{"name": "Technology", "parentId": "all"}' \
  "$COMMAFEED_HOST/rest/category/add" | jq .

Request body — AddCategoryRequest:

  • name (string, required, max 128 chars) — category name
  • parentId (string, optional) — parent category ID, omit for root level

Response: category ID (integer).

Modify a category

curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d '{"id": 10, "name": "Tech News", "parentId": "all", "position": 0}' \
  "$COMMAFEED_HOST/rest/category/modify"

Request body — CategoryModificationRequest:

  • id (integer, required) — category ID
  • name (string) — new name
  • parentId (string) — new parent category ID
  • position (integer) — position in parent

Delete a category

curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d '{"id": 10}' \
  "$COMMAFEED_HOST/rest/category/delete"

Request body — IDRequest:

  • id (integer, required) — category ID

Get category entries

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/category/entries?id=all&readType=unread&limit=20&order=desc" | jq .

Query parameters:

  • id (required) — category ID, "all" for all feeds, or "starred" for starred entries
  • readType (required) — all or unread
  • limit (integer, default 20, max 1000) — entries per page
  • offset (integer, default 0) — pagination offset
  • order (string, default desc) — desc or asc
  • newerThan (long) — Unix timestamp in ms, only entries newer than this
  • keywords (string) — search filter
  • tag (string) — filter by tag
  • excludedSubscriptionIds (string) — comma-separated subscription IDs to exclude

Response — Entries object:

{
  "name": "All",
  "entries": [
    {
      "id": "feed/42:entry/abc123",
      "guid": "https://example.com/article-1",
      "title": "Article Title",
      "content": "<p>Article HTML content...</p>",
      "author": "John Doe",
      "date": "2026-03-07T09:00:00Z",
      "insertedDate": "2026-03-07T09:05:00Z",
      "url": "https://example.com/article-1",
      "feedId": "42",
      "feedName": "Example Feed",
      "feedUrl": "https://example.com/rss",
      "feedLink": "https://example.com",
      "iconUrl": "/rest/feed/favicon/42",
      "read": false,
      "starred": false,
      "markable": true,
      "tags": [],
      "categories": "tech, news",
      "rtl": false,
      "enclosureUrl": null,
      "enclosureType": null,
      "mediaThumbnailUrl": null
    }
  ],
  "timestamp": 1741339200000,
  "hasMore": true,
  "offset": 0,
  "limit": 20,
  "errorCount": 0,
  "ignoredReadStatus": false
}

Get unread counts

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/category/unreadCount" | jq .

Response — array of UnreadCount:

[
  {"feedId": 42, "unreadCount": 15},
  {"feedId": 55, "unreadCount": 3}
]

Mark category as read

curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d '{"id": "all", "olderThan": 1741339200000, "read": true}' \
  "$COMMAFEED_HOST/rest/category/mark"

Request body — MarkRequest:

  • id (string, required) — category ID or "all"
  • read (boolean) — true to mark read, false for unread
  • olderThan (long) — timestamp, mark only entries older than this
  • insertedBefore (long) — mark only entries inserted before this
  • keywords (string) — filter by keywords

Collapse/expand category

curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d '{"id": 10, "collapse": true}' \
  "$COMMAFEED_HOST/rest/category/collapse"

Feed Subscriptions

Method Path Description
POST /rest/feed/subscribe Subscribe to a feed
POST /rest/feed/unsubscribe Unsubscribe from a feed
POST /rest/feed/modify Modify subscription (rename, move, filter)
GET /rest/feed/get/{id} Get subscription details
POST /rest/feed/fetch Fetch feed info by URL (preview before subscribing)
GET /rest/feed/refreshAll Queue all feeds for refresh
GET /rest/feed/entries Get entries for a specific feed
GET /rest/feed/entriesAsFeed Get feed entries as RSS/Atom XML
POST /rest/feed/mark Mark feed entries as read
GET /rest/feed/favicon/{id} Get feed favicon
GET /rest/feed/export Export all subscriptions as OPML
POST /rest/feed/import Import subscriptions from OPML file

Subscribe to a feed

curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/rss",
    "title": "Example Blog",
    "categoryId": 10
  }' \
  "$COMMAFEED_HOST/rest/feed/subscribe" | jq .

Request body — SubscribeRequest:

  • url (string, required) — feed URL
  • title (string, required) — display name
  • categoryId (integer) — target category ID

Response: subscription ID (integer).

Unsubscribe from a feed

curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d '{"id": 42}' \
  "$COMMAFEED_HOST/rest/feed/unsubscribe"

Modify a subscription

curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d '{
    "id": 42,
    "name": "New Feed Name",
    "categoryId": 10,
    "position": 0,
    "filter": "title.contains(\"important\")",
    "pushNotificationsEnabled": false,
    "autoMarkAsReadAfterDays": 30
  }' \
  "$COMMAFEED_HOST/rest/feed/modify"

Request body — FeedModificationRequest:

  • id (integer, required) — subscription ID
  • name (string) — new display name
  • categoryId (integer) — move to a different category
  • position (integer) — position in category
  • filter (string) — CEL expression to auto-mark entries as read
  • pushNotificationsEnabled (boolean) — enable push notifications
  • autoMarkAsReadAfterDays (integer) — auto-mark read after N days (null to disable)

Get subscription details

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/feed/get/42" | jq .

Response — Subscription object:

{
  "id": 42,
  "name": "Hacker News",
  "message": null,
  "errorCount": 0,
  "lastRefresh": "2026-03-07T10:00:00Z",
  "nextRefresh": "2026-03-07T10:15:00Z",
  "feedUrl": "https://news.ycombinator.com/rss",
  "feedLink": "https://news.ycombinator.com",
  "iconUrl": "/rest/feed/favicon/42",
  "unread": 15,
  "categoryId": "10",
  "position": 0,
  "newestItemTime": "2026-03-07T10:30:00Z",
  "filter": null,
  "pushNotificationsEnabled": false,
  "autoMarkAsReadAfterDays": null
}

Fetch feed info (preview)

curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/rss"}' \
  "$COMMAFEED_HOST/rest/feed/fetch" | jq .

Request body — FeedInfoRequest:

  • url (string, required, 1–4096 chars) — feed URL to probe

Response — FeedInfo: title, url, link for the discovered feed.

Refresh all feeds

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/feed/refreshAll"

Note: Returns 429 Too Many Requests if called too frequently.

Get feed entries

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/feed/entries?id=42&readType=unread&limit=50&order=desc" | jq .

Query parameters (same pattern as category entries):

  • id (required) — subscription ID
  • readType (required) — all or unread
  • limit (integer, default 20, max 1000)
  • offset (integer, default 0)
  • orderdesc or asc
  • newerThan (long) — Unix timestamp in ms
  • keywords (string) — search filter

Response: Entries object (same structure as category entries).

Mark feed as read

curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d '{"id": "42", "read": true}' \
  "$COMMAFEED_HOST/rest/feed/mark"

OPML Export

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/feed/export" -o subscriptions.opml

Response: OPML XML file.

OPML Import

curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -F "file=@subscriptions.opml" \
  "$COMMAFEED_HOST/rest/feed/import"

Note: Uses multipart/form-data, not JSON.


Entries (individual entry operations)

Method Path Description
POST /rest/entry/mark Mark a single entry as read/unread
POST /rest/entry/markMultiple Mark multiple entries at once
POST /rest/entry/star Star/unstar an entry
POST /rest/entry/tag Set tags on an entry
GET /rest/entry/tags Get all user tags

Mark entry as read/unread

curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d '{"id": "feed/42:entry/abc123", "read": true}' \
  "$COMMAFEED_HOST/rest/entry/mark"

Request body — MarkRequest:

  • id (string, required) — entry ID
  • read (boolean) — true = read, false = unread

Mark multiple entries

curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d '{
    "requests": [
      {"id": "feed/42:entry/abc123", "read": true},
      {"id": "feed/42:entry/def456", "read": true}
    ]
  }' \
  "$COMMAFEED_HOST/rest/entry/markMultiple"

Star/unstar an entry

curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "feed/42:entry/abc123",
    "feedId": 42,
    "starred": true
  }' \
  "$COMMAFEED_HOST/rest/entry/star"

Request body — StarRequest:

  • id (string, required) — entry ID
  • feedId (integer, required) — feed subscription ID
  • starred (boolean, required) — true to star, false to unstar

Tag an entry

curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d '{
    "entryId": 12345,
    "tags": ["important", "read-later"]
  }' \
  "$COMMAFEED_HOST/rest/entry/tag"

Request body — TagRequest:

  • entryId (integer, required) — entry ID
  • tags (array of strings, required) — tags to assign (replaces existing)

Get all user tags

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/entry/tags" | jq .

Response: array of tag strings.


User Profile & Settings

Method Path Description
GET /rest/user/profile Get user profile
POST /rest/user/profile Update user profile
POST /rest/user/profile/deleteAccount Delete own account
GET /rest/user/settings Get user settings
POST /rest/user/settings Save user settings

Get profile

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/user/profile" | jq .

Response — UserModel:

{
  "id": 1,
  "name": "admin",
  "email": "admin@example.com",
  "apiKey": "abc-123-def-456",
  "enabled": true,
  "admin": true
}

Update profile

curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d '{
    "currentPassword": "old-pass",
    "newPassword": "new-pass",
    "newEmail": "new@example.com"
  }' \
  "$COMMAFEED_HOST/rest/user/profile"

Request body — ProfileModificationRequest:

  • currentPassword (string, required) — current password for verification
  • newPassword (string) — new password
  • newEmail (string) — new email
  • newApiKey (boolean) — true to regenerate API key

Get settings

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/user/settings" | jq .

Response — Settings:

{
  "language": "en",
  "readingMode": "unread",
  "readingOrder": "desc",
  "showRead": true,
  "scrollMarks": true,
  "scrollSpeed": 400,
  "scrollMode": "if_needed",
  "customCss": "",
  "customJs": "",
  "entriesToKeepOnTopWhenScrolling": 1,
  "starIconDisplayMode": "always",
  "externalLinkIconDisplayMode": "always",
  "markAllAsReadConfirmation": true,
  "markAllAsReadNavigateToNextUnread": false,
  "customContextMenu": true,
  "mobileFooter": false,
  "unreadCountTitle": true,
  "unreadCountFavicon": true,
  "disablePullToRefresh": false,
  "primaryColor": null,
  "sharingSettings": {
    "email": false,
    "gmail": false,
    "facebook": false,
    "twitter": false,
    "tumblr": false,
    "pocket": false,
    "instapaper": false,
    "buffer": false
  }
}

Settings enums:

  • readingMode: all, unread
  • readingOrder: asc, desc
  • scrollMode: always, never, if_needed
  • starIconDisplayMode / externalLinkIconDisplayMode: always, never, on_desktop, on_mobile

Save settings

curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d '{
    "language": "en",
    "readingMode": "unread",
    "readingOrder": "desc",
    "showRead": false,
    "scrollMarks": true
  }' \
  "$COMMAFEED_HOST/rest/user/settings"

Server Info

Method Path Description
GET /rest/server/get Get server information (no auth required)
GET /rest/server/proxy Proxy an image through the server

Get server info

curl -s "$COMMAFEED_HOST/rest/server/get" | jq .

Response — ServerInfo:

{
  "announcement": "",
  "version": "5.x.x",
  "gitCommit": "abc1234",
  "loginPageTitle": "CommaFeed",
  "allowRegistrations": false,
  "googleAnalyticsTrackingCode": null,
  "smtpEnabled": false,
  "demoAccountEnabled": false,
  "websocketEnabled": true,
  "websocketPingInterval": 15000,
  "treeMode": "unread"
}

Proxy image

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/server/proxy?u=https://example.com/image.png" \
  -o proxied-image.png

Admin Endpoints

Require ADMIN role.

Method Path Description
GET /rest/admin/metrics Get server metrics
GET /rest/admin/user/getAll List all users
GET /rest/admin/user/get/{id} Get user by ID
POST /rest/admin/user/save Create or update a user
POST /rest/admin/user/delete Delete a user

Get server metrics

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/admin/metrics" | jq .

List all users

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/admin/user/getAll" | jq .

Response: array of UserModel.

Get user by ID

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/admin/user/get/1" | jq .

Create / update user

curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "newuser",
    "password": "securepass123",
    "email": "user@example.com",
    "enabled": true,
    "admin": false
  }' \
  "$COMMAFEED_HOST/rest/admin/user/save"

Request body — AdminSaveUserRequest:

  • name (string, required) — username
  • password (string) — password (required for new users)
  • email (string) — email address
  • enabled (boolean, required) — account active
  • admin (boolean, required) — admin role

Delete user

curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d '{"id": 5}' \
  "$COMMAFEED_HOST/rest/admin/user/delete"

Registration & Password Reset

Method Path Description
POST /rest/user/register Register new account (if allowed)
POST /rest/user/initialSetup Create initial admin (first-run only)
POST /rest/user/passwordReset Request password reset email
POST /rest/user/passwordResetCallback Confirm password reset with token

Register

curl -s -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "name": "newuser",
    "password": "mypassword",
    "email": "user@example.com"
  }' \
  "$COMMAFEED_HOST/rest/user/register"

Request: name (3–32 chars), password, email (all required).


Common Patterns

List all feeds with unread counts

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/category/get" | \
  jq '[.. | .feeds? // empty | .[] | {id, name, unread, feedUrl}]'

Get all unread entries across all feeds

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/category/entries?id=all&readType=unread&limit=100" | \
  jq '.entries[] | {title, url, feedName, date}'

Get starred entries

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/category/entries?id=starred&readType=all&limit=50" | \
  jq '.entries[] | {title, url, starred, date}'

Search entries by keyword

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/category/entries?id=all&readType=all&keywords=kubernetes&limit=50" | \
  jq '.entries[] | {title, url, feedName}'

Mark all entries as read

# Get current timestamp
TS=$(date +%s)000
curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d "{\"id\": \"all\", \"read\": true, \"olderThan\": $TS}" \
  "$COMMAFEED_HOST/rest/category/mark"

Subscribe to a feed and put it in a category

# 1. Fetch feed info to verify URL
curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://blog.example.com/feed"}' \
  "$COMMAFEED_HOST/rest/feed/fetch" | jq .

# 2. Subscribe
curl -s -X POST \
  -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://blog.example.com/feed",
    "title": "Example Blog",
    "categoryId": 10
  }' \
  "$COMMAFEED_HOST/rest/feed/subscribe" | jq .

Export OPML backup

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/feed/export" -o "commafeed-backup-$(date +%Y%m%d).opml"

Paginate through all entries

OFFSET=0
LIMIT=100
while true; do
  RESPONSE=$(curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
    "$COMMAFEED_HOST/rest/category/entries?id=all&readType=all&limit=$LIMIT&offset=$OFFSET")

  COUNT=$(echo "$RESPONSE" | jq '.entries | length')
  HAS_MORE=$(echo "$RESPONSE" | jq '.hasMore')

  echo "$RESPONSE" | jq '.entries[] | {title, date, feedName}'

  if [ "$HAS_MORE" = "false" ] || [ "$COUNT" -eq 0 ]; then
    break
  fi

  OFFSET=$((OFFSET + LIMIT))
done

Get entries by tag

curl -s -u "$COMMAFEED_USER:$COMMAFEED_PASS" \
  "$COMMAFEED_HOST/rest/category/entries?id=all&readType=all&tag=important&limit=50" | \
  jq '.entries[] | {title, url, tags}'

Node.js Example

const COMMAFEED_HOST = process.env.COMMAFEED_HOST;
const COMMAFEED_USER = process.env.COMMAFEED_USER;
const COMMAFEED_PASS = process.env.COMMAFEED_PASS;

const AUTH = 'Basic ' + Buffer.from(`${COMMAFEED_USER}:${COMMAFEED_PASS}`).toString('base64');

async function cfApi(method, path, body = null) {
  const url = `${COMMAFEED_HOST}/rest${path}`;
  const options = {
    method,
    headers: {
      'Authorization': AUTH,
      'Content-Type': 'application/json',
    },
  };
  if (body) options.body = JSON.stringify(body);
  const res = await fetch(url, options);
  if (!res.ok) throw new Error(`CommaFeed API ${res.status}: ${await res.text()}`);
  const text = await res.text();
  return text ? JSON.parse(text) : null;
}

// Get all subscriptions
const tree = await cfApi('GET', '/category/get');
console.log(tree);

// Get unread entries
const entries = await cfApi('GET', '/category/entries?id=all&readType=unread&limit=20');
for (const e of entries.entries) {
  console.log(`[${e.feedName}] ${e.title}${e.url}`);
}

// Subscribe to a feed
const subId = await cfApi('POST', '/feed/subscribe', {
  url: 'https://example.com/rss',
  title: 'Example Feed',
});
console.log('Subscribed, ID:', subId);

// Star an entry
await cfApi('POST', '/entry/star', {
  id: entries.entries[0].id,
  feedId: parseInt(entries.entries[0].feedId),
  starred: true,
});

Python Example

import os
import requests

COMMAFEED_HOST = os.environ["COMMAFEED_HOST"]
COMMAFEED_USER = os.environ["COMMAFEED_USER"]
COMMAFEED_PASS = os.environ["COMMAFEED_PASS"]

AUTH = (COMMAFEED_USER, COMMAFEED_PASS)
HEADERS = {"Content-Type": "application/json"}

def cf_api(method, path, json=None):
    url = f"{COMMAFEED_HOST}/rest{path}"
    resp = requests.request(method, url, auth=AUTH, headers=HEADERS, json=json)
    resp.raise_for_status()
    return resp.json() if resp.text else None

# Get category tree
tree = cf_api("GET", "/category/get")
for cat in tree["children"]:
    print(f"Category: {cat['name']}")
    for feed in cat["feeds"]:
        print(f"  {feed['name']} — unread: {feed['unread']}")

# Get unread entries
entries = cf_api("GET", "/category/entries?id=all&readType=unread&limit=20")
for e in entries["entries"]:
    print(f"[{e['feedName']}] {e['title']}")

# Subscribe to a feed
sub_id = cf_api("POST", "/feed/subscribe", {
    "url": "https://example.com/rss",
    "title": "Example Feed",
})
print(f"Subscribed, ID: {sub_id}")

# Export OPML
resp = requests.get(f"{COMMAFEED_HOST}/rest/feed/export", auth=AUTH)
with open("backup.opml", "w") as f:
    f.write(resp.text)

Error Handling

Code Meaning
200 Success
400 Bad request (missing/invalid fields)
401 Not authorized (wrong credentials)
403 Forbidden (insufficient role — need ADMIN)
404 Resource not found (wrong feed/entry/category ID)
429 Too many requests (refreshAll rate limit)
500 Server error

Implementation Notes

  • Always read COMMAFEED_HOST, COMMAFEED_USER, COMMAFEED_PASS from environment variables — never hardcode
  • Authentication is HTTP Basic Auth, not API key headers
  • The /rest/server/get endpoint does not require authentication
  • Entry IDs follow the format feed/{feedId}:entry/{guid} — preserve them as strings
  • Timestamps are in milliseconds (Unix epoch) for newerThan / olderThan parameters
  • OPML import uses multipart/form-data, all other POST endpoints use application/json
  • The readType parameter is required for entry listing endpoints
  • Maximum limit is 1000 entries per request — use offset + hasMore for pagination
  • id=all means all feeds, id=starred means starred entries in category endpoints
  • Tags replace the full tag list on an entry — send the complete desired set
  • CEL filter expressions on subscriptions auto-mark non-matching entries as read
Weekly Installs
8
GitHub Stars
1
First Seen
9 days ago
Installed on
opencode8
antigravity8
qwen-code8
junie8
claude-code8
github-copilot8