{"openapi":"3.0.0","info":{"title":"Screvi Public API","version":"1.0.0","description":"Public API for reading and writing your Screvi highlights, sources, articles and tags.\n\n## Authentication\n\nSend a scoped API key as `X-API-Key: sk_live_…` or `Authorization: Bearer sk_live_…`. Create one under Settings → API in Screvi.\n\n## Scopes\n\n- `read` — every `GET` endpoint.\n- `write` — `POST`, `PATCH` and `DELETE`. Grant it only to tools you trust to change your library.\n\nThe older account-wide key still works for reads, but is permanently read-only: it is the same secret used in email links and on Kobo devices, so it can never be used to modify data. Requests made with it carry a `Deprecation` header.\n\n## Errors\n\nEvery error response carries a stable `code` alongside the human-readable `error`, so clients can branch on the code rather than on message text. Validation failures (`422`) also include a `fields` object mapping each rejected field to the reason.\n\n## Syncing\n\nPoll `GET /highlights?updated_since=<ISO timestamp>&include_deleted=true`. Deleted highlights come back once more with `deleted: true` so a local mirror can remove its copy; without `include_deleted` they simply vanish and your copy drifts."},"servers":[{"url":"/api/v1","description":"Public API v1"}],"tags":[{"name":"Highlights","description":"Read, search, create, update and delete your highlights."},{"name":"Search","description":"AI-powered semantic search across your highlights."},{"name":"Sources","description":"Browse, create, update and delete your sources (books, podcasts, videos, tweets, custom collections)."},{"name":"Articles","description":"Saved web pages and newsletters: list, read the full text, save by URL, and triage between inbox, Later and archive."},{"name":"Tags","description":"List your tags, create them, and attach or remove them on highlights and articles."},{"name":"Agent Skills","description":"## What is skill.md?\n\n[Agent Skills](https://agentskills.io) is an open standard for giving AI coding agents the context they need to use your API correctly. A `skill.md` file contains structured instructions — endpoint documentation, common patterns, gotchas — that agents load on demand when working with your product.\n\nScrevi publishes a `skill.md` so that tools like Claude Code, Cursor, GitHub Copilot, and other skills-compatible agents can interact with the Screvi API accurately without needing the full documentation in context.\n\n## How it works\n\n1. **Discovery** — The agent fetches `/.well-known/skills/default/skill.md` (or the convenience alias `/skill.md`) from the API host.\n2. **Activation** — When a task involves Screvi, the agent reads the skill file to learn available endpoints, authentication, rate limits, and patterns.\n3. **Execution** — The agent follows the instructions to make correct API calls on behalf of the user.\n\n## Installing the skill\n\nIf your coding agent supports the skills standard, you can install the Screvi skill:\n\n```\nnpx skills add https://api.screvi.com\n```\n\nOr point your agent directly at:\n\n```\nhttps://api.screvi.com/.well-known/skills/default/skill.md\n```\n\n## Endpoints\n\nThe skill file is served at two public URLs (no authentication required):"}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"A scoped API key (`sk_live_…`) created in Screvi settings. Read endpoints also accept the legacy account key, but write endpoints require a scoped key holding the `write` scope."},"BearerAuth":{"type":"http","scheme":"bearer","description":"The same key, sent as `Authorization: Bearer sk_live_…`."}},"schemas":{"Highlight":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"content":{"type":"string","description":"The highlight text"},"note":{"type":"string","nullable":true,"description":"User note attached to the highlight"},"favorite":{"type":"boolean"},"url":{"type":"string","nullable":true},"location":{"type":"number","nullable":true,"description":"Position in the source (Kindle location, page, or offset)"},"date":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"tags":{"type":"array","items":{"$ref":"#/components/schemas/Tag"}},"source":{"$ref":"#/components/schemas/HighlightSource"},"metadata":{"type":"object","description":"Free-form JSON stored with the highlight. Only returned when `include_metadata=true`."}}},"HighlightSource":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"type":{"type":"string","enum":["book","article","tweet","self","podcast","video","custom","youtube","pdf"]},"author":{"type":"string","nullable":true},"image_url":{"type":"string","nullable":true},"url":{"type":"string","nullable":true}}},"Source":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"type":{"type":"string","enum":["book","article","tweet","self","podcast","video","custom","youtube","pdf"]},"author":{"type":"string","nullable":true},"image_url":{"type":"string","nullable":true},"url":{"type":"string","nullable":true},"origin":{"type":"string","nullable":true},"note":{"type":"string","nullable":true,"description":"Note written on the source itself, distinct from notes on its highlights"},"tags":{"type":"array","items":{"$ref":"#/components/schemas/Tag"},"description":"Tags on the source itself. Populated for articles; always empty for books and other import sources, which cannot be tagged yet."},"highlight_count":{"type":"integer"},"created_at":{"type":"string","format":"date-time"},"metadata":{"type":"object","description":"Free-form JSON stored with the source. Only returned when `include_metadata=true`."}}},"Tag":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"color":{"type":"string","nullable":true}}},"TagWithCounts":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"color":{"type":"string","nullable":true},"highlight_count":{"type":"integer"},"article_count":{"type":"integer"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer"},"per_page":{"type":"integer"},"total":{"type":"integer"},"total_pages":{"type":"integer"},"has_more":{"type":"boolean"}}},"SearchResult":{"type":"object","description":"A highlight with search relevance metadata","allOf":[{"$ref":"#/components/schemas/Highlight"},{"type":"object","properties":{"similarity":{"type":"number","nullable":true,"description":"Cosine similarity score (0-1) for semantic matches, null for text matches"},"match_type":{"type":"string","enum":["semantic","keyword","both"],"description":"How this result was matched: by embedding, by keyword, or by both"},"relevance":{"type":"number","description":"Fused rank score; only comparable within one search"},"verified_relevance":{"type":"number","nullable":true,"description":"With verify=true: probability (0-1) that the highlight is about the query. Null when not verified."}}}]},"Article":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string","nullable":true},"author":{"type":"string","nullable":true},"url":{"type":"string"},"canonical_url":{"type":"string","nullable":true},"excerpt":{"type":"string","nullable":true},"status":{"type":"string","enum":["unread","reading","read","archived"],"description":"Reading status"},"home_status":{"type":"string","enum":["inbox","later","archive"],"description":"Where the article sits in the app"},"favorite":{"type":"boolean"},"progress_percentage":{"type":"integer"},"reading_time_minutes":{"type":"integer","nullable":true},"word_count":{"type":"integer","nullable":true},"source_domain":{"type":"string","nullable":true},"site_name":{"type":"string","nullable":true},"language":{"type":"string","nullable":true},"saved_at":{"type":"string","format":"date-time"},"published_date":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time"},"parse_state":{"type":"string","enum":["queued","fetching","parsed","failed"]},"note":{"type":"string","nullable":true},"deleted":{"type":"boolean"},"image_url":{"type":"string","nullable":true},"screvi_url":{"type":"string","description":"Link to the article in the Screvi app"},"highlight_count":{"type":"integer"},"tags":{"type":"array","items":{"$ref":"#/components/schemas/Tag"}}}},"ArticleWithContent":{"allOf":[{"$ref":"#/components/schemas/Article"},{"type":"object","properties":{"content_format":{"type":"string","enum":["html","text"]},"content":{"type":"string","nullable":true,"description":"The body, or null when not fetched yet"},"content_length":{"type":"integer","description":"Total body length in characters"},"content_offset":{"type":"integer"},"content_truncated":{"type":"boolean","description":"True when offset + returned length < content_length"},"parse_error":{"type":"string","nullable":true},"parse_failure":{"type":"object","nullable":true,"description":"Structured reason the body could not be extracted"},"pdf_url":{"type":"string","nullable":true},"epub_url":{"type":"string","nullable":true},"last_fetched_at":{"type":"string","format":"date-time","nullable":true},"started_reading_at":{"type":"string","format":"date-time","nullable":true},"completed_reading_at":{"type":"string","format":"date-time","nullable":true},"summary":{"type":"string","nullable":true,"description":"AI summary, when one has been generated"},"transcript":{"type":"object","nullable":true,"description":"Timed transcript for video and podcast articles"},"highlights":{"type":"array","items":{"$ref":"#/components/schemas/Highlight"}}}}]},"Error":{"type":"object","properties":{"error":{"type":"string","description":"Human-readable message; may be reworded"},"code":{"type":"string","description":"Stable machine-readable code to branch on","enum":["missing_api_key","invalid_api_key","legacy_key_read_only","insufficient_scope","subscription_required","rate_limited","validation_failed","invalid_parameter","not_found","conflict","payload_too_large","internal_error"]},"fields":{"type":"object","additionalProperties":{"type":"string"},"description":"Per-field problems on validation_failed / invalid_parameter"}}}}},"security":[{"ApiKeyAuth":[]}],"paths":{"/.well-known/skills/default/skill.md":{"get":{"summary":"Get skill.md (standard path)","description":"Returns the Screvi Agent Skill file at the standard `.well-known` discovery path. This endpoint requires no authentication.\n\nAgents and the `skills` CLI use this URL to discover and install the Screvi skill automatically.","tags":["Agent Skills"],"security":[],"servers":[{"url":"","description":"API root"}],"responses":{"200":{"description":"The skill.md file contents","content":{"text/markdown":{"schema":{"type":"string"}}}},"404":{"description":"Skill file not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/skill.md":{"get":{"summary":"Get skill.md (convenience alias)","description":"Convenience alias that serves the same skill.md file. No authentication required.","tags":["Agent Skills"],"security":[],"servers":[{"url":"","description":"API root"}],"responses":{"200":{"description":"The skill.md file contents","content":{"text/markdown":{"schema":{"type":"string"}}}},"404":{"description":"Skill file not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tags":{"get":{"summary":"List tags","description":"Every tag you own, with the number of highlights and articles carrying it. Use the returned `id` or `name` with the `tag_id` / `tag` filters on /highlights.\n","tags":["Tags"],"responses":{"200":{"description":"Your tags"},"401":{"description":"Missing or invalid API key"}}},"post":{"summary":"Create a tag","description":"Creates a tag by name. Names are matched case-insensitively against your existing tags: if one already exists the response is 200 with that tag rather than a duplicate. Requires the `write` scope.\n","tags":["Tags"],"security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":100},"color":{"type":"string","description":"Hex colour, e.g. \"#115e59\""}}}}}},"responses":{"200":{"description":"Already existed"},"201":{"description":"Created"},"422":{"description":"Validation failed"}}}},"/highlights":{"get":{"summary":"List highlights","description":"Returns a paginated list of your highlights, ordered by most recent first. Includes the source/article each highlight belongs to, and any tags.\n","tags":["Highlights"],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","default":1},"description":"Page number"},{"in":"query","name":"per_page","schema":{"type":"integer","default":20,"maximum":100},"description":"Number of highlights per page (max 100)"},{"in":"query","name":"source_id","schema":{"type":"string","format":"uuid"},"description":"Filter by source ID"},{"in":"query","name":"favorite","schema":{"type":"boolean"},"description":"Filter by favorite status"},{"in":"query","name":"tag_id","schema":{"type":"string","format":"uuid"},"description":"Only highlights carrying this tag. Each highlight still comes back with its full tag list, not just the matched one.\n"},{"in":"query","name":"tag","schema":{"type":"string"},"description":"Same as tag_id but by tag name, matched case-insensitively against your own tags. Get names from GET /tags.\n"},{"in":"query","name":"updated_since","schema":{"type":"string","format":"date-time"},"description":"Only return highlights updated after this timestamp (ISO 8601)"},{"in":"query","name":"include_deleted","schema":{"type":"boolean"},"description":"Include deleted highlights as tombstones (`deleted: true`). Required for incremental sync, otherwise deletions are invisible and a local mirror drifts.\n"},{"in":"query","name":"include_metadata","schema":{"type":"boolean"},"description":"Include each item's `metadata` object, exactly as it was written via POST or PATCH. Off by default to keep responses small.\n"}],"responses":{"200":{"description":"A paginated list of highlights","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Highlight"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Create a highlight","description":"Creates a highlight. Attach it to an existing source with `source_id`, or to a saved article with `article_id` (not both). Omit both to create a standalone highlight. `tags` takes tag names (matched case-insensitively, created when new) or tag ids. Requires an API key with the `write` scope.\n","tags":["Highlights"],"security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["content"],"properties":{"content":{"type":"string","maxLength":20000},"note":{"type":"string","maxLength":20000},"favorite":{"type":"boolean"},"url":{"type":"string","format":"uri"},"location":{"type":"integer","minimum":0},"date":{"type":"string","format":"date-time"},"source_id":{"type":"string","format":"uuid"},"article_id":{"type":"string","format":"uuid"},"tags":{"type":"array","items":{"type":"string"},"description":"Tag names or ids"},"metadata":{"type":"object"}}}}}},"responses":{"201":{"description":"The created highlight","with its source and tags":null},"403":{"description":"Key lacks the write scope"},"422":{"description":"Validation failed"}}}},"/highlights/random":{"get":{"summary":"Get a random highlight","description":"Returns a single random highlight with its source and tags.","tags":["Highlights"],"responses":{"200":{"description":"A random highlight","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Highlight"}}}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No highlights found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/highlights/{id}":{"get":{"summary":"Get a highlight","description":"Returns a single highlight by ID, including its source and tags.","tags":["Highlights"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Highlight ID"},{"in":"query","name":"include_metadata","schema":{"type":"boolean"},"description":"Include each item's `metadata` object, exactly as it was written via POST or PATCH. Off by default to keep responses small.\n"}],"responses":{"200":{"description":"The requested highlight","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Highlight"}}}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Highlight not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"summary":"Update a highlight","description":"Partial update. Only the fields present in the body are changed; omit a field to leave it alone, or send `null` to clear it. Requires the `write` scope.\n","tags":["Highlights"],"security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"The updated highlight"},"404":{"description":"Not found"},"422":{"description":"Validation failed"}}},"delete":{"summary":"Delete a highlight","description":"Soft delete. The highlight stops appearing in list and search results, and shows up in `GET /highlights?include_deleted=true` as a tombstone so sync clients can mirror the deletion. Idempotent: deleting an already-deleted highlight returns 200. Requires the `write` scope.\n","tags":["Highlights"],"security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Deletion confirmed"},"404":{"description":"Not found"}}}},"/search":{"get":{"summary":"Hybrid semantic + keyword search highlights","description":"Search your highlights using AI-powered semantic search combined with full-text keyword search. Results are ranked using Reciprocal Rank Fusion (RRF) which merges both signals for higher quality results. Searches highlight content, notes, source names, and authors.\n","tags":["Search"],"parameters":[{"in":"query","name":"q","required":true,"schema":{"type":"string","maxLength":1000},"description":"Search query (natural language, max 1000 characters)"},{"in":"query","name":"source","schema":{"type":"string"},"description":"Filter by source name or author (partial match)"},{"in":"query","name":"min_relevance","schema":{"type":"number","minimum":0,"maximum":1,"default":0.72},"description":"Minimum vector similarity threshold (0-1)"},{"in":"query","name":"verify","schema":{"type":"boolean","default":false},"description":"Check each semantic match with a relevance model and drop the ones that are not about the query, so a search with no real matches returns no results. Keyword matches are always kept. Adds ~0.5s.\n"},{"in":"query","name":"page","schema":{"type":"integer","default":1},"description":"Page number"},{"in":"query","name":"per_page","schema":{"type":"integer","default":20,"maximum":50},"description":"Number of results per page (max 50)"}],"responses":{"200":{"description":"Search results with relevance scores","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SearchResult"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"400":{"description":"Missing or invalid query parameter","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sources":{"get":{"summary":"List sources","description":"Returns a paginated list of your sources (books, articles, tweets, etc.) along with the number of highlights in each.\n","tags":["Sources"],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","default":1},"description":"Page number"},{"in":"query","name":"per_page","schema":{"type":"integer","default":20,"maximum":100},"description":"Number of sources per page (max 100)"},{"in":"query","name":"type","schema":{"type":"string","enum":["book","article","tweet","podcast","video","self","custom","youtube","author","topics"]},"description":"Filter by source type"},{"in":"query","name":"search","schema":{"type":"string"},"description":"Search sources by name or author"},{"in":"query","name":"updated_since","schema":{"type":"string","format":"date-time"},"description":"Only return sources updated after this timestamp (ISO 8601)"},{"in":"query","name":"include_deleted","schema":{"type":"boolean"},"description":"Include deleted sources as tombstones (`deleted: true`). Required for incremental sync, otherwise deletions are invisible and a local mirror drifts.\n"},{"in":"query","name":"include_empty","schema":{"type":"boolean"},"description":"Include articles that have no highlights yet. Off by default, since existing sync clients treat \"source\" as \"something with highlights\". Turn it on to reach an article that was tagged or annotated but never highlighted — its `tags` and `note` are otherwise unreachable.\n"},{"in":"query","name":"include_metadata","schema":{"type":"boolean"},"description":"Include each item's `metadata` object, exactly as it was written via POST or PATCH. Off by default to keep responses small.\n"}],"responses":{"200":{"description":"A paginated list of sources","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Source"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Create a source","description":"Creates a source (a book, podcast, video, tweet, or custom collection) to hang highlights off. Saved web articles are not created here: use `POST /api/articles`. Requires the `write` scope.\n","tags":["Sources"],"security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","type"],"properties":{"name":{"type":"string","maxLength":500},"type":{"type":"string","enum":["book","podcast","video","tweet","self","custom"]},"author":{"type":"string","maxLength":300},"url":{"type":"string","format":"uri"},"image_url":{"type":"string","format":"uri"},"isbn":{"type":"string","maxLength":20},"asin":{"type":"string","maxLength":20},"note":{"type":"string","maxLength":10000},"published_date":{"type":"string","format":"date"},"metadata":{"type":"object"}}}}}},"responses":{"201":{"description":"The created source"},"409":{"description":"type self: the user already has a Personal Notes source (its id is in source_id)"},"422":{"description":"Validation failed"}}}},"/sources/{id}":{"get":{"summary":"Get a source","description":"Returns a single source by ID, including its highlights.\n","tags":["Sources"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Source ID"},{"in":"query","name":"page","schema":{"type":"integer","default":1},"description":"Page number for highlights"},{"in":"query","name":"per_page","schema":{"type":"integer","default":50,"maximum":100},"description":"Number of highlights per page (max 100)"},{"in":"query","name":"include_metadata","schema":{"type":"boolean"},"description":"Include `metadata` on the source and on each of its highlights, exactly as written via POST or PATCH. Off by default to keep responses small.\n"}],"responses":{"200":{"description":"The requested source with its highlights","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"allOf":[{"$ref":"#/components/schemas/Source"},{"type":"object","properties":{"highlights":{"type":"array","items":{"$ref":"#/components/schemas/Highlight"}}}}]},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Source not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"summary":"Update a source","description":"Partial update. Only fields present in the body change. `type` cannot be changed after creation, because highlights and review filters are already keyed off it. Requires the `write` scope.\n","tags":["Sources"],"security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"The updated source"},"404":{"description":"Not found"},"422":{"description":"Validation failed"}}},"delete":{"summary":"Delete a source","description":"Soft delete. By default a source with highlights is refused with 409, so a stray call can't take a book's worth of highlights with it; pass `?cascade=true` to delete the source and its highlights together. Requires the `write` scope.\n","tags":["Sources"],"security":[{"ApiKeyAuth":[]}],"parameters":[{"in":"query","name":"cascade","schema":{"type":"boolean"},"description":"Also soft-delete the source's highlights."}],"responses":{"200":{"description":"Deletion confirmed"},"404":{"description":"Not found"},"409":{"description":"Source still has highlights and cascade was not set"}}}},"/highlights/bulk":{"post":{"summary":"Create up to 100 highlights in one request","description":"Batched create for importers and agents. Counts as a single request against the rate limit. The whole batch is one transaction: if any highlight is invalid, nothing is written and the response says which index failed. Each item takes the same fields as `POST /highlights`, including `tags`. Requires an API key with the `write` scope.\n","tags":["Highlights"],"security":[{"ApiKeyAuth":[]}],"responses":{"201":{"description":"The created highlights"},"413":{"description":"More than 100 highlights supplied"},"422":{"description":"Validation failed","with the offending index":null}}}},"/articles":{"get":{"summary":"List saved articles","description":"Your saved articles, newest first by default. Unlike `/sources`, this includes articles with no highlights. Filter by reading status, inbox/later/archive, favourites, tag, or free text over title, author and domain.\n","tags":["Articles"],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","default":1}},{"in":"query","name":"per_page","schema":{"type":"integer","default":20,"maximum":100}},{"in":"query","name":"q","schema":{"type":"string"},"description":"Match title","author or domain":null},{"in":"query","name":"status","schema":{"type":"string","enum":["unread","reading","read","archived"]}},{"in":"query","name":"home_status","schema":{"type":"string","enum":["inbox","later","archive"]}},{"in":"query","name":"favorite","schema":{"type":"boolean"}},{"in":"query","name":"has_highlights","schema":{"type":"boolean"}},{"in":"query","name":"tag","schema":{"type":"string"},"description":"Tag name (case-insensitive) or id"},{"in":"query","name":"updated_since","schema":{"type":"string","format":"date-time"}},{"in":"query","name":"saved_since","schema":{"type":"string","format":"date-time"},"description":"Saved at or after this instant"},{"in":"query","name":"saved_before","schema":{"type":"string","format":"date-time"},"description":"Saved before this instant"},{"in":"query","name":"include_deleted","schema":{"type":"boolean"},"description":"Include tombstones for sync"},{"in":"query","name":"sort_by","schema":{"type":"string","enum":["saved_at","published_date","reading_time_minutes","highlight_count","updated_at"],"default":"saved_at"}},{"in":"query","name":"order","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}}],"responses":{"200":{"description":"A paginated list of articles","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Article"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}}}},"post":{"summary":"Save an article by URL","description":"Saves a web page to the reading inbox. The row is created immediately with `parse_state: queued` and the page is fetched and parsed in the background; read it with `GET /articles/{id}` a few seconds later. Saving a URL that is already in the library returns 200 with the existing article's id and `duplicate: true`. Short links are resolved first. A link to a single social post (a tweet, an Instagram, Threads or Bluesky post, a Substack note) is saved the way the app saves it, as a highlight: the response then has `kind: highlight`, `id` is the highlight id and `source_id` its source. Links to PDFs are stored as PDF articles. Requires the `write` scope.\n","tags":["Articles"],"security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"},"tags":{"type":"array","items":{"type":"string"},"description":"Tag names or ids"}}}}}},"responses":{"200":{"description":"Already saved; returns the existing article's id","url":null,"parse_state":null,"tags and `duplicate":"true`"},"201":{"description":"Saved and queued for parsing"},"422":{"description":"Validation failed"}}}},"/articles/{id}":{"get":{"summary":"Read an article","description":"One article with its body, tags and highlights. `format=text` returns the body as plain text with paragraph breaks (the right choice for feeding a model); `format=html` (default) returns the sanitised HTML. Long bodies can be paged with `offset` and `max_chars`; `content_truncated` says whether there is more. If the body has not been fetched yet, `content` is null and `parse_state` says why.\n","tags":["Articles"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"query","name":"format","schema":{"type":"string","enum":["html","text"],"default":"html"}},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0}},{"in":"query","name":"max_chars","schema":{"type":"integer","minimum":1000}}],"responses":{"200":{"description":"The article","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ArticleWithContent"}}}}}},"404":{"description":"Not found"}}},"patch":{"summary":"Update an article","description":"Triage and metadata edits. `home_status` moves the article between the inbox, Later and the archive, exactly as the app does. `tags` replaces the tag set. The article body is not editable: it is shared between everyone who saved the same page. Requires the `write` scope.\n","tags":["Articles"],"security":[{"ApiKeyAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"home_status":{"type":"string","enum":["inbox","later","archive"]},"favorite":{"type":"boolean"},"title":{"type":"string","maxLength":500,"nullable":true},"excerpt":{"type":"string","maxLength":2000,"nullable":true},"note":{"type":"string","maxLength":10000,"nullable":true},"tags":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"The updated article"},"404":{"description":"Not found"},"422":{"description":"Validation failed"}}}},"/tags/{id}":{"patch":{"summary":"Rename or recolor a tag","description":"Everything carrying the tag keeps it under the new name. Requires the `write` scope.","tags":["Tags"],"security":[{"ApiKeyAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":100},"color":{"type":"string"}}}}}},"responses":{"200":{"description":"The updated tag"},"404":{"description":"Not found"},"422":{"description":"Validation failed","including a name clash":null}}},"delete":{"summary":"Delete a tag","description":"Removes the tag from every highlight and article and deletes it. The highlights and articles themselves are untouched. This is a hard delete. Requires the `write` scope.\n","tags":["Tags"],"security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Deleted","with how many rows carried it":null},"404":{"description":"Not found"}}}},"/highlights/{id}/tags":{"post":{"summary":"Add tags to a highlight","description":"Adds tags without touching the ones already there. Names are matched case-insensitively and created when new. Requires the `write` scope.\n","tags":["Tags"],"security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["tags"],"properties":{"tags":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"The highlight's full tag list after the change"},"404":{"description":"Not found"}}},"delete":{"summary":"Remove tags from a highlight","description":"Removes the named tags. The tags themselves are kept. Requires the `write` scope.","tags":["Tags"],"security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["tags"],"properties":{"tags":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"The highlight's full tag list after the change"},"404":{"description":"Not found"}}}},"/articles/{id}/tags":{"post":{"summary":"Add tags to an article","description":"Same contract as `POST /highlights/{id}/tags`. Requires the `write` scope.","tags":["Tags"],"security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"The article's full tag list after the change"},"404":{"description":"Not found"}}},"delete":{"summary":"Remove tags from an article","description":"Same contract as `DELETE /highlights/{id}/tags`. Requires the `write` scope.","tags":["Tags"],"security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"The article's full tag list after the change"},"404":{"description":"Not found"}}}}}}