API
Papex exposes a set of JSON HTTP APIs under /api.
Interactive reference
A complete, machine-readable OpenAPI 3.1 specification is served at /api/openapi.json, and an interactive, Try-it-capable explorer (powered by Scalar) is available at /api-docs. Open it to browse every endpoint, inspect request and response schemas, and send live requests from your browser.
Keeping the docs in sync (code-first)
The OpenAPI document is generated from the code, not written by hand. Each route owns a sibling fragment route.openapi.ts that is the single source of truth for that endpoint's docs. The static part (info, components/schemas, components/responses, security) lives in src/lib/openapi/base.ts.
The generator (src/lib/openapi/generate.ts) scans every fragment, merges them into the base, and writes src/lib/openapi/spec.generated.ts — the file served by /api/openapi.json.
# regenerate after editing a fragment (/api/openapi.json + /api-docs update)
npm run openapi:generateThis is wired into predev and prebuild, so the spec is always rebuilt before next dev / next build. Never edit spec.generated.ts by hand — it is overwritten on every run.
Documenting a new endpoint
When you add a route handler src/app/api/foo/bar/route.ts, create a sibling route.openapi.ts:
export default {
"/api/foo/bar": {
get: {
tags: ["Discovery"],
summary: "Describe what it does",
// security: [] // omit for public endpoints
responses: {
200: { description: "OK", content: { "application/json": { schema: { type: "object" } } } },
},
},
},
} as const;Run npm run openapi:generate (or just start/build) and the endpoint appears in /api/openapi.json and /api-docs automatically. Shared schemas live in src/lib/openapi/base.ts (e.g. #/components/schemas/PaperListItem).
Authentication
There are two ways to authenticate:
- Session cookie (
papex_session) — issued on login and used by the browser. Sent automatically for same-origin requests. - API key (
Authorization: Bearer pk_…) — for scripts and third-party integrations. Create keys from Settings → API Keys (/settings/api-keys). A key is bound to your account and inherits your role's RBAC permissions, so every endpoint that works with a session cookie also works with an API key. The raw secret is shown only once at creation; only its SHA-256 hash is stored.
Example request with an API key:
curl -H "Authorization: Bearer pk_live_xxxx" https://your-host/api/papers?pageSize=1Public (unauthenticated) endpoints — such as listing papers, search, categories, authors and health — work for anonymous callers, session cookies, and API keys alike.
Auth
POST /api/auth/register— register{username, email, displayName, password}POST /api/auth/login— login{identifier, password}POST /api/auth/logout— logoutGET /api/auth/me— current user
API keys
GET /api/settings/api-keys— list your keysPOST /api/settings/api-keys— create a key{name, scopes?:["read"|"write"], environment?:"live"|"test", expiresAt?:ISODate|null}DELETE /api/settings/api-keys?id=<keyId>— revoke a key
Papers
GET /api/papers— list. Query params:q(full-text ortitle:/au:/abs:/cat:-prefixed),category,tag,sort(new|updated|by_citations),from(ISO date, only papers created on/after),page,pageSize. Rows include a resolvedcitationCount.GET /api/papers/:id— detail (includessubmitter,tags,commentCount)GET /api/papers/:id/comments— commentsGET /api/papers/:id/citations— citation graph{ outgoing, incoming }GET /api/papers/:id/tags— tags of a paperPOST /api/papers— submit (auth required, needspaper:publish); accepts JSON or multipart (meta + optionalpdffile)POST /api/papers/:id/moderate— moderate{action:"approve"|"reject"|"withdraw", reason?}(needspaper:moderate)POST /api/papers/:id/citations— add a citation{targetArxivId?|targetDoi?|targetTitle?}(owner/moderator/admin)POST /api/papers/:id/tags/DELETE /api/papers/:id/tags— attach/detach a tag{tagId|name}(owner/moderator/admin; creates the tag if the name is new)POST /api/submit/archive— upload a source-packagetar.gzto auto-ingest, link citations and build PDF (auth required; see Submission guide)
Categories
GET /api/categories— category tree
Tags
GET /api/tags— all tags with usage counts (ordered by popularity)POST /api/tags— create a tag{name}(auth required; idempotent by name)
Subscriptions
GET /api/subscriptions— list my subscriptions, enriched (category/author/paper names resolved intotitle+ ahrefdeep link)POST /api/subscriptions— subscribe / unsubscribe (toggle){type:"category"|"author"|"paper", refId}DELETE /api/subscriptions— unsubscribe{type, refId}
Feed & notifications
Announcements are generated when a paper enters one of your subscriptions (new-in-category, new-from-author), when someone replies to your comment, or by an admin broadcast.
GET /api/feed— current user's announcements (?markRead=1also marks them all read)POST /api/feed— mark a single announcement read{id}
The header bell (FeedBell) shows a live unread badge kept in sync through a Zustand store, so reading anywhere updates the badge immediately.
Bookmarks
GET /api/bookmarks— list my bookmarks (each resolved to its paper title andgroupName); pass?paperId=to instead get{ bookmarked: boolean }for a single paperPOST /api/bookmarks— toggle a bookmark{paperId, group?}(returns{ bookmarked: true|false })PATCH /api/bookmarks/:paperId— move a bookmark into a group{group}(null clears it)DELETE /api/bookmarks— remove a bookmark{paperId}
Messages
Messages are classified by kind into 8 categories: system, ticket_reply, announcement, review_result, co_review_request, co_review_result, admin_message, community_reply.
GET /api/messages— current user's messages + unread count (supports?kind=filter)GET /api/messages/stats— unread statsPOST /api/messages/:id/read— mark readPOST /api/messages—{action:"read-all"}mark all read
Tickets
GET /api/tickets— my tickets (?scope=alladmin only)POST /api/tickets— create{subject, type, priority, message}GET /api/tickets/:id— detailPOST /api/tickets/:id— replyPATCH /api/tickets/:id— admin update status/priority
Feedback
POST /api/feedback— submit feedback (auth required, auto-creates a ticket)
Co-review
GET /api/co-reviews?scope=mine|all— list (mine / all, respective permission required)POST /api/co-reviews— assign{paperId, reviewerId, note?}(needsco_review:assign)GET /api/co-reviews/:id— detailPOST /api/co-reviews/:id/respond— reviewer responds{accepted:boolean}POST /api/co-reviews/:id/submit— submit opinion{decision:"approve"|"reject"|"revise", comment}
Admin
Admin endpoints require a moderator / admin base role and are authorized per fine-grained permission.
GET /api/admin/users— user list (pagination / search, needsuser:manage)PATCH /api/admin/users/:id— set roles{roleKeys:string[]}or override{permission:{key:string, grant:boolean|null}}(needsuser:manage/permission:manage)GET /api/admin/roles— role list (needsrole:manage)PUT /api/admin/roles/:id— set role permissions{permissionKeys:string[]}POST /api/admin/messages— broadcast{scope:"all"|"role"|"userIds", role?, userIds?, kind:"announcement"|"system"|"admin_message", title, body, link?}(needsmessage:broadcast)GET /api/admin/stats— platform stats