v0.1.0
OpenAPI 3.1.0

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.

Server:https://stage-api.paperpile.com/v1

Staging server

Get a token
Client Libraries

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}/files lists that reference's files, the same as GET /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.

Account Operations

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.

Search Operations

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
Request Example for get/ping
curl https://stage-mcp.paperpile.com/ping
{
  "ok": true
}

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

required
application/json
Empty object

Responses

  • application/json
  • application/json
  • application/json
  • application/json
Request Example for post/
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"
}'
{}

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
Request Example for get/.well-known/oauth-protected-resource
curl https://stage-mcp.paperpile.com/.well-known/oauth-protected-resource
{
  "resource": "https://stage-mcp.paperpile.com",
  "authorization_servers": [
    "https://stage-api.paperpile.com"
  ]
}

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
Request Example for get/.well-known/oauth-authorization-server
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"
  ]
}

Models