RSSaurus API (v1)
This API lets you read and manage items from RSSaurus.
CLI
RSSaurus also has an official command-line client.
- Repo: https://github.com/RSSaurus/rssaurus-cli
- Homebrew:
brew install RSSaurus/tap/rssaurus
Examples:
rssaurus auth whoami --host https://YOUR_APP_HOST
rssaurus items --host https://YOUR_APP_HOST
rssaurus mark-read --url "https://example.com/article" --host https://YOUR_APP_HOST
Webhooks (push new items)
RSSaurus can send a signed webhook when new feed items are created during a feed refresh.
Create a webhook
- In the RSSaurus web UI, go to Settings → Webhooks
- Add an HTTPS URL (HTTP is rejected)
- RSSaurus will POST when new items are created during feed refresh
Webhook URLs are validated with SSRF protection (private/internal IP ranges are blocked).
Headers
Each webhook request includes:
Content-Type: application/jsonX-RSSaurus-Request-Id: UUID for this delivery attemptX-RSSaurus-Timestamp: unix secondsX-RSSaurus-Signature:sha256=<hex>where:
hex = HMAC_SHA256(secret, "{timestamp}.{raw_body}")
Replay protection is implemented by the receiver: reject requests where the timestamp is too old (e.g. > 5 minutes) and verify the signature.
Event types
feed_items.created
Payload (feed_items.created)
Webhook payloads are intentionally lightweight (no full content):
{
"event_type": "feed_items.created",
"occurred_at": "2026-02-01T05:00:00Z",
"feed": {
"id": 123,
"title": "Example Feed",
"url": "https://example.com",
"feed_url": "https://example.com/feed.xml"
},
"items": [
{
"id": 456,
"guid": "https://example.com/post",
"url": "https://example.com/post",
"title": "An item title",
"published_at": "2026-02-01T04:59:00Z"
}
]
}
Fetch full content
Webhook payloads intentionally do not include full content. If you want item Markdown/content, fetch it via:
GET /api/v1/items/:idGET /api/v1/items/:id.md
Authentication
Use a Bearer token in the Authorization header:
curl -H "Authorization: Bearer YOUR_TOKEN" https://YOUR_APP_HOST/api/v1/me
Tokens are managed in Settings → API Tokens (requires login).
Scopes
Tokens have scopes. Some endpoints require specific scopes.
readendpoints: typicallyfeeds:read/items:readwriteendpoints: typicallyitems:write/saved:write
If a token is missing a scope, you’ll get:
{ "error": "forbidden", "missing_scope": "saved:write" }
Endpoints
GET /api/v1/me
Returns info about the authenticated user.
curl -H "Authorization: Bearer YOUR_TOKEN" \
https://YOUR_APP_HOST/api/v1/me
GET /api/v1/feeds
List feeds.
curl -H "Authorization: Bearer YOUR_TOKEN" \
https://YOUR_APP_HOST/api/v1/feeds
GET /api/v1/items
List items.
Query params:
status:unread|read| (omit for all)limit: default 50, max 200feed_id: filter to a feedcursor: opaque pagination cursor (returned asnext_cursor)
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://YOUR_APP_HOST/api/v1/items?status=unread&limit=50" | jq
GET /api/v1/items.jsonl
List items as JSONL (NDJSON): one JSON object per line.
- Same query params as
GET /api/v1/items - Pagination cursor is returned in
X-Next-Cursor
curl -sS -H "Authorization: Bearer YOUR_TOKEN" \
"https://YOUR_APP_HOST/api/v1/items.jsonl?status=unread&limit=200" \
| head -n 5
To paginate:
cursor=$(curl -sSI -H "Authorization: Bearer YOUR_TOKEN" \
"https://YOUR_APP_HOST/api/v1/items.jsonl?status=unread&limit=200" \
| awk -F': ' 'tolower($1)=="x-next-cursor"{print $2}' | tr -d $'\r')
curl -sS -H "Authorization: Bearer YOUR_TOKEN" \
"https://YOUR_APP_HOST/api/v1/items.jsonl?status=unread&limit=200&cursor=${cursor}" \
| head
GET /api/v1/items/:id
Fetch a single item.
curl -H "Authorization: Bearer YOUR_TOKEN" \
https://YOUR_APP_HOST/api/v1/items/123 | jq
GET /api/v1/items/:id.md
Fetch a single item formatted as Markdown (agent-friendly).
curl -H "Authorization: Bearer YOUR_TOKEN" \
https://YOUR_APP_HOST/api/v1/items/123.md
POST /api/v1/items/:id/read
Mark a single item as read.
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
https://YOUR_APP_HOST/api/v1/items/123/read
POST /api/v1/items/:id/unread
Mark a single item as unread.
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
https://YOUR_APP_HOST/api/v1/items/123/unread
POST /api/v1/items/mark_read
Mark many items as read.
Request body options:
{ "all": true }{ "ids": [1,2,3] }{ "url": "https://example.com/article" }{ "urls": ["https://a", "https://b"] }
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ids":[1,2,3]}' \
https://YOUR_APP_HOST/api/v1/items/mark_read
POST /api/v1/saved_items
Save an item by URL.
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/article"}' \
https://YOUR_APP_HOST/api/v1/saved_items
DELETE /api/v1/saved_items/:id
Delete a saved item.
curl -X DELETE \
-H "Authorization: Bearer YOUR_TOKEN" \
https://YOUR_APP_HOST/api/v1/saved_items/7
Rate limiting
RSSaurus applies two layers of rate limiting:
- Per-token, per-endpoint limits (implemented in the API base controller). When these apply, responses include:
X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-Reset(unix timestamp)
- Global throttles (Rack::Attack) that protect the app from burst abuse. These may return
429before the per-endpoint limiter, and may not includeX-RateLimit-*headers.
If rate limited, you’ll get:
429with{ "error": "rate_limited" }Retry-Afterheader
Subscription access
If the user is out of trial and not subscribed, the API returns:
402with{ "error": "payment_required" }
Errors
401– missing/invalid token:{ "error": "unauthorized" }402– subscription required:{ "error": "payment_required" }403– token missing scope:{ "error": "forbidden", "missing_scope": "..." }429– rate limited:{ "error": "rate_limited" }