Requires migration 033_paper_citations.sql and a separately running citation worker.
| Endpoint | Behavior |
|---|---|
GET /api/papers/{id}/citations?direction=outgoing&limit=20&offset=0 |
Paginated local references or direction=incoming for local citing papers; reads never trigger provider calls |
POST /api/papers/{id}/citations/reindex |
HTTP 202 with OpenAlex/Crossref job summaries; pending/running work is reused |
POST /api/papers/{id}/citations/pdf/reindex |
Explicit opt-in for a managed PDF; defaults to local PyMuPDF with migrations 034 and 035; HTTP 202 with its job summary. Optional ?engine=grobid requires GROBID_URL |
GET /api/citation-jobs/{uuid} |
Current status of a durable job; does not expose lease tokens or captured provider payloads |
The citation page contains items, total, limit, offset, direction, runs, jobs, unresolvedCount, indexedPapers, libraryPapers, and pendingJobs. Each item contains id, title, authors, year, and evidence; evidence includes provider, method, indexedAt, and the original provider reference object (its keys retain provider casing). Run summaries include active, resolution, status, indexedAt, candidate metadata, and messages.
Both endpoints of a link must belong to the source paper's library. Pagination is ordered by year descending, title, then ID; limit is 1–100 and offset is nonnegative. Missing papers/jobs return the existing 404 error contract. Missing citation schema returns 503 with installation guidance. Empty results mean no indexed local links, not absence of citations. See operation and recovery.
PDF extraction returns 503 when its service/migration is unavailable and 422 for an unmanaged PDF. Its evidence provider is grobid; the original reference includes unstructured, title, authors, date, pdf_sha256, parser_version, and tei. Paper responses expose pdfRevision for same-URL replacement invalidation. Raw evidence keeps provider/parser field casing.
All endpoints are prefixed with /api/papers.
| Method | Path | Description |
|---|---|---|
| GET | /api/papers |
List papers |
| POST | /api/papers |
Create a paper manually |
| GET | /api/papers/{id} |
Get a single paper |
| PATCH | /api/papers/{id} |
Update a paper |
| DELETE | /api/papers/{id} |
Delete a paper |
Query parameters:
| Parameter | Type | Description |
|---|---|---|
collection_id |
string | Filter by collection membership |
status |
string | Filter by status (inbox, to-read, read) |
search |
string | Text search on title/authors |
library_id |
string | Filter by library |
Returns an array of Paper objects.
Paper responses include the read-only hasStoredPdf boolean. It identifies a URL
in the configured Supabase PDF bucket or GCS PDF bucket; it does not perform a
live file-existence check. External publisher URLs return false. Clients should
use this field for stored-PDF filters and controls rather than inspecting URL
substrings. No database column or paper-ID migration is required.
Optional query parameter: ?check_duplicates=true
When check_duplicates=true, the server runs deduplication before creating. If a duplicate is found, returns 409 with:
{
"duplicates": [
{
"paper": { ... },
"confidence": "exact",
"matchField": "doi"
}
],
"paper": { ... }
}Request body (PaperCreate):
{
"title": "Attention Is All You Need",
"authors": ["Vaswani, A.", "Shazeer, N."],
"year": 2017,
"publishedDate": "2017-06-12",
"venue": "NeurIPS",
"doi": "10.48550/arXiv.1706.03762",
"arxivId": "1706.03762",
"status": "inbox",
"tags": ["transformers"],
"abstract": "...",
"source": "human",
"pdfUrl": null,
"libraryId": "lib_default"
}Returns the created Paper at 201.
Partial update. All fields are optional. Uses exclude_unset=True semantics — only provided fields are updated.
Request body (PaperUpdate): any subset of Paper fields.
Resolve a DOI, arXiv ID, or URL to paper metadata and add it to the library.
Resolution logic:
- DOI → Crossref Works API
- arXiv ID → arXiv Atom API
- URL → arXiv/doi.org sniff, then OpenReview/Zenodo/HTML meta extraction
Runs three-tier dedup (DOI, arXiv ID, normalized title) before creating. If a duplicate is found, returns 200 with the existing paper plus "alreadyExists": true.
Request body:
{
"identifier": "1706.03762",
"libraryId": "lib_default"
}Response on new paper (201):
{
"id": "p_abc123",
"title": "Attention Is All You Need",
"alreadyExists": false,
...
}Response on duplicate (200):
{
"id": "p_existing",
"title": "Attention Is All You Need",
"alreadyExists": true,
"duplicates": [{ "paper": {...}, "confidence": "exact", "matchField": "doi" }]
}After creation, if the paper has a pdf_url, a background task auto-downloads the PDF to the configured PDF storage provider. If the library has auto_note_enabled, AI notes are generated in the background.
Phase 1: parse a .bib file and return a preview with duplicate detection.
Request: multipart/form-data with:
file: the.bibfilelibrary_id(optional query param): restrict dedup scope to a library
Response: array of entry objects:
[
{
"key": "vaswani2017attention",
"paper": { "title": "...", "authors": [...], ... },
"duplicate": false,
"duplicateId": null,
"duplicateConfidence": null,
"duplicateMatchField": null,
"error": null
}
]Duplicate entries have "duplicate": true, "duplicateId", "duplicateConfidence" ("exact" or "likely"), and "duplicateMatchField" ("doi", "arxiv_id", or "title").
Phase 2: create papers from confirmed BibTeX entries (after user review).
Request body:
{
"entries": [ { "title": "...", "authors": [...], ... } ],
"libraryId": "lib_default"
}Performs intra-batch dedup to skip entries that match papers created earlier in the same import. For arXiv entries, re-resolves via the arXiv API for richer metadata and PDF URL.
Response (201): array of result objects, each with a "status" field:
"created"— paper was created; full paper object included"skipped"— duplicate detected;"reason"and"duplicateId"included"failed"— parse/create error;"error"included
After creation, PDF download and AI note generation are queued as background tasks.
Export papers and websites as a downloadable .bib file.
Query parameters (use one):
| Parameter | Description |
|---|---|
ids |
Comma-separated paper/website/repo IDs (e.g., p_abc,w_def,gh_xyz) |
library_id |
Export all items in a library |
collection_id |
Export all items in a collection |
Returns Content-Type: application/x-bibtex with Content-Disposition: attachment; filename="researchos-export.bib".
Papers become @article or @inproceedings entries. Websites become @misc.
Upload a PDF file for a paper.
Request: multipart/form-data with field file (must be application/pdf or application/octet-stream).
Stores the PDF in the configured storage provider at {paper_id}.pdf, updates papers.pdf_url to the public storage URL, invalidates cached extracted text, then queues AI note generation as a background task.
Returns the updated Paper object.
Remove the paper's PDF from its recorded managed storage location, clear pdf_url, and delete any cached extracted text. Storage failures preserve the database reference for retry.
Returns 204 No Content.
Download the paper's existing external pdf_url into the configured storage provider. PDFs already in either configured managed bucket are rejected as already stored.
Use this for papers imported via DOI/arXiv where the PDF URL was set but not yet downloaded. Returns 422 if the paper has no pdf_url or the PDF is already in storage.
Returns the updated Paper object.
Return cached extraction metadata (not the full text):
{
"paperId": "p_abc123",
"pageCount": 12,
"extractedAt": "2024-03-15T10:42:01Z",
"charCount": 45032
}Returns { "paperId": "...", "pageCount": null, "extractedAt": null, "charCount": 0 } if not yet extracted.
Extract and cache text from the paper's PDF. Uses pymupdf4llm to produce Markdown. Returns the same metadata shape as GET /api/papers/{id}/text.
Extract metadata (title, authors, date, venue, abstract, DOI) from an uploaded PDF using LLM.
Request: multipart/form-data with field file.
Response:
{
"title": "Extracted Title",
"authors": ["Author One", "Author Two"],
"year": 2023,
"publishedDate": "2023-06-15",
"venue": "NeurIPS",
"doi": "10.1234/example",
"abstract": "..."
}Used by the frontend QuickAdd Upload mode to pre-fill the form.
Find related papers using Semantic Scholar or other scholarly APIs.
Query parameters:
| Parameter | Default | Description |
|---|---|---|
limit |
12 | Number of results (1–30) |
Returns a structured related papers result object.
List structured author links for a paper. Returns an array of {link, author} objects.
Link a structured author record to this paper.
Request body:
{
"authorId": "a_xyz789",
"position": 0,
"rawName": "Vaswani, A."
}Returns the created PaperAuthor link at 201. Returns 409 if the author is already linked.
Unlink a structured author from this paper. Returns 204.
See notes.md for paper note endpoints and chat.md for paper chat endpoints.
Paper notes: GET/POST /api/papers/{id}/notes, POST /api/papers/{id}/notes/generate
Paper chat: GET/POST/DELETE /api/papers/{id}/chat
{
"id": "p_abc123",
"title": "Attention Is All You Need",
"authors": ["Vaswani, A.", "Shazeer, N."],
"year": 2017,
"publishedDate": "2017-06-12",
"venue": "NeurIPS",
"doi": "10.48550/arXiv.1706.03762",
"arxivId": "1706.03762",
"status": "inbox",
"tags": ["transformers", "attention"],
"abstract": "...",
"source": "human",
"agentRun": null,
"relevanceScore": null,
"agentReasoning": null,
"rejected": false,
"collections": ["c_ml"],
"pdfUrl": "https://.../storage/v1/object/public/pdfs/p_abc123.pdf",
"githubUrl": null,
"websiteUrl": null,
"links": [{ "name": "Project Page", "url": "https://..." }],
"libraryId": "lib_default",
"createdAt": "2024-03-15T10:00:00Z",
"itemType": "paper"
}