{"openapi":"3.1.0","info":{"title":"FeedNest Public API","version":"1.0.0","description":"The FeedNest Public API gives AI agents, automation platforms, and custom integrations programmatic access to your feeds, articles, highlights, notes, tags, and reading stats.\n\n## Base URL\n\n```\nhttps://feednest.com/api/v1\n```\n\n## Authentication\n\nAll endpoints require a **Bearer token** (API key) in the `Authorization` header. Generate keys in **Settings → Developer API** (requires a Pro subscription).\n\n```\nAuthorization: Bearer fn_live_XXXX\n```\n\nKeys use the format `fn_live_` followed by 32 hex characters. Each key is scoped to specific permissions:\n\n| Scope | Description |\n|-------|-------------|\n| `read` | Read feeds, articles, folders, tags, stats, highlights, notes, saved items |\n| `write` | Mark as read/unread, save/unsave, tag, highlight, add notes, extract content |\n| `ai` | Text-to-speech audio generation |\n\n## Rate Limits\n\n| Endpoint group | Limit |\n|----------------|-------|\n| Default | 1,000 requests/hour per key |\n| AI endpoints | 50 requests/hour |\n| Save URL | 100 requests/hour |\n\nRate-limited responses return `429` with `Retry-After` and `X-RateLimit-*` headers.\n\n## Pagination\n\nAll list endpoints use **keyset pagination**. Pass `limit` (max 100) and use the last item's cursor fields (`before_published_at` + `before_id`) to fetch the next page. Check `meta.has_more` to know if more results exist.\n\n## Error Format\n\n```json\n{\n  \"error\": {\n    \"code\": \"ERROR_CODE\",\n    \"message\": \"Human-readable description.\"\n  }\n}\n```\n\n## Versioning and deprecation\n\nThe API is versioned in the URL path. `/api/v1` is the current stable version, and no breaking change will ever be made inside a version.\n\nBackwards-compatible additions (a new endpoint, a new optional field, a new optional parameter) ship inside `v1` without notice, so clients must ignore unknown response fields rather than failing on them.\n\nA breaking change means a new version path, for example `/api/v2`. When that happens:\n\n| Signal | Meaning |\n|--------|---------|\n| `Deprecation` response header | RFC 9745. Carries the date the version was declared deprecated. |\n| `Sunset` response header | RFC 8594. Carries the date the version stops responding. |\n| `Link` header, `rel=\"successor-version\"` | Points at the replacement endpoint. |\n\nA deprecated version keeps working for **at least 12 months** after the `Deprecation` date. Deprecations are announced on the [changelog](https://feednest.com/changelog), which every API response links via the `X-API-Changelog` header.\n\n## Looking for AI assistant integration?\n\nFeedNest also exposes an **MCP server** at `mcp.feednest.com` for connecting to Claude, ChatGPT, Cursor, and other AI assistants. See the [MCP documentation](https://docs.feednest.com).","contact":{"url":"https://feednest.com"},"license":{"name":"Proprietary"},"x-mcp-server":{"url":"https://mcp.feednest.com","docs":"https://docs.feednest.com"},"x-protected-resource-metadata":"https://www.feednest.com/.well-known/oauth-protected-resource","x-versioning":{"strategy":"uri-path","current":"v1","status":"stable","breaking_changes":"new-version-path","deprecation_headers":["Deprecation","Sunset","Link"],"minimum_sunset_period_days":365,"changelog":"https://feednest.com/changelog"}},"servers":[{"url":"https://feednest.com","description":"Production"}],"tags":[{"name":"Health","description":"Service health checks"},{"name":"Articles","description":"Newsfeed article operations"},{"name":"Highlights","description":"Article highlight management"},{"name":"Notes","description":"Article note management"},{"name":"Tags","description":"Tag management and article tagging"},{"name":"Feeds","description":"RSS feed subscriptions"},{"name":"Folders","description":"Feed folder organization"},{"name":"Saved","description":"Saved/bookmarked articles"},{"name":"Stats","description":"Reading statistics"},{"name":"Output Feeds","description":"Public RSS output feeds"},{"name":"AI","description":"AI-powered features"}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"API key in the format `fn_live_XXXX`. Pass via `Authorization: Bearer fn_live_XXXX`.","x-scopes":{"read":"Read feeds, articles, folders, tags, stats, highlights, notes, saved items","write":"Mark as read/unread, save/unsave, tag, highlight, add notes, extract content","ai":"Text-to-speech audio generation"},"x-token-issuance":{"type":"self-service-api-key","url":"https://www.feednest.com/dashboard/settings/developer-api","documentation":"https://docs.feednest.com/api-keys"}}},"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Request limit per window"},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Remaining requests in current window"},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"UTC epoch seconds when the window resets"},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying (on 429 responses)"}},"parameters":{"articleId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Article ID"},"limit":{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Maximum items to return (1–100)"},"before_published_at":{"name":"before_published_at","in":"query","schema":{"type":"string","format":"date-time"},"description":"Cursor: return items published before this timestamp"},"before_id":{"name":"before_id","in":"query","schema":{"type":"string","format":"uuid"},"description":"Cursor: return items with IDs before this value"},"q":{"name":"q","in":"query","schema":{"type":"string"},"description":"Full-text search query"},"date_from":{"name":"date_from","in":"query","schema":{"type":"string","format":"date"},"description":"Filter: articles published on or after this date"},"date_to":{"name":"date_to","in":"query","schema":{"type":"string","format":"date"},"description":"Filter: articles published on or before this date"},"sort":{"name":"sort","in":"query","schema":{"type":"string","enum":["newest","oldest"],"default":"newest"},"description":"Sort order by published date"},"unread_only":{"name":"unread_only","in":"query","schema":{"type":"string","enum":["true","false"],"default":"false"},"description":"When `true`, only return unread articles"},"count_only":{"name":"count_only","in":"query","schema":{"type":"string","enum":["true","false"],"default":"false"},"description":"When `true`, return only the article count (`{ count, capped }`) instead of the full list. Counts up to 999; `capped` is `true` if more exist."}},"schemas":{"ArticleV1":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":["string","null"]},"source":{"type":["string","null"]},"url":{"type":"string","format":"uri"},"published_at":{"type":["string","null"],"format":"date-time"},"summary_preview":{"type":["string","null"]},"image_url":{"type":["string","null"],"format":"uri"},"is_read":{"type":"boolean"},"is_saved":{"type":"boolean"},"tags":{"type":"array","items":{"type":"string"}},"highlights_count":{"type":"integer"},"notes_count":{"type":"integer"}},"required":["id","title","source","url","published_at","summary_preview","image_url","is_read","is_saved","tags","highlights_count","notes_count"]},"ArticleDetailV1":{"allOf":[{"$ref":"#/components/schemas/ArticleV1"},{"type":"object","properties":{"content":{"type":["string","null"]},"full_content":{"type":["string","null"]},"highlights":{"type":"array","items":{"$ref":"#/components/schemas/HighlightV1"}},"note":{"oneOf":[{"$ref":"#/components/schemas/NoteV1"},{"type":"null"}]},"author":{"type":["string","null"]},"reading_time_seconds":{"type":["integer","null"]}},"required":["content","full_content","highlights","note","author","reading_time_seconds"]}]},"RecentlyReadV1":{"allOf":[{"$ref":"#/components/schemas/ArticleV1"},{"type":"object","properties":{"opened_at":{"type":["string","null"],"format":"date-time"},"reading_time_seconds":{"type":["integer","null"]}},"required":["opened_at","reading_time_seconds"]}]},"HighlightV1":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"text":{"type":"string"},"color":{"type":"string"},"start_position":{"type":["integer","null"]},"end_position":{"type":["integer","null"]}},"required":["id","text","color","start_position","end_position"]},"NoteV1":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"article_id":{"type":"string","format":"uuid"},"content":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"total_notes_count":{"type":"integer"}},"required":["id","article_id","content","created_at","updated_at","total_notes_count"]},"FeedV1":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"url":{"type":"string","format":"uri"},"description":{"type":["string","null"]},"image":{"type":["string","null"],"format":"uri"},"category":{"type":["string","null"]},"unread_count":{"type":"integer"},"folder_ids":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["id","name","url","description","image","category","unread_count","folder_ids"]},"FolderV1":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"color":{"type":"string"},"feed_count":{"type":"integer"}},"required":["id","name","color","feed_count"]},"TagV1":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"color":{"type":"string"},"article_count":{"type":"integer"}},"required":["id","name","color","article_count"]},"SavedArticleV1":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":["string","null"]},"url":{"type":"string","format":"uri"},"source":{"type":["string","null"]},"published_at":{"type":["string","null"],"format":"date-time"},"saved_at":{"type":"string","format":"date-time"},"is_read":{"type":"boolean"},"tags":{"type":"array","items":{"type":"string"}},"has_highlights":{"type":"boolean"},"has_notes":{"type":"boolean"}},"required":["id","title","url","source","published_at","saved_at","is_read","tags","has_highlights","has_notes"]},"OutputFeedV1":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":["string","null"]},"slug":{"type":["string","null"]},"public_url":{"type":"string","format":"uri","description":"Full URL to the public RSS feed: /feed/{username}/{slug}"},"source_type":{"type":"string"},"source_id":{"type":"string"},"created_at":{"type":"string","format":"date-time"}},"required":["id","title","slug","public_url","source_type","source_id","created_at"]},"StatsV1":{"type":"object","properties":{"articles_read_today":{"type":"integer"},"reading_time_today_minutes":{"type":"integer"},"streak_days":{"type":"integer"},"total_feeds":{"type":"integer"},"unread_count":{"type":"integer"},"saved_count":{"type":"integer"}},"required":["articles_read_today","reading_time_today_minutes","streak_days","total_feeds","unread_count","saved_count"]},"ErrorResponse":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["UNAUTHORIZED","FORBIDDEN","SUBSCRIPTION_REQUIRED","NOT_FOUND","RATE_LIMITED","INSUFFICIENT_TOKENS","VALIDATION_ERROR","INTERNAL_ERROR","SERVICE_UNAVAILABLE"]},"message":{"type":"string"},"details":{"type":"object"}},"required":["code","message"]}},"required":["error"]},"ActionResponse":{"type":"object","properties":{"success":{"type":"boolean","const":true}},"required":["success"]}},"responses":{"Unauthorized":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Forbidden":{"description":"Insufficient permissions or subscription required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"ValidationError":{"description":"Input validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"RateLimited":{"description":"Rate limit exceeded","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"security":[{"BearerAuth":[]}],"paths":{"/api/v1/health":{"get":{"operationId":"getHealth","tags":["Health"],"summary":"Health check","description":"Returns service health, API version, and a timestamp. Needs no API key, so an agent can confirm the API is reachable and read the advertised rate-limit policy from the `RateLimit-Policy` header before it has credentials.","security":[],"responses":{"200":{"description":"Service is healthy","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ok"]},"version":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}},"required":["status","version","timestamp"]}}}},"503":{"description":"Service degraded or unavailable","content":{"application/json":{"schema":{"oneOf":[{"type":"object","description":"Database degraded","properties":{"status":{"type":"string","enum":["degraded"]},"version":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}},"required":["status","version","timestamp"]},{"type":"object","description":"API disabled via kill switch","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["SERVICE_UNAVAILABLE"]},"message":{"type":"string"}},"required":["code","message"]},"status":{"type":"string","enum":["unavailable"]},"version":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}},"required":["error","status","version","timestamp"]}]}}}}}}},"/api/v1/articles":{"get":{"operationId":"listArticles","tags":["Articles"],"summary":"List newsfeed articles","description":"Returns the authenticated user's newsfeed: articles from every subscribed feed, newest first. Supports full-text search via `q` and date filtering via `published_after` / `published_before`. Paginate with `limit` plus the `before_published_at` and `before_id` cursors taken from the last item of the previous page, and check `meta.has_more` to know whether another page exists. Requires the `read` scope.","parameters":[{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/before_published_at"},{"$ref":"#/components/parameters/before_id"},{"$ref":"#/components/parameters/unread_only"},{"$ref":"#/components/parameters/count_only"},{"$ref":"#/components/parameters/q"},{"$ref":"#/components/parameters/date_from"},{"$ref":"#/components/parameters/date_to"},{"$ref":"#/components/parameters/sort"}],"responses":{"200":{"description":"Paginated article list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ArticleV1"}},"meta":{"type":"object","properties":{"has_more":{"type":"boolean"}},"required":["has_more"]}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/articles/by-feed/{feedId}":{"get":{"operationId":"listArticlesByFeed","tags":["Articles"],"summary":"List articles for a specific feed","description":"Returns articles belonging to one subscribed feed, newest first. `feedId` is the subscription id from `GET /api/v1/feeds` (the `user_feed` row), not the id of the shared feed record. Uses the same keyset pagination as the newsfeed. Requires the `read` scope.","parameters":[{"name":"feedId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/before_published_at"},{"$ref":"#/components/parameters/before_id"},{"$ref":"#/components/parameters/unread_only"},{"$ref":"#/components/parameters/count_only"},{"$ref":"#/components/parameters/q"},{"$ref":"#/components/parameters/date_from"},{"$ref":"#/components/parameters/date_to"},{"$ref":"#/components/parameters/sort"}],"responses":{"200":{"description":"Paginated article list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ArticleV1"}},"meta":{"type":"object","properties":{"has_more":{"type":"boolean"}},"required":["has_more"]}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/articles/by-folder/{folderId}":{"get":{"operationId":"listArticlesByFolder","tags":["Articles"],"summary":"List articles for a specific folder","description":"Returns articles from every feed inside one folder, newest first. `folderId` comes from `GET /api/v1/folders`. Uses the same keyset pagination as the newsfeed. Requires the `read` scope.","parameters":[{"name":"folderId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/before_published_at"},{"$ref":"#/components/parameters/before_id"},{"$ref":"#/components/parameters/unread_only"},{"$ref":"#/components/parameters/count_only"},{"$ref":"#/components/parameters/q"},{"$ref":"#/components/parameters/date_from"},{"$ref":"#/components/parameters/date_to"},{"$ref":"#/components/parameters/sort"}],"responses":{"200":{"description":"Paginated article list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ArticleV1"}},"meta":{"type":"object","properties":{"has_more":{"type":"boolean"}},"required":["has_more"]}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/articles/by-tag/{tagId}":{"get":{"operationId":"listArticlesByTag","tags":["Articles"],"summary":"List articles with a specific tag","description":"Returns articles the user has tagged with one specific tag, newest first. `tagId` comes from `GET /api/v1/tags`. Uses the same keyset pagination as the newsfeed. Requires the `read` scope.","parameters":[{"name":"tagId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/before_published_at"},{"$ref":"#/components/parameters/before_id"},{"$ref":"#/components/parameters/count_only"},{"$ref":"#/components/parameters/q"},{"$ref":"#/components/parameters/date_from"},{"$ref":"#/components/parameters/date_to"},{"$ref":"#/components/parameters/sort"}],"responses":{"200":{"description":"Paginated article list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ArticleV1"}},"meta":{"type":"object","properties":{"has_more":{"type":"boolean"}},"required":["has_more"]}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/articles/tagged":{"get":{"operationId":"listTaggedArticles","tags":["Articles"],"summary":"List all tagged articles","description":"Returns articles carrying at least one tag, read and unread alike, newest first. Pass a `tagId` to narrow the result to a single tag. Uses the same keyset pagination as the newsfeed. Requires the `read` scope.","parameters":[{"name":"tag_id","in":"query","schema":{"type":"string","format":"uuid"},"description":"Filter to a specific tag"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/before_published_at"},{"$ref":"#/components/parameters/before_id"},{"$ref":"#/components/parameters/count_only"},{"$ref":"#/components/parameters/q"},{"$ref":"#/components/parameters/date_from"},{"$ref":"#/components/parameters/date_to"},{"$ref":"#/components/parameters/sort"}],"responses":{"200":{"description":"Paginated article list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ArticleV1"}},"meta":{"type":"object","properties":{"has_more":{"type":"boolean"}},"required":["has_more"]}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/articles/highlighted":{"get":{"operationId":"listHighlightedArticles","tags":["Articles"],"summary":"List articles with highlights","description":"Returns only the articles that carry at least one highlight, newest first. Useful for rebuilding a reading-notes view without walking the whole newsfeed. Requires the `read` scope.","parameters":[{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/before_published_at"},{"$ref":"#/components/parameters/before_id"},{"$ref":"#/components/parameters/count_only"},{"$ref":"#/components/parameters/q"},{"$ref":"#/components/parameters/date_from"},{"$ref":"#/components/parameters/date_to"},{"$ref":"#/components/parameters/sort"}],"responses":{"200":{"description":"Paginated article list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ArticleV1"}},"meta":{"type":"object","properties":{"has_more":{"type":"boolean"}},"required":["has_more"]}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/articles/noted":{"get":{"operationId":"listNotedArticles","tags":["Articles"],"summary":"List articles with notes","description":"Returns only the articles that carry a note, newest first. Useful for rebuilding a reading-notes view without walking the whole newsfeed. Requires the `read` scope.","parameters":[{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/before_published_at"},{"$ref":"#/components/parameters/before_id"},{"$ref":"#/components/parameters/count_only"},{"$ref":"#/components/parameters/q"},{"$ref":"#/components/parameters/date_from"},{"$ref":"#/components/parameters/date_to"},{"$ref":"#/components/parameters/sort"}],"responses":{"200":{"description":"Paginated article list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ArticleV1"}},"meta":{"type":"object","properties":{"has_more":{"type":"boolean"}},"required":["has_more"]}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/articles/recently-read":{"get":{"operationId":"listRecentlyRead","tags":["Articles"],"summary":"List recently read articles","description":"Returns the articles the user opened most recently, newest first. Paginated by `before_opened_at` rather than `before_published_at`, because the ordering is by reading time and not publication time. The `since` parameter is an alias for `date_from`. Requires the `read` scope.","parameters":[{"$ref":"#/components/parameters/limit"},{"name":"before_opened_at","in":"query","schema":{"type":"string","format":"date-time"},"description":"Cursor: return items opened before this timestamp"},{"$ref":"#/components/parameters/before_id"},{"$ref":"#/components/parameters/q"},{"name":"since","in":"query","schema":{"type":"string","format":"date"},"description":"Alias for `date_from`"},{"$ref":"#/components/parameters/date_from"},{"$ref":"#/components/parameters/date_to"},{"$ref":"#/components/parameters/sort"}],"responses":{"200":{"description":"Paginated recently-read list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RecentlyReadV1"}},"meta":{"type":"object","properties":{"has_more":{"type":"boolean"}},"required":["has_more"]}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/articles/mark-all-read":{"post":{"operationId":"markAllRead","tags":["Articles"],"summary":"Mark all articles as read","description":"Marks every unread article as read in one call, optionally narrowed to a single feed or folder. This is bulk and cannot be undone article by article, so confirm with the user before calling it. Requires the `write` scope.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"feed_id":{"type":"string","format":"uuid"},"folder_id":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"Articles marked as read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/articles/{id}":{"get":{"operationId":"getArticle","tags":["Articles"],"summary":"Get article detail","description":"Returns one article with its full stored content plus the user's highlights, note, tags, and read state. When the source feed truncated the content, the reading time will be under a minute and `POST /api/v1/articles/{id}/extract` can fetch the full text. Requires the `read` scope.","parameters":[{"$ref":"#/components/parameters/articleId"}],"responses":{"200":{"description":"Article detail","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ArticleDetailV1"}},"required":["data"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/articles/{id}/extract":{"post":{"operationId":"extractArticle","tags":["Articles"],"summary":"Extract full article content","description":"Fetches the source URL and parses the full article text, for feeds that publish only a truncated summary. Call it when `GET /api/v1/articles/{id}` returns a reading time under a minute, not for every article. Requires the `write` scope.","parameters":[{"$ref":"#/components/parameters/articleId"}],"responses":{"200":{"description":"Extracted article detail","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ArticleDetailV1"}},"required":["data"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/articles/{id}/read":{"post":{"operationId":"markArticleRead","tags":["Articles"],"summary":"Mark article as read","description":"Marks the article as read for the authenticated user. Idempotent: marking an already-read article succeeds and changes nothing. Requires the `write` scope.","parameters":[{"$ref":"#/components/parameters/articleId"}],"responses":{"200":{"description":"Article marked as read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/articles/{id}/unread":{"post":{"operationId":"markArticleUnread","tags":["Articles"],"summary":"Mark article as unread","description":"Marks the article as unread for the authenticated user. Idempotent: marking an already-unread article succeeds and changes nothing. Requires the `write` scope.","parameters":[{"$ref":"#/components/parameters/articleId"}],"responses":{"200":{"description":"Article marked as unread","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/articles/{id}/save":{"post":{"operationId":"saveArticle","tags":["Articles"],"summary":"Save/bookmark an article","description":"Adds the article to the user's saved list. Idempotent: saving an already-saved article succeeds and creates no duplicate. Requires the `write` scope.","parameters":[{"$ref":"#/components/parameters/articleId"}],"responses":{"200":{"description":"Article saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"unsaveArticle","tags":["Articles"],"summary":"Remove article from saved","description":"Removes the article from the user's saved list. Idempotent: unsaving an article that was not saved succeeds and changes nothing. Requires the `write` scope.","parameters":[{"$ref":"#/components/parameters/articleId"}],"responses":{"200":{"description":"Article unsaved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/articles/{id}/highlights":{"get":{"operationId":"listHighlights","tags":["Highlights"],"summary":"List highlights for an article","description":"Returns every highlight the user has created on one article, each with its exact quoted text and position. Requires the `read` scope.","parameters":[{"$ref":"#/components/parameters/articleId"}],"responses":{"200":{"description":"Highlight list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/HighlightV1"}}},"required":["data"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createHighlight","tags":["Highlights"],"summary":"Create a highlight on an article","description":"Creates a highlight on an article. The `text` must appear verbatim in the article content, so fetch the article with `GET /api/v1/articles/{id}` first and copy the passage exactly. Paraphrased or invented text is rejected with a validation error. Requires the `write` scope.","parameters":[{"$ref":"#/components/parameters/articleId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","maxLength":10000},"color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$","default":"#FFEB3B"}},"required":["text"]}}}},"responses":{"201":{"description":"Highlight created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/HighlightV1"}},"required":["data"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/articles/{id}/highlights/{highlightId}":{"delete":{"operationId":"deleteHighlight","tags":["Highlights"],"summary":"Delete a highlight","description":"Deletes one highlight by id. The highlight must belong to the authenticated user. Requires the `write` scope.","parameters":[{"$ref":"#/components/parameters/articleId"},{"name":"highlightId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Highlight deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/articles/{id}/tags":{"post":{"operationId":"addTagToArticle","tags":["Tags"],"summary":"Add a tag to an article","description":"Attaches an existing tag to an article. Call `GET /api/v1/tags` first and reuse a matching tag rather than creating a near-duplicate. Requires the `write` scope.","parameters":[{"$ref":"#/components/parameters/articleId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"tag_id":{"type":"string","format":"uuid"}},"required":["tag_id"]}}}},"responses":{"200":{"description":"Tag added to article","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/articles/{id}/tags/{tagId}":{"delete":{"operationId":"removeTagFromArticle","tags":["Tags"],"summary":"Remove a tag from an article","description":"Detaches a tag from an article. The tag itself is not deleted and stays available for other articles. Requires the `write` scope.","parameters":[{"$ref":"#/components/parameters/articleId"},{"name":"tagId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Tag removed from article","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/articles/{id}/notes":{"get":{"operationId":"getArticleNote","tags":["Notes"],"summary":"Get the note for an article","description":"Returns the user's note for one article, or a 404 when no note exists. An article carries at most one note. Requires the `read` scope.","parameters":[{"$ref":"#/components/parameters/articleId"}],"responses":{"200":{"description":"Article note (null if none exists)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"oneOf":[{"$ref":"#/components/schemas/NoteV1"},{"type":"null"}]}},"required":["data"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createArticleNote","tags":["Notes"],"summary":"Create or update a note on an article","description":"Creates the note on an article, or replaces the existing one. An article carries at most one note, so this is an upsert rather than an append. Requires the `write` scope.","parameters":[{"$ref":"#/components/parameters/articleId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string","maxLength":50000}},"required":["content"]}}}},"responses":{"201":{"description":"Note created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/NoteV1"}},"required":["data"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"deleteArticleNote","tags":["Notes"],"summary":"Delete the note on an article","description":"Deletes the note attached to an article. Requires the `write` scope.","parameters":[{"$ref":"#/components/parameters/articleId"}],"responses":{"200":{"description":"Note deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/feeds":{"get":{"operationId":"listFeeds","tags":["Feeds"],"summary":"List subscribed feeds","description":"Returns every feed the user subscribes to, with title, site URL, folder assignment, and unread count. The `id` on each item is the subscription id to pass to feed-scoped endpoints. Requires the `read` scope.","parameters":[{"name":"folder_id","in":"query","schema":{"type":"string","format":"uuid"},"description":"Filter feeds by folder"},{"name":"q","in":"query","schema":{"type":"string"},"description":"Search feeds by name"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Maximum items to return (1–100, default 50 for feeds)"},{"name":"before_name","in":"query","schema":{"type":"string"},"description":"Cursor: return the next page of feeds sorted after this name"},{"$ref":"#/components/parameters/before_id"}],"responses":{"200":{"description":"Paginated feed list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/FeedV1"}},"meta":{"type":"object","properties":{"has_more":{"type":"boolean"}},"required":["has_more"]}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/feeds/export-opml":{"get":{"operationId":"exportOpml","tags":["Feeds"],"summary":"Export feeds as OPML","description":"Returns every subscribed feed as an OPML document, the interchange format other RSS readers import. The response is XML rather than the usual JSON envelope. Requires the `read` scope.","responses":{"200":{"description":"OPML document","content":{"application/xml":{"schema":{"type":"string"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/folders":{"get":{"operationId":"listFolders","tags":["Folders"],"summary":"List all folders","description":"Returns the user's folders in display order, each with the number of feeds it contains. Folders group subscriptions and drive `GET /api/v1/articles/by-folder/{folderId}`. Requires the `read` scope.","responses":{"200":{"description":"Folder list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/FolderV1"}}},"required":["data"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/tags":{"get":{"operationId":"listTags","tags":["Tags"],"summary":"List all tags","description":"Returns every tag the user has created, with usage counts. Call this before creating a tag so an equivalent one is reused instead of duplicated. Requires the `read` scope.","responses":{"200":{"description":"Tag list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TagV1"}}},"required":["data"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createTag","tags":["Tags"],"summary":"Create a new tag","description":"Creates a new tag. Tag names are unique per user, so creating a tag whose name already exists returns a validation error rather than a duplicate. Requires the `write` scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":50},"color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$","default":"#6B7280"}},"required":["name"]}}}},"responses":{"201":{"description":"Tag created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TagV1"}},"required":["data"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/tags/{id}":{"delete":{"operationId":"deleteTag","tags":["Tags"],"summary":"Delete a tag","description":"Deletes a tag and detaches it from every article that carried it. The articles themselves are not affected. Requires the `write` scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Tag ID"}],"responses":{"200":{"description":"Tag deleted","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"message":{"type":"string"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/saved":{"get":{"operationId":"listSaved","tags":["Saved"],"summary":"List saved articles","description":"Returns the user's saved articles, most recently saved first, including items saved from the web rather than from a subscribed feed. Requires the `read` scope.","parameters":[{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/before_published_at"},{"$ref":"#/components/parameters/before_id"},{"$ref":"#/components/parameters/q"},{"$ref":"#/components/parameters/date_from"},{"$ref":"#/components/parameters/date_to"},{"$ref":"#/components/parameters/sort"}],"responses":{"200":{"description":"Paginated saved articles","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SavedArticleV1"}},"meta":{"type":"object","properties":{"has_more":{"type":"boolean"}},"required":["has_more"]}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/saved/url":{"post":{"operationId":"saveUrl","tags":["Saved"],"summary":"Save an article by URL","description":"Saves any web URL to the user's saved list, fetching and parsing the page so it reads like a feed article. Creates the article when FeedNest has not seen the URL before, otherwise reuses it. Rate limited to 100 requests per hour. Requires the `write` scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","maxLength":2048}},"required":["url"]}}}},"responses":{"201":{"description":"URL saved","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"article_id":{"type":"string","format":"uuid"},"url":{"type":"string","format":"uri"},"title":{"type":["string","null"]}},"required":["id","article_id","url","title"]}},"required":["data"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/stats":{"get":{"operationId":"getStats","tags":["Stats"],"summary":"Get reading statistics","description":"Returns aggregate reading statistics for the authenticated user: totals for feeds, articles, unread items, saved items, highlights, and notes, plus reading streak data. Useful for building a dashboard or a daily briefing without paging through content. Requires the `read` scope.","responses":{"200":{"description":"User reading stats","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/StatsV1"}},"required":["data"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/output-feeds":{"get":{"operationId":"listOutputFeeds","tags":["Output Feeds"],"summary":"List public output feeds","description":"Returns the public RSS output feeds the user has published from their folders and tags, each with its public URL. Anyone holding that URL can subscribe without a FeedNest account. Requires the `read` scope.","responses":{"200":{"description":"Output feed list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/OutputFeedV1"}}},"required":["data"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/ai/text-to-speech":{"post":{"operationId":"textToSpeech","tags":["AI"],"summary":"Generate audio from an article","description":"Generates a text-to-speech audio file for the given article. Requires the `ai` scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"article_id":{"type":"string","format":"uuid"},"retry":{"type":"boolean","description":"Set true to retry a previously failed generation."}},"required":["article_id"]}}}},"responses":{"200":{"description":"Audio generated or in progress. When `status` is `in_progress`, another user is generating the same audio, so poll again in a few seconds.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"audio_url":{"type":["string","null"],"format":"uri","description":"Signed URL for the audio file. Null when status is in_progress."},"duration_seconds":{"type":"integer","description":"Estimated duration in seconds (based on word count at ~150 WPM)."},"status":{"type":"string","enum":["in_progress"],"description":"Present only when audio is being generated by another request. Poll again shortly."}},"required":["audio_url","duration_seconds"]}},"required":["data"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Audio generation failed (e.g., a previous generation attempt failed). The client may retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}}}