Paperpile API
The Paperpile REST API gives scripts, integrations, and agents access to the data in your Paperpile libraries. Every reference belongs to a library — your personal library, or a shared library or shared folder you have access to — where it can be filed in folders, tagged with labels, and have files such as PDFs attached. The API lets you list, search, create, update, and delete references, folders, labels, and files, and manage shared libraries, using an API key or an OAuth 2.0 access token.
Staging server
Authentication (Collapsed)
All API requests are authenticated with a Bearer token in the Authorization header:
Authorization: Bearer YOUR_TOKEN
Two kinds of token are accepted, and they behave identically once issued:
| Token | How you get it | Use it for |
|---|---|---|
| API key | Generated in your Paperpile account settings | Scripts and integrations acting as you |
| OAuth 2.0 access token | Issued by the authorization-code flow below | Applications acting on behalf of other users |
Either token grants access to your personal library and all shared libraries you have access to. The same tokens authenticate the MCP endpoint.
OAuth 2.0
Paperpile supports the authorization-code flow with PKCE, and dynamic client registration, so an app can onboard without a manually provisioned client.
Discover the endpoints from the authorization server metadata rather than hard-coding them — it is served at
/.well-known/oauth-authorization-server (and /.well-known/openid-configuration) on the Paperpile API host:
https://stage-api.paperpile.com/.well-known/oauth-authorization-server
It advertises the authorization_endpoint, token_endpoint, and registration_endpoint, along with the supported
parameters:
| Metadata | Value |
|---|---|
response_types_supported |
code |
grant_types_supported |
authorization_code |
code_challenge_methods_supported |
S256 |
token_endpoint_auth_methods_supported |
none, client_secret_basic, client_secret_post |
PKCE is required, and S256 is the only supported challenge method.
Clients that need to discover which authorization server protects a resource can read the Protected Resource
Metadata at /.well-known/oauth-protected-resource on the platform host — this is how MCP clients bootstrap.
Scopes
Each token has a scope that determines which operations it can perform:
| Scope | Allowed methods |
|---|---|
read |
GET, HEAD, OPTIONS |
read_write |
All of the above, plus POST, PUT, PATCH, DELETE |
A read-scoped token used on a write request (POST, PUT, PATCH, DELETE) is rejected with 403 Forbidden
(authorization_error / forbidden).
Errors
| Situation | Status | type / code |
|---|---|---|
Missing Authorization header or malformed Bearer |
401 |
authentication_error / unauthorized |
| Token not recognized | 401 |
authentication_error / unauthorized |
| Token scope too low for the method | 403 |
authorization_error / forbidden |
| Add-on type token | 403 |
authorization_error / forbidden |
Add-on tokens (issued for in-app add-ons) are not accepted on this API and always return 403.
Request and response format (Collapsed)
Every REST endpoint lives under https://stage-api.paperpile.com/v1. The system and protocol endpoints are the exception: they sit at
the root of their hosts, and each one lists its own server.
Requests
Send request bodies as JSON with Content-Type: application/json. Uploading a file is the one exception: it takes
multipart/form-data, with the file in the file part.
PATCH updates are partial. Send only the fields you want to change, and at least one of them.
A query parameter that accepts several values is repeated: ?reference_ids=ID1&reference_ids=ID2.
Responses
Successful responses are JSON, apart from the two downloads: GET /files/{file_id}/content returns the file's bytes,
and GET /files/bulk returns a .tar.gz archive.
| Request | Status | Body |
|---|---|---|
GET |
200 |
The resource, or a list |
POST that creates something |
201 |
The new resource |
PATCH |
200 |
The updated resource |
DELETE |
204 |
Empty |
The exceptions: bulk updates and deletes answer 200 with { "succeeded": <count> }; adding references to a folder or
a label answers 204; and POST /references/save, POST /catalog/lookup, and highlighting answer 200. Each
operation lists its exact responses.
Lists come in an envelope:
{ "data": [], "total": 0, "cursor": null }
cursor is null unless more results remain; see Pagination. GET /libraries and GET /folders/{folder_id}/folders
return a bare array instead.
Identifiers and timestamps
IDs are opaque strings. The personal library's ID is always personal. Timestamps such as created_at are Unix
timestamps in seconds and can have a fractional part, for example 1705312200.512.
Errors (Collapsed)
An error answers with an HTTP status and a JSON body holding an error object:
{
"error": {
"type": "validation_error",
"code": "invalid_request",
"message": "Request validation failed",
"details": {
"target": "body",
"issues": [{ "message": "\"file\" is required", "path": ["file"], "type": "any.required" }]
}
}
}
type is the category and code the specific reason. Branch on those, not on message, which is written for people and
can change. details is optional; for a request that fails validation it names the part of the request that failed
(target) and lists each problem (issues).
| Status | type |
Meaning |
|---|---|---|
400 |
validation_error |
The request, or one of its fields, is invalid |
401 |
authentication_error |
The token is missing or not valid |
403 |
authorization_error |
The token may not do this, for example a read-only token on a write |
404 |
not_found_error |
The resource or the route does not exist |
409 |
conflict_error |
The request conflicts with data that already exists |
409 |
not_ready_error |
Your library has not finished its first sync yet |
429 |
rate_limit_error |
Too many requests; see Rate limiting |
500 |
internal_error |
Something failed inside Paperpile |
502, or upstream status |
api_error |
A service Paperpile relies on failed or refused the request |
503 |
sync_error |
The change could not be synced to Paperpile's servers |
A few responses use an older shape instead, { "error": { "msg": "…", "status": 404 } }: a request to a path outside
/v1 that doesn't exist, some errors from upstream services, and unexpected server errors (500). Check for
error.type first and fall back to the HTTP status.
The MCP endpoint (POST /) reports rate limiting as a JSON-RPC error, and its 401 responses carry a WWW-Authenticate
header pointing to the OAuth protected resource metadata.
Pagination (Collapsed)
List endpoints accept a limit parameter to control page size: default 50, max 100. Values above 100 are rejected with a 400.
Most list responses share a common envelope:
| Field | Description |
|---|---|
data |
Array of results |
cursor |
Opaque token to fetch the next page, or null when no more pages |
total |
Total count of matching items |
Which lists are actually paginated
Only the references list is cursor-paginated. The other list endpoints currently return all matching rows in a single response with cursor: null:
| Endpoint | Behavior |
|---|---|
GET /references |
Cursor-paginated. cursor is non-null while more pages remain. |
GET /folders |
Returns all rows at once; cursor is always null. |
GET /labels |
Returns all rows at once; cursor is always null. |
GET /files |
Returns all rows at once; cursor is always null. |
GET /folders/{folder_id}/folders |
Returns a bare JSON array — no {data, total, cursor} envelope. |
Example references response:
{
"data": [...],
"cursor": "eyJpZCI6MTIzFQ",
"total": 250
}
To get the next page of references, pass the cursor back:
GET /references?cursor=eyJpZCI6MTIzfQ
When cursor comes back null, there are no further pages.
Rate limiting (Collapsed)
API requests are rate limited per API key (falling back to per user). The default limit is 200 requests per 60-second window.
Every response includes headers describing your current limits:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum requests allowed per window |
X-RateLimit-Remaining |
Requests remaining in the current window |
X-RateLimit-Reset |
Unix time, in seconds, when the current window resets |
When you exceed the limit you receive a 429 Too Many Requests response with a Retry-After header
giving the number of seconds to wait before retrying. The body uses the standard error envelope:
{
"error": {
"type": "rate_limit_error",
"code": "rate_limited",
"message": "Rate limit exceeded"
}
}
The MCP endpoint shares the same per-key limit and headers, but answers 429 as a JSON-RPC error object instead:
{
"jsonrpc": "2.0",
"id": null,
"error": { "code": -32000, "message": "Rate limit exceeded. Retry after 42 seconds." }
}
Field selection (Collapsed)
Field selection lets you limit the response to only the fields you need. Use fields on list endpoints:
GET /references?fields=id&fields=title&fields=author
When fields is specified, only the listed fields are included in each result object. id is always included; for references, type is always included too.
Expandable fields return IDs by default. Use expand to get the full related objects:
GET /references?expand=folders&expand=labels
GET /files?expand=reference
When you combine fields with expand, every expanded field must also be listed in fields — otherwise the request returns 400. For example: GET /references?fields=id&fields=folders&expand=folders.
Search query syntax (Collapsed)
The q parameter used for /references supports the following search syntax.
Basic search
A basic query consists of one or more search terms which will search across all fields:
machine learning neural networks
Field-specific search Prefix search terms to limit search to specific fields:
title:"machine learning"
author:Smith
abstract:CRISPR
journal:Nature
year:2020
publisher:Springer
| Prefix | Searches |
|---|---|
title: |
Title only |
author: |
Author names |
abstract: |
Abstract text |
journal: |
Journal name |
year: |
Publication year |
publisher: |
Publisher name |
note: |
Your notes on the reference |
Phrase search
Use double quotes to match only references with exact phrases:
"RNA structure" "neural network"
Negative search
Exclude terms with not:
machine learning not:reinforcement
not:Smith
Combining operators You can mix and match operators and search terms:
author:Smith title:"deep learning" transformers not:year:2019
Resource and endpoint conventions (Collapsed)
Your personal library or a library you name
Paths without a library act on your personal library: GET /references, POST /folders, and so on. Every
reference, folder, label, file, and search endpoint also has a long form that names the library, for working in a shared
library or a shared folder:
GET /references → your personal library
GET /libraries/{library_id}/references → the library you name
Apart from library_id, the two forms take the same parameters and request bodies and return the same responses.
personal is a valid library_id, so GET /libraries/personal/references is the same as GET /references. This
reference documents the short form only; GET /libraries lists the libraries you can name.
File endpoints with a reference in the path
File endpoints also accept a reference in the path, /references/{reference_id}/files/…:
GET /references/{reference_id}/fileslists that reference's files, the same asGET /files?reference={reference_id}.-
The single-file endpoints,
/references/{reference_id}/files/{file_id}and below, find the file by its ID and answer exactly like their/files/{file_id}form. - Uploading only exists with a reference,
POST /references/{reference_id}/files, because every file belongs to one.
Query filters and nested paths
Some lists can be reached through a query filter or through a nested path:
| Query filter | Nested path |
|---|---|
GET /folders?parent={id} |
GET /folders/{folder_id}/folders |
GET /references?folder={id} |
GET /folders/{folder_id}/references |
GET /references?label={id} |
GET /labels/{label_id}/references |
GET /files?reference={id} |
GET /references/{reference_id}/files |
Each pair returns the same items, in the same envelope, with one exception: GET /folders/{folder_id}/folders returns a
bare array rather than the { data, total, cursor } envelope of GET /folders?parent={id}.
Account (Collapsed)
The account a token belongs to, and what that token is allowed to do.
GET /me is the cheapest way to verify a token: it reads no library data, so it answers immediately
whatever the size of the library behind it — unlike every other endpoint here, which opens the
library to serve the request.
Use it to confirm a key still works, to show which account a stored key belongs to, and to read the
token's scope before attempting a write that a read token would be refused.
Libraries (Collapsed)
Libraries are the top-level container for organizing data. Each user has one personal library and can create or join shared libraries for collaboration.
The type field indicates whether a library is personal, shared, or shared_folder. If a user shares a folder
from their personal library, that folder appears as a library of type shared_folder to the other collaborators.
The personal library always has the id personal. It cannot be created, renamed, or deleted. Create, update, and
delete operate on shared libraries and shared folders only, and require a token with the read_write scope.
GET /libraries returns the complete list of libraries you can access in a single response — it is not paginated.
Other resources (references, folders, labels, and files) can be addressed within a specific library via the
/libraries/{library_id}/... paths; without the prefix they default to your personal library. See the
Library object for the full schema.
References (Collapsed)
References are the core resource in your library. They represent citable sources like journal articles, books, conference papers, websites, datasets, and more.
Each reference has a type field (e.g., article, book, website) that determines its available fields. Common
fields like title, author, and doi appear across most types, while others are type-specific (e.g., journal for
articles, isbn for books).
Beyond bibliographic data, references include organizational metadata folders, labels, starred, and trashed
flags—as well as associated files (PDFs).
To create a reference, only type and title are required. Updates accept any mutable field but must include at least
one; the type of an existing reference cannot be changed.
To add several references at once, POST /references/save accepts any combination of dois (resolved against
Paperpile's metadata service), a bibtex document, and complete references objects — up to 50 per source. Results are
grouped by source and reported per entry: dois keyed by DOI, bibtex by citation key, and references in the same
order as the entries you sent, each created, duplicate, or failed. labels, folders, and note given on the
request apply to everything saved, in addition to anything already set on an individual entry. An entry already in your
library is reported as duplicate rather than inserted; pass skipDuplicates=false to always insert. A request must
carry at least one of dois, bibtex, or references.
A request-level note is appended to whatever note the entry already carries — a BibTeX note field or a note on a
supplied reference object is never overwritten.
References created through POST /references and POST /references/save are marked for a PDF crawl: the next time your
Paperpile browser extension syncs, it looks for each new reference's full-text PDF and attaches it. Pass
findPdfs=false to skip that. The crawl runs after the response is sent and its result is not reported here — it needs
a signed-in extension to be running, and not every reference has a reachable PDF. Poll GET /references/{id}/files to
see whether one arrived.
A DOI the metadata service has no record for fails with doi_not_resolved, while a DOI whose lookup could not be
completed — the service was unreachable or returned an error — fails with reference_lookup_failed, so a transient
outage is distinguishable from a DOI that genuinely has no match and is worth retrying.
Identifier and date fields are validated on both create and update, matching the Paperpile app. Identifiers must be
well-formed — for example, doi like 10.1000/xyz123, pmid as 6 to 8 digits, pmc like PMC1234567, and isbn a
valid ISBN-10 or ISBN-13; other identifiers (arxivid, ismn, zbl, mr, lccn) must match their standard formats.
Date fields (published, accessed, originally_published, conference_date, filed) accept a partial date YYYY,
YYYY-MM, or YYYY-MM-DD, with month 01 to 12, day 01 to 31, and year 2030 or earlier. Invalid values are rejected
with a 400 validation_error rather than being silently dropped; each field's description states its specific rule.
Fields are also checked against the reference's type: a field that doesn't apply to the type (for example booktitle on
an article) is rejected with a 400 rather than silently ignored. Each type's valid fields are those shown in its
schema below.
The reference lists — GET /references and the folder- and label-scoped variants (/folders/{folder_id}/references,
/labels/{label_id}/references) — are the only cursor-paginated lists in this API. Use limit (1–100, default 50)
together with the cursor from the previous response to page through results; cursor is null on the final page. Use
expand (folders, labels) to inflate ID arrays into full objects, and fields to limit the returned columns.
Bulk operations (PATCH /references/bulk, DELETE /references/bulk) return 200 with { "succeeded": <count> }.
To work in a shared library instead of your personal library, see Resource and endpoint conventions.
See the Reference object for the full schema.
Folders (Collapsed)
Folders organize references into a hierarchical structure, similar to directories in a file system. Folders can contain subfolders.
A reference can belong to multiple folders without duplication. Removing a reference from a folder or deleting the
folder does not delete the reference itself. Deleting a folder detaches its references by default; pass
trash_references=true to move those references (and its subfolders' references) to the trash instead.
The folders list is not paginated — every matching folder is returned in a single response and cursor is always
null. The references inside a folder (GET /folders/{folder_id}/references) are paginated like the main references
list and return a real cursor. GET /folders/{folder_id}/folders returns the direct subfolders as a bare array (no
pagination envelope).
Every folder operation is also available scoped to a specific library under /libraries/{library_id}/folders. The
non-scoped form operates on your personal library; the scoped form targets one library, such as a shared library.
See the Folder object for the full schema.
Labels (Collapsed)
Labels are colored tags for organizing references. Unlike folders, labels have a flat structure with no hierarchy.
A reference can have multiple labels. Removing a label from a reference or deleting the label does not delete the
reference itself. Deleting a label keeps the labeled references by default; pass trash_references=true to move those
references to the trash instead.
Labels are best suited for cross-cutting themes like status ("To Read", "Important") or topics that span multiple folders.
The labels list is not paginated — every label is returned in a single response and cursor is always null, so the
limit and cursor parameters do not apply. The references carrying a label (GET /labels/{label_id}/references) are
paginated like the main references list and return a real cursor. To add or remove a label on references, send the
reference IDs in the request body as { "ids": [...] }.
Every label operation is also available scoped to a specific library under /libraries/{library_id}/labels. The
non-scoped form operates on your personal library; the scoped form targets one library, such as a shared library.
See the Label object for the full schema.
Files (Collapsed)
Files are documents attached to references, typically PDFs of the referenced work.
Each reference can have one primary file and any number of supplementary files. The primary field distinguishes the
main PDF (e.g., the article itself) from supplementary materials, and is the only file field you can change after
upload — set primary: true with PATCH /files/{file_id}. The filename is fixed at upload time.
Upload a file by POSTing a multipart/form-data body (with the binary in the file part) to
/references/{reference_id}/files. Files always belong to a reference, so there is no non-scoped upload endpoint;
POST /files is not available.
File lists are not paginated: GET /files (and its reference- and library-scoped variants) returns every matching
file in a single response, and the cursor field is always null. Filter the global list with reference to get
the files for one reference, or use the dedicated GET /references/{reference_id}/files alias.
To read normalized searchable PDF text, use GET /files/{file_id}/text_content with page, or
page_start and page_end for an inclusive span. Optional start/end are page-local UTF-8 byte
bounds, inclusive/exclusive. For spans they affect only the first/last page. start must be zero or
more; a negative end counts from that page's end, and omitting end includes its final character.
Omit selectors and bounds for all pages.
Fulltext search results' fileId, pageNumber, and charOffsets map directly to the file, page,
and byte bounds. These offsets enclose matched tokens, not necessarily the entire displayed snippet.
Widen with start: Math.max(0, charOffsets[0] - context) and end: charOffsets[1] + context.
A negative start is rejected. For annotation hits, request their whole page.
| Limit | Value | Behaviour |
|---|---|---|
max_bytes |
default 20,000; maximum 100,000 | Shared text budget, spent in page order; invalid budgets rejected |
| Page numbers | Positive safe integers | Exact pages and span starts must exist; span ends clamp to the last available page at or before page_end |
| Output entries | At most 100 | Independent of the byte budget |
start / end |
Safe integers; start zero or more |
Resolve a negative end from the page end, clamp, then align to UTF-8 characters |
Responses contain file_id, offset_unit: "utf8_bytes", source, total_pages, and a content array
with one entry per page. Each entry includes range_index, page, actual start/end, page_bytes,
text, and truncated. Resume a truncated page from its returned end. Blank pages are retained.
Extraction covers at most 100 pages; total_pages is the available extraction, not the PDF's full length.
The reader uses stored extraction and the same normalization as search. Missing extraction returns
404 unless the host has an explicitly configured compatible Poppler fallback. Normalization can alter
accents and ligatures; raw exported text and PDF text need not have matching coordinates. For durable
locations, cite the page or highlight the passage with POST /files/{file_id}/highlight.
To work in a shared library, or to address a file through its reference, see Resource and endpoint conventions.
See the File object for the full schema.
Search (Collapsed)
GET /search looks through everything in a library at once — reference metadata, the text of attached PDFs, and PDF
annotations — and tells you for each result where it matched. Use it to find passages and quotes; to filter references
by their fields, use GET /references.
Full-text results carry the file, page, and byte offsets of the match, which GET /files/{file_id}/text_content accepts
to read the surrounding text.
Catalog (Collapsed)
The Catalog API searches external bibliographic catalogs rather than your own library. Nothing here reads or writes your
data, so these endpoints need no library context and a token with the read scope is enough.
Results use the same reference schema as your library, so a paper found through the catalog can be handed straight to
POST /references to save it.
GET /catalog/search runs a metadata search; repeat query_list to run several queries in one request.
GET /catalog/citations and GET /catalog/references walk the two directions of the citation graph for a DOI —
citations returns the papers that cite it, references returns the works it cites. Both can run to thousands of results,
so they are paged: read on with offset.
POST /catalog/lookup resolves a list of DOIs to full metadata without saving anything. DOIs with no matching record
come back in not_found rather than being silently dropped, so the response accounts for every DOI you asked about.
Search for papers
Searches external bibliographic catalogs. Results use the same reference schema as your library, so a paper found here can be passed straight to POST /references.
Repeat query_list to run several queries in parallel; limit then applies to each one and the combined results are deduplicated, so the response can hold fewer papers than limit × queries — duplicates_removed says how many were merged.
PubMed publishes no citation counts, so min_citation_count and a citation-count sort are rejected for that engine rather than silently ignored.
Query Parameters
- Type: stringquery
A search query to find papers. May be left blank when searching by author or journal.
- Type: array of stringquery
_list Search queries to run in parallel, at most 10. Repeat the parameter for several. Overrides
query.limitapplies to each query, and the combined results are deduplicated, so overlapping queries return fewer papers thanlimit× queries —duplicates_removedsays how many were merged. - Type: integerlimitmin:1max:200
The maximum number of results to return per query. Defaults to 10, maximum 200.
- Type: integeroffsetmin:0
How many results to skip, for reading past the first page. Applies per query, as
limitdoes. Compare against the per-query match counts to know whether more remain. - Type: "semantic-scholar" or "open-alex" or "pubmed"enumengine
Which catalog to search. Semantic Scholar suits general queries; OpenAlex resolves an author name to a specific researcher, where Semantic Scholar matches it as free text; PubMed covers the life sciences.
- Type: stringyear
_range Filter by year or year range (e.g. "2020", "2015-2024").
- Type: stringauthor
Filter by author name or OpenAlex author ID.
- Type: stringjournal
Filter by journal/venue name or OpenAlex source ID.
- Type: integermin
_citation _count min:0Only return papers with at least this many citations. Not supported by the PubMed engine.
- Type: "relevance" or "date" or "citation-count"enumsort
How to sort the results. Citation-count sort is not supported by the PubMed engine.
- Type: "CONCISE" or "DETAILED"enumresponse
_format CONCISE returns title, year, citation count and an author summary. DETAILED returns full metadata, including the abstract where the catalog publishes one.
Responses
- application/json
- application/json
- application/json
- application/json
- application/json
curl 'https://stage-api.paperpile.com/v1/catalog/search?limit=10&offset=0&engine=semantic-scholar&sort=relevance&response_format=CONCISE' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN'
{
"papers": [
{
"citation_count": 1,
"reference_count": 1,
"query_index": 1,
"abstract": "string",
"accessed": "string",
"address": "string",
"affiliation": "string",
"application_number": "string",
"archive": "string",
"archivePrefix": "string",
"archive_location": "string",
"archive_place": "string",
"arxiv_category": "string",
"arxivid": "string",
"assignee": "string",
"associated_doi": "string",
"author": [
{
"family": "string",
"given": "string",
"initials": "string",
"kind": "person",
"orcid": "string",
"suffix": "string"
}
],
"booktitle": "string",
"call_number": "string",
"chapter": "string",
"class": "string",
"conference": "string",
"conference_date": "string",
"conference_location": "string",
"contributor": [
{
"family": "string",
"given": "string",
"initials": "string",
"kind": "person",
"orcid": "string",
"suffix": "string"
}
],
"copyright": "string",
"court": "string",
"database": "string",
"department": "string",
"doi": "string",
"edition": "string",
"editor": [
{
"family": "string",
"given": "string",
"initials": "string",
"kind": "person",
"orcid": "string",
"suffix": "string"
}
],
"eprint": "string",
"filed": "string",
"folders": [
"string"
],
"foreign_patents": "string",
"genre": "string",
"history": "string",
"howpublished": "string",
"institution": "string",
"international_class": "string",
"isbn": "string",
"isbn_alt": "string",
"ismn": "string",
"issue": "string",
"issuing_authority": "string",
"journal": "string",
"jurisdiction": "string",
"keywords": "string",
"labels": [
"string"
],
"language": "string",
"lccn": "string",
"legislative_body": "string",
"medium": "string",
"mr": "string",
"note": "string",
"num_pages": "string",
"num_volumes": "string",
"number": "string",
"original_address": "string",
"original_publisher": "string",
"original_title": "string",
"originally_published": "string",
"pages": "string",
"patent_number": "string",
"patent_publication_type": "string",
"pmc": "string",
"pmid": "string",
"priority_numbers": "string",
"published": "string",
"publisher": "string",
"running_time": "string",
"scale": "string",
"school": "string",
"search_class": "string",
"section": "string",
"series": "string",
"series_editor": [
{
"family": "string",
"given": "string",
"initials": "string",
"kind": "person",
"orcid": "string",
"suffix": "string"
}
],
"series_number": "string",
"session": "string",
"socialmedia_handle": "string",
"starred": true,
"states": "string",
"status": "string",
"subtype": "string",
"title": "string",
"titleshort": "string",
"translator": [
{
"family": "string",
"given": "string",
"initials": "string",
"kind": "person",
"orcid": "string",
"suffix": "string"
}
],
"trashed": true,
"type": "article",
"url": "string",
"user_citekey": "string",
"version": "string",
"volume": "string",
"zbl": "string"
}
],
"total": 1,
"query_matches": [
{
"query": "string",
"matched": 1
}
],
"engine": "semantic-scholar",
"queries": [
"string"
],
"failed_queries": [
"string"
],
"unresolved_ids": [
"string"
],
"duplicates_removed": 1
}Matching papers
Get papers citing a DOI
Returns the papers that cite the given DOI — who built on this work. Use /catalog/references for the opposite direction.
A heavily cited paper runs to tens of thousands of citations, so results are paged: read on with offset while has_more is true. total is present where the catalog counts its results; Semantic Scholar's citation endpoints do not, so rely on has_more.
Catalog coverage differs, so a DOI one catalog has no record for fails with 404 rather than returning an empty list — "nothing cites this paper" and "this catalog does not have this paper" are different answers. A DOI minted by arXiv is often absent even where the paper itself is present.
Query Parameters
- Type: stringdoi
The DOI of the paper to read citations for.
- Type: "CITATION_COUNT" or "LATEST"enumsort
How to sort the citations. Only the OpenAlex engine can sort a citation list.
- Type: integerlimitmin:1max:50
The maximum number of results to return. Defaults to 10, maximum 50.
- Type: integeroffsetmin:0
How many citations to skip, for reading past the first page. A heavily cited paper has far more than one page of them —
has_moresays whether to keep going, andtotalsays how many where the catalog counts them. - Type: "semantic-scholar" or "open-alex"enumengine
Which catalog to read citations from. Only OpenAlex can sort, so
sortis ignored elsewhere. - Type: stringyear
_range Filter citations by year or year range (e.g. "2020", "2015-2024").
- Type: "CONCISE" or "DETAILED"enumresponse
_format The level of detail to return for each citing paper.
Responses
- application/json
- application/json
- application/json
- application/json
- application/json
curl 'https://stage-api.paperpile.com/v1/catalog/citations?sort=CITATION_COUNT&limit=10&offset=0&engine=semantic-scholar&response_format=CONCISE' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN'
{
"papers": [
{
"citation_count": 1,
"reference_count": 1,
"query_index": 1,
"abstract": "string",
"accessed": "string",
"address": "string",
"affiliation": "string",
"application_number": "string",
"archive": "string",
"archivePrefix": "string",
"archive_location": "string",
"archive_place": "string",
"arxiv_category": "string",
"arxivid": "string",
"assignee": "string",
"associated_doi": "string",
"author": [
{
"family": "string",
"given": "string",
"initials": "string",
"kind": "person",
"orcid": "string",
"suffix": "string"
}
],
"booktitle": "string",
"call_number": "string",
"chapter": "string",
"class": "string",
"conference": "string",
"conference_date": "string",
"conference_location": "string",
"contributor": [
{
"family": "string",
"given": "string",
"initials": "string",
"kind": "person",
"orcid": "string",
"suffix": "string"
}
],
"copyright": "string",
"court": "string",
"database": "string",
"department": "string",
"doi": "string",
"edition": "string",
"editor": [
{
"family": "string",
"given": "string",
"initials": "string",
"kind": "person",
"orcid": "string",
"suffix": "string"
}
],
"eprint": "string",
"filed": "string",
"folders": [
"string"
],
"foreign_patents": "string",
"genre": "string",
"history": "string",
"howpublished": "string",
"institution": "string",
"international_class": "string",
"isbn": "string",
"isbn_alt": "string",
"ismn": "string",
"issue": "string",
"issuing_authority": "string",
"journal": "string",
"jurisdiction": "string",
"keywords": "string",
"labels": [
"string"
],
"language": "string",
"lccn": "string",
"legislative_body": "string",
"medium": "string",
"mr": "string",
"note": "string",
"num_pages": "string",
"num_volumes": "string",
"number": "string",
"original_address": "string",
"original_publisher": "string",
"original_title": "string",
"originally_published": "string",
"pages": "string",
"patent_number": "string",
"patent_publication_type": "string",
"pmc": "string",
"pmid": "string",
"priority_numbers": "string",
"published": "string",
"publisher": "string",
"running_time": "string",
"scale": "string",
"school": "string",
"search_class": "string",
"section": "string",
"series": "string",
"series_editor": [
{
"family": "string",
"given": "string",
"initials": "string",
"kind": "person",
"orcid": "string",
"suffix": "string"
}
],
"series_number": "string",
"session": "string",
"socialmedia_handle": "string",
"starred": true,
"states": "string",
"status": "string",
"subtype": "string",
"title": "string",
"titleshort": "string",
"translator": [
{
"family": "string",
"given": "string",
"initials": "string",
"kind": "person",
"orcid": "string",
"suffix": "string"
}
],
"trashed": true,
"type": "article",
"url": "string",
"user_citekey": "string",
"version": "string",
"volume": "string",
"zbl": "string"
}
],
"total": 1,
"has_more": true,
"engine": "semantic-scholar"
}Citing papers
Get the reference list of a DOI
Returns the works the given DOI cites — its bibliography. Use /catalog/citations for the opposite direction.
A review cites several hundred works, so results are paged: read on with offset while has_more is true. The list has no sort, so walking it in order covers all of it.
Some publishers have their reference lists withheld from Semantic Scholar; that fails with 404 rather than returning an empty bibliography, and engine=open-alex usually has the list.
Catalog coverage differs, so a DOI one catalog has no record for fails with 404 rather than returning an empty list — "nothing cites this paper" and "this catalog does not have this paper" are different answers. A DOI minted by arXiv is often absent even where the paper itself is present.
Query Parameters
- Type: stringdoi
The DOI of the paper to read the reference list of.
- Type: integerlimitmin:1max:50
The maximum number of results to return. Defaults to 10, maximum 50.
- Type: integeroffsetmin:0
How many references to skip, for reading past the first page. A review cites hundreds of works —
has_moresays whether to keep going. The list has no sort, so walking it in order covers all of it. - Type: "semantic-scholar" or "open-alex"enumengine
Which catalog to read the reference list from. Semantic Scholar is the default, but it withholds the list for some publishers — use
open-alexif it reports the references as unavailable. - Type: "CONCISE" or "DETAILED"enumresponse
_format The level of detail to return for each referenced paper.
Responses
- application/json
- application/json
- application/json
- application/json
- application/json
curl 'https://stage-api.paperpile.com/v1/catalog/references?limit=10&offset=0&engine=semantic-scholar&response_format=CONCISE' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN'
{
"papers": [
{
"citation_count": 1,
"reference_count": 1,
"query_index": 1,
"abstract": "string",
"accessed": "string",
"address": "string",
"affiliation": "string",
"application_number": "string",
"archive": "string",
"archivePrefix": "string",
"archive_location": "string",
"archive_place": "string",
"arxiv_category": "string",
"arxivid": "string",
"assignee": "string",
"associated_doi": "string",
"author": [
{
"family": "string",
"given": "string",
"initials": "string",
"kind": "person",
"orcid": "string",
"suffix": "string"
}
],
"booktitle": "string",
"call_number": "string",
"chapter": "string",
"class": "string",
"conference": "string",
"conference_date": "string",
"conference_location": "string",
"contributor": [
{
"family": "string",
"given": "string",
"initials": "string",
"kind": "person",
"orcid": "string",
"suffix": "string"
}
],
"copyright": "string",
"court": "string",
"database": "string",
"department": "string",
"doi": "string",
"edition": "string",
"editor": [
{
"family": "string",
"given": "string",
"initials": "string",
"kind": "person",
"orcid": "string",
"suffix": "string"
}
],
"eprint": "string",
"filed": "string",
"folders": [
"string"
],
"foreign_patents": "string",
"genre": "string",
"history": "string",
"howpublished": "string",
"institution": "string",
"international_class": "string",
"isbn": "string",
"isbn_alt": "string",
"ismn": "string",
"issue": "string",
"issuing_authority": "string",
"journal": "string",
"jurisdiction": "string",
"keywords": "string",
"labels": [
"string"
],
"language": "string",
"lccn": "string",
"legislative_body": "string",
"medium": "string",
"mr": "string",
"note": "string",
"num_pages": "string",
"num_volumes": "string",
"number": "string",
"original_address": "string",
"original_publisher": "string",
"original_title": "string",
"originally_published": "string",
"pages": "string",
"patent_number": "string",
"patent_publication_type": "string",
"pmc": "string",
"pmid": "string",
"priority_numbers": "string",
"published": "string",
"publisher": "string",
"running_time": "string",
"scale": "string",
"school": "string",
"search_class": "string",
"section": "string",
"series": "string",
"series_editor": [
{
"family": "string",
"given": "string",
"initials": "string",
"kind": "person",
"orcid": "string",
"suffix": "string"
}
],
"series_number": "string",
"session": "string",
"socialmedia_handle": "string",
"starred": true,
"states": "string",
"status": "string",
"subtype": "string",
"title": "string",
"titleshort": "string",
"translator": [
{
"family": "string",
"given": "string",
"initials": "string",
"kind": "person",
"orcid": "string",
"suffix": "string"
}
],
"trashed": true,
"type": "article",
"url": "string",
"user_citekey": "string",
"version": "string",
"volume": "string",
"zbl": "string"
}
],
"total": 1,
"has_more": true,
"engine": "semantic-scholar"
}Referenced papers
Look papers up by DOI
Returns full metadata for a list of DOIs without saving anything to your library. DOIs with no matching record are listed in not_found rather than dropped, so the response accounts for every DOI asked for. Sending the same DOI twice resolves it once.
A POST because a batch of DOIs does not fit comfortably in a query string.
Body·CatalogLookupInput
- Type: array of string …100doismin length:1required
The DOIs to look up, at most 100 per call. An empty array is accepted and returns an empty result. DOIs with no matching record come back in
not_foundrather than being dropped. - Type: "CONCISE" or "DETAILED"enumresponse
_format The level of detail to return for each paper. Defaults to DETAILED, the point of the endpoint.
Responses
- application/json
- application/json
- application/json
- application/json
- application/json
- application/json
curl https://stage-api.paperpile.com/v1/catalog/lookup \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
--data '{
"dois": [
""
],
"response_format": "DETAILED"
}'
{
"papers": [
{
"citation_count": 1,
"reference_count": 1,
"query_index": 1,
"abstract": "string",
"accessed": "string",
"address": "string",
"affiliation": "string",
"application_number": "string",
"archive": "string",
"archivePrefix": "string",
"archive_location": "string",
"archive_place": "string",
"arxiv_category": "string",
"arxivid": "string",
"assignee": "string",
"associated_doi": "string",
"author": [
{
"family": "string",
"given": "string",
"initials": "string",
"kind": "person",
"orcid": "string",
"suffix": "string"
}
],
"booktitle": "string",
"call_number": "string",
"chapter": "string",
"class": "string",
"conference": "string",
"conference_date": "string",
"conference_location": "string",
"contributor": [
{
"family": "string",
"given": "string",
"initials": "string",
"kind": "person",
"orcid": "string",
"suffix": "string"
}
],
"copyright": "string",
"court": "string",
"database": "string",
"department": "string",
"doi": "string",
"edition": "string",
"editor": [
{
"family": "string",
"given": "string",
"initials": "string",
"kind": "person",
"orcid": "string",
"suffix": "string"
}
],
"eprint": "string",
"filed": "string",
"folders": [
"string"
],
"foreign_patents": "string",
"genre": "string",
"history": "string",
"howpublished": "string",
"institution": "string",
"international_class": "string",
"isbn": "string",
"isbn_alt": "string",
"ismn": "string",
"issue": "string",
"issuing_authority": "string",
"journal": "string",
"jurisdiction": "string",
"keywords": "string",
"labels": [
"string"
],
"language": "string",
"lccn": "string",
"legislative_body": "string",
"medium": "string",
"mr": "string",
"note": "string",
"num_pages": "string",
"num_volumes": "string",
"number": "string",
"original_address": "string",
"original_publisher": "string",
"original_title": "string",
"originally_published": "string",
"pages": "string",
"patent_number": "string",
"patent_publication_type": "string",
"pmc": "string",
"pmid": "string",
"priority_numbers": "string",
"published": "string",
"publisher": "string",
"running_time": "string",
"scale": "string",
"school": "string",
"search_class": "string",
"section": "string",
"series": "string",
"series_editor": [
{
"family": "string",
"given": "string",
"initials": "string",
"kind": "person",
"orcid": "string",
"suffix": "string"
}
],
"series_number": "string",
"session": "string",
"socialmedia_handle": "string",
"starred": true,
"states": "string",
"status": "string",
"subtype": "string",
"title": "string",
"titleshort": "string",
"translator": [
{
"family": "string",
"given": "string",
"initials": "string",
"kind": "person",
"orcid": "string",
"suffix": "string"
}
],
"trashed": true,
"type": "article",
"url": "string",
"user_citekey": "string",
"version": "string",
"volume": "string",
"zbl": "string"
}
],
"not_found": [
"string"
]
}Resolved papers
System (Collapsed)
Infrastructure, discovery, and protocol endpoints that live at the host root, outside the /v1 prefix. All of them
except the MCP endpoint work without authentication.
GET /ping— liveness/health check. Returns{ "ok": true }.-
POST /— the MCP server: JSON-RPC over the Model Context Protocol's Streamable HTTP transport. Authenticate it like the REST API. -
GET /.well-known/oauth-protected-resource— OAuth 2.0 Protected Resource Metadata (RFC 9728). Names the protected resource and the authorization server that issues tokens for it. -
GET /.well-known/oauth-authorization-server— OAuth 2.0 Authorization Server Metadata (RFC 8414), describing the authorization-code + PKCE flow. Also served at/.well-known/openid-configuration.
Because these paths are not under /v1, each operation declares its own host-root server.
Note that the two metadata documents are served by different hosts. The Protected Resource Metadata comes from the
platform host and points at the authorization server; the Authorization Server Metadata itself is served by the
Paperpile API host (https://stage-api.paperpile.com). Discover the OAuth endpoints from that document rather than hard-coding them — see
Authentication.
Health check
Liveness probe. Returns { "ok": true } when the service is up. Unauthenticated. This endpoint lives at the host root, outside the /v1 prefix.
Responses
- application/json
curl https://stage-mcp.paperpile.com/ping
{
"ok": true
}Service is healthy
Send an MCP message
JSON-RPC entry point of the Paperpile MCP server, over the Model Context Protocol Streamable HTTP transport. Lives at the host root, outside the /v1 prefix.
The endpoint is stateless: every POST is an independent exchange, no mcp-session-id is issued, and one sent by a client is ignored.
Authenticate with Authorization: Bearer <token>, using either an API key or an OAuth access token — see Authentication. The endpoint itself only requires the read scope, but tools that modify your library fail on a read-only token.
Body
Responses
- application/json
- application/json
- application/json
- application/json
curl https://stage-mcp.paperpile.com/ \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}'
{}JSON-RPC response, or an SSE stream when the transport upgrades the connection.
OAuth Protected Resource Metadata
OAuth 2.0 Protected Resource Metadata, per RFC 9728. Advertises the protected resource and the authorization servers that can issue tokens for it. Unauthenticated. Lives at the host root, outside the /v1 prefix.
Responses
- application/json
curl https://stage-mcp.paperpile.com/.well-known/oauth-protected-resource
{
"resource": "https://stage-mcp.paperpile.com",
"authorization_servers": [
"https://stage-api.paperpile.com"
]
}Protected resource metadata
OAuth Authorization Server Metadata
OAuth 2.0 Authorization Server Metadata, per RFC 8414. Advertises the issuer and OAuth endpoints used for the authorization-code flow with PKCE. Also served at /.well-known/openid-configuration. Unauthenticated.
This endpoint is served by the Paperpile API host, not the platform host — it is the authorization server that /.well-known/oauth-protected-resource points to.
Responses
- application/json
curl https://stage-api.paperpile.com/.well-known/oauth-authorization-server
{
"issuer": "https://stage-api.paperpile.com",
"authorization_endpoint": "https://stage-api.paperpile.com/api/oauth/authorize",
"token_endpoint": "https://stage-api.paperpile.com/api/oauth/token",
"registration_endpoint": "https://stage-api.paperpile.com/api/oauth/register",
"code_challenge_methods_supported": [
"S256"
],
"response_types_supported": [
"code"
],
"grant_types_supported": [
"authorization_code"
],
"token_endpoint_auth_methods_supported": [
"none",
"client_secret_basic",
"client_secret_post"
]
}Authorization server metadata