commafeed-api
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 nameparentId(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 IDname(string) — new nameparentId(string) — new parent category IDposition(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 entriesreadType(required) —allorunreadlimit(integer, default 20, max 1000) — entries per pageoffset(integer, default 0) — pagination offsetorder(string, defaultdesc) —descorascnewerThan(long) — Unix timestamp in ms, only entries newer than thiskeywords(string) — search filtertag(string) — filter by tagexcludedSubscriptionIds(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 unreadolderThan(long) — timestamp, mark only entries older than thisinsertedBefore(long) — mark only entries inserted before thiskeywords(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 URLtitle(string, required) — display namecategoryId(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 IDname(string) — new display namecategoryId(integer) — move to a different categoryposition(integer) — position in categoryfilter(string) — CEL expression to auto-mark entries as readpushNotificationsEnabled(boolean) — enable push notificationsautoMarkAsReadAfterDays(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 IDreadType(required) —allorunreadlimit(integer, default 20, max 1000)offset(integer, default 0)order—descorascnewerThan(long) — Unix timestamp in mskeywords(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 IDread(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 IDfeedId(integer, required) — feed subscription IDstarred(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 IDtags(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 verificationnewPassword(string) — new passwordnewEmail(string) — new emailnewApiKey(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,unreadreadingOrder:asc,descscrollMode:always,never,if_neededstarIconDisplayMode/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
ADMINrole.
| 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) — usernamepassword(string) — password (required for new users)email(string) — email addressenabled(boolean, required) — account activeadmin(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_PASSfrom environment variables — never hardcode - Authentication is HTTP Basic Auth, not API key headers
- The
/rest/server/getendpoint 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/olderThanparameters - OPML import uses
multipart/form-data, all other POST endpoints useapplication/json - The
readTypeparameter is required for entry listing endpoints - Maximum
limitis 1000 entries per request — useoffset+hasMorefor pagination id=allmeans all feeds,id=starredmeans 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