Claude Code Plugins

Community-maintained marketplace

Feedback

Use this skill whenever the user wants to interact with a CommaFeed RSS reader instance via its REST API. Triggers include: listing, subscribing, unsubscribing from RSS feeds; reading, starring, or tagging feed entries; managing categories; marking entries as read/unread; importing or exporting OPML; managing user settings or profile; viewing unread counts; refreshing feeds; fetching feed info by URL; administering users on a CommaFeed instance. Also use for requests like \"show my feeds\", \"subscribe to RSS feed\", \"list unread articles\", \"star this entry\", \"export my feeds as OPML\", \"mark all as read\", \"get unread count\", or \"refresh my feeds\". Always use this skill for any CommaFeed API interaction.

Install Skill

Shared

Installs to .agents/skills, used by Codex, Amp, Warp, Cursor, OpenCode, and more.

CodexAmp
Warp
CursorOpenCode
Cline
Gemini CLI
GitHub Copilot
Personal

Available across projects.

$npx skills-installer add @biggora/claude-plugins-registry/commafeed-api --client shared
Project

Writes to .agents/skills.

$npx skills-installer add @biggora/claude-plugins-registry/commafeed-api -p --client shared
Note: Review the skill instructions before using it.

SKILL.md

name commafeed-api
description Use this skill whenever the user wants to interact with a CommaFeed RSS reader instance via its REST API. Triggers include: listing, subscribing, unsubscribing from RSS feeds; reading, starring, or tagging feed entries; managing categories; marking entries as read/unread; importing or exporting OPML; managing user settings or profile; viewing unread counts; refreshing feeds; fetching feed info by URL; administering users on a CommaFeed instance. Also use for requests like "show my feeds", "subscribe to RSS feed", "list unread articles", "star this entry", "export my feeds as OPML", "mark all as read", "get unread count", or "refresh my feeds". Always use this skill for any CommaFeed API interaction.

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