Skip to content

HTTP API

The web UI uses this API, and scripts can call it too. Every route except /api/health, /api/login, /api/me and /mcp needs a session when a password is set. Write requests also need the X-CSRF-Token header, which echoes the lxw_csrf cookie.

Most routes map to one linxiv CLI command. The right column names the command or describes what the server does.

Route linxiv command / behaviour
POST /api/password {current, new, remember?} Change the stored admin password (409 when it comes from LINXIV_WEB_PASSWORD(_HASH)); logs out every other session
POST /api/sessions/revoke-others Log out every session except the caller's; returns {revoked}
GET /api/activity?limit, GET /api/activity/export Activity log, newest first / as CSV: every POST/PUT/PATCH/DELETE under /api/ with time, client address, route, path parameters, status (Settings saves: key names only). Kept in <data dir>/logs/activity.jsonl, 5 MiB + one rotated file
DELETE /api/activity {confirm: "delete activity log"} Clear the activity log (the clear is recorded)
GET /api/system/diagnostics Markdown report for bug reports: versions, configuration, library counts, disk, health, running jobs, DNS + TCP 443 reachability of outside services. Secrets and addresses that could locate the server only as set / not set
GET /api/search?q&source=arxiv\|openalex\|crossref&max search
POST /api/papers/{id}/fetch?source= fetch, then paper get
GET /api/papers?q&tag&pdf=has\|none&fulltext&limit&offset&category&sort&dir list filtered server-side; a q of 3+ characters also runs paper search (TeX full text and notes), hits flagged deep_match; fulltext=true returns only the paper search hits. Each paper also gets pdf_file (its PDF's path inside the PDF folder) and title_suspect (the title looks like junk read from the PDF, or unlike the file name)
GET /api/papers/{id}, GET /api/papers/{id}/versions paper get (plus pdf_file and title_suspect), paper versions
DELETE /api/papers/{id} paper delete (soft delete, goes to trash)
GET /api/papers/{id}/pdf pdf path, then serves the file (only from inside the data or PDF dir)
POST /api/papers/{id}/pdf {url?, version?} pdf download (for arXiv papers the URL is derived)
DELETE /api/papers/{id}/pdf pdf delete
PUT /api/papers/{id}/metadata {title, authors[], published, summary, category?, doi?, url?} paper get, then paper repair with the full record and the paper's current tags (repair clears any field it isn't given)
GET /api/papers/{id}/lookup?source=crossref\|arxiv\|openalex\|pubmed\|biorxiv&by=auto\|filename Candidate metadata: doi resolve (by DOI), search (by arXiv id or title), NCBI E-utilities for PubMed (by DOI, then title), api.biorxiv.org (by DOI). by=filename searches by the title in the PDF's file name, ignoring the stored DOI, and adds a last candidate built from the file name and its year folder
GET/POST/DELETE /api/papers/{id}/tags tag list / tag add / tag remove
GET /api/pdfs, GET /api/pdfs/storage pdf list, pdf storage (recounted including subfolders)
POST /api/pdfs/import (multipart file) pdf import
GET /api/pdfs/layout/preview?layout=, POST /api/jobs/pdf-layout PDF folder layout (none, year, journal, year/journal, journal/year): preview, then a job that moves files and updates their stored path
POST /api/import/bibtex, POST /api/import/zotero (multipart file) bibtex import, zotero import (upsert: existing entries are updated)
POST /api/add {input} arXiv id/link: fetch; DOI or doi.org link: doi save; other http(s) link: server downloads the PDF (public hosts only, size-capped), then pdf import
GET /api/stats, /api/categories, /api/tags stats, categories, tag list-all
GET /api/authors?hide_single, GET/PATCH/DELETE /api/authors/{id} author list, author get, author update, author delete (409 while linked to papers)
GET /api/authors/{id}/merge-candidates, POST /api/authors/{id}/merge author merge-candidates (plus same-name hints), author merge
POST /api/orcid/backfill {limit?} orcid backfill (1 to 100; 409 while one runs)
GET /api/projects?status=active\|archived, POST /api/projects, PATCH /api/projects/{id} project list, project create, project update
GET /api/projects/{id}, GET /api/projects/{id}/papers project get; papers = project get + list, filtered to members in project order
POST /api/projects/{id}/papers ({"source_ids": [...]}), DELETE /api/projects/{id}/papers/{source_id} project add-papers (200 with added/failed), project remove-paper
GET /api/projects/{id}/export?format=lxproj\|bibtex\|obsidian\|zotero&pdfs project export [--pdfs], export-bibtex, export-obsidian, export-zotero; written to a temp dir, sent as a download, then deleted
POST /api/projects/import/preview, POST /api/projects/import (multipart file, on_conflict=merge\|overwrite) project import --preview, project import (.lxproj, zip checked before the CLI sees it)
POST /api/projects/{id}/archive, POST /api/projects/{id}/restore, DELETE /api/projects/{id} project archive, project restore, project delete (soft delete, goes to trash)
GET /api/notes?paper=&project=, POST /api/notes, PATCH /api/notes/{id}, DELETE /api/notes/{id} note list, note create [--project-id], note update, note delete (permanent); title/content capped at 100 kB
GET /api/annotations?paper=&project=, POST /api/annotations, PATCH /api/annotations/{id}, DELETE /api/annotations/{id} annotation list/create/update/delete (PDF highlights; anchors validated server-side)
GET /api/graph Knowledge graph of papers, authors and tags, built from list and author list
GET /api/graph/export The same graph as a file: format=graphml (default) or format=csv (zip of nodes.csv and edges.csv); exclude_single_authors=true drops single-paper authors
GET /api/feed, POST /api/feed/dismiss feed get for the stored home feed URL (cached 5 minutes), feed dismiss
GET /api/feed/rules, POST /api/feed/rules, DELETE /api/feed/rules/{id} feed rules list/add/delete
GET /api/versions/new, POST /api/versions/check {limit?}, POST /api/versions/{id}/ack versions new, versions check --limit (1 to 100, default 20; stores new versions it finds), versions ack (a second ack still returns ok)
POST /api/papers/{id}/source {force?} paper fetch-source [--force]: downloads the paper's arXiv LaTeX source and indexes its text for full-text search (400 for non-arXiv papers)
GET /api/sources/pending, GET /api/sources/worker Saved arXiv papers whose source isn't indexed; status of the background full-text indexer
POST /api/jobs/index-sources {limit?}, GET /api/jobs?kind=, GET /api/jobs/{id}, POST /api/jobs/{id}/cancel Background job: one paper fetch-source per pending paper, 7 s apart (409 if one is already running)
GET /api/trash trash list
POST /api/trash/papers/{id}/restore, DELETE /api/trash/papers/{id} trash restore, trash hard-delete (paper must be in the trash)
POST /api/trash/projects/{id}/restore, DELETE /api/trash/projects/{id} trash restore-project, trash hard-delete-project
GET /api/backup, POST /api/backup/import, POST /api/backup/restore (multipart file) backup (database snapshot download), import (merge another database), restore (replace the library; refused while a job runs)
GET /api/settings, PATCH /api/settings settings get, settings update, limited to an allowlist (storage cap, TeX rendering, contact addresses, home feed, search history, full-text worker, AI and MCP settings); API keys are write-only
GET /api/ai/models, POST /api/ai/test, GET /api/ai/schemas Provider model list, connection test, the JSON Schema and example each AI action uses (409 while AI is off)
POST /api/ai/tags/{id}, POST /api/ai/summary/{id}?pdf=, POST /api/ai/cleanup/{id} AI tag suggestions, summary, metadata cleanup; previews only, nothing is written
GET /api/mcp, POST /api/mcp/token, DELETE /api/mcp/token MCP status, create (shown once) or revoke the access token
POST/GET/DELETE /mcp MCP Streamable HTTP endpoint (bearer token; see MCP server)

Paper ids are linXiv source ids such as arxiv:1706.03762. Old-style arXiv ids keep their slash: /api/papers/arxiv:hep-th/9901001.