Overview
Libris is an agent-native reading library — Goodreads × Strava, but designed so an agent can drive it as fluently as a human. The library is a plain, honest record of what you read; agents bring the intelligence.
There is no first-party AI inside Libris. No summaries, no recommendations, no "smart" widgets. The product is the data layer and the surfaces that expose it. If you want recommendations, you bring an agent; if you want a year-in-review, you bring an agent. Libris will tell that agent everything it knows.
Two surfaces sit on the same service layer:
- REST at
/v1/*. Every endpoint is documented in REST API. The web UI is a thin client over these same routes — there are no human-only escape hatches. - MCP over stdio (Claude Desktop / Claude Code) and Streamable HTTP (remote agents). Eighteen tools, all listed in MCP server.
Anything a human can do, an agent can do. Anything an agent can do, a human can do. That parity is enforced at the auth layer — both surfaces accept the same API keys, and both write to the same activity-event feed. If the two ever drift, that's a bug.
Authentication
Both REST and MCP authenticate the same way: a long-lived API key prefixed with libris_. Issue and revoke keys at /settings/api-keys. The raw key is shown exactly once at creation — store it somewhere you can reach it.
Send it on every request, in either header (the API accepts both):
Authorization: Bearer libris_xxx # or, equivalently: x-api-key: libris_xxx
A minimal smoke test once you have a key:
curl -H "Authorization: Bearer $LIBRIS_KEY" \ https://your-libris.example.com/v1/activity?limit=5
The web UI uses cookie-based sessions for the same routes — REST endpoints accept either an API key or a logged-in session, with the key path checked first. Browsers get sessions, agents get keys, the underlying behavior is identical.
Keys are scoped to the issuing user and act as that user. There are no per-key scopes or rate limits in V1; that lands once the surface needs it.
Conventions
A few cross-cutting conventions apply across both REST and MCP. Knowing these up front avoids re-deriving them from individual endpoint shapes.
Book references
MCP tools that operate on a book accept a bookRef string instead of forcing the caller to look up an ID first. Resolution is in order:
- If the value is a UUID, treat it as a
books.id. - Otherwise, if it looks like an ISBN-13, match against
books.isbn13. - Otherwise, do a fuzzy title match scoped to the caller's library (i.e. through their
user_booksrows). The agent should have already added the book — Libris will not silently search the global catalog.
REST endpoints take a UUID directly. The fuzzy-match convenience is an MCP affordance only.
Time inputs
Endpoints that accept a since parameter (notably GET /v1/activity and recent_activity) take either an ISO 8601 timestamp or a duration shorthand:
30s # 30 seconds ago 15m # 15 minutes ago 2h # 2 hours ago 7d # 7 days ago
All timestamps emitted by the API are UTC ISO 8601. Date-only values (e.g. session_date) are still serialized as full ISO timestamps with a 00:00 UTC component.
Pagination
Listing endpoints take a limit query parameter (default 50, max 200). Results are ordered most-recent-first by default; sort controls live on individual endpoints (e.g. GET /v1/books accepts sort and direction). The activity feed returns a nextCursor when there is more.
The activity-event guarantee
Every state-changing call — adding a book, changing status, logging a session, rating, favoriting, joining a collection, setting a goal — writes one row to activity_events. That row is what an agent reads to discover what happened, when, and why. See Activity events for the type catalog. The one carve-out is PUT /v1/books/{id}/notes: free-text notes are private scratch space and intentionally do not produce a feed entry.
REST API
All endpoints live under /v1 and accept the auth headers described above. Click any row to expand its parameters, request body, and response shapes. The canonical spec lives in openapi.yaml at the repo root — this page is generated from it.
books
Add books to the caller's library, fetch them, and update per-user state (status, rating, review, favorite, notes).
GET/v1/booksSearch the caller's library
Search the caller's library
qstringstatus"to_read" | "reading" | "completed" | "dnf" | "paused"collectionstringgenrestringfavoritebooleansort"date_added" | "date_started" | "date_completed" | "rating" | "title"direction"asc" | "desc"limitinteger
- 200OK
field type notes items array of object
POST/v1/booksAdd a book to the caller's library
Add a book to the caller's library
query</code> or <code>isbn| field | type | notes |
|---|---|---|
| query | string | |
| isbn | string | |
| status | enum: "to_read" | "reading" | "completed" | "dnf" | "paused" | |
| collection | string | |
| genres | array of string | |
| recommendedBy | string | |
| recommendedSourceUrl | string (uri) |
- 201Created
field type notes bookId string (uuid) created boolean
GET/v1/books/{id}Fetch book + caller's user_books state + authors + genres
Fetch book + caller's user_books state + authors + genres
idstringreq
- 200OK
- 404Not found
DELETE/v1/books/{id}Remove the book from the caller's library
Remove the book from the caller's library
idstringreq
- 204No Content
- 404Not in library
PUT/v1/books/{id}/favoriteToggle favorite
Toggle favorite
idstringreq
| field | type | notes |
|---|---|---|
| favoritereq | boolean |
- 200OK
PUT/v1/books/{id}/notesSet free-text notes (no activity event)
Set free-text notes (no activity event)
idstringreq
| field | type | notes |
|---|---|---|
| notesreq | string | maxLength 20000 |
- 200OK
PUT/v1/books/{id}/ratingSet rating and optional review
Set rating and optional review
idstringreq
| field | type | notes |
|---|---|---|
| starsreq | integer | min 1 · max 5 |
| review | string |
- 200OK
PUT/v1/books/{id}/statusChange reading status
Change reading status
idstringreq
| field | type | notes |
|---|---|---|
| statusreq | enum: "to_read" | "reading" | "completed" | "dnf" | "paused" |
- 200OK
curl -H "Authorization: Bearer $LIBRIS_KEY" \ "https://your-libris.example.com/v1/books?status=reading&limit=10"
sessions
Reading sessions — log retroactively or use a single live timer. Each session is one continuous stretch on one book.
GET/v1/sessionsList the caller's reading sessions
List the caller's reading sessions
limitinteger
- 200OK
POST/v1/sessionsLog a completed (retroactive) reading session
Log a completed (retroactive) reading session
| field | type | notes |
|---|---|---|
| bookIdreq | string (uuid) | |
| sessionDate | string (date-time) | |
| startPagereq | integer | min 0 |
| endPagereq | integer | min 0 |
| durationMinutes | integer | min 0 |
| notes | string |
- 201Created
POST/v1/sessions/startStart the caller's live reading timer (one per user)
Start the caller's live reading timer (one per user)
| field | type | notes |
|---|---|---|
| bookIdreq | string (uuid) | |
| startPage | integer | min 0 |
- 201Created
POST/v1/sessions/stopStop the caller's live timer; promotes to a reading_sessions row
Stop the caller's live timer; promotes to a reading_sessions row
| field | type | notes |
|---|---|---|
| endPage | integer | min 0 |
- 200OK
curl -X POST -H "Authorization: Bearer $LIBRIS_KEY" \
-H "Content-Type: application/json" \
-d '{"bookId":"<uuid>","startPage":120,"endPage":156,"durationMinutes":42}' \
"https://your-libris.example.com/v1/sessions"collections
User-defined groups of books. Collections do not own their members; deleting one leaves the books in place.
GET/v1/collectionsList the caller's collections
List the caller's collections
- 200OK
POST/v1/collectionsCreate or update a collection by name
Create or update a collection by name
| field | type | notes |
|---|---|---|
| namereq | string | |
| description | string |
- 201Created
GET/v1/collections/{id}Fetch a single collection
Fetch a single collection
idstringreq
- 200OK
- 404Not found
PATCH/v1/collections/{id}Rename or update description on a collection
Rename or update description on a collection
idstringreq
| field | type | notes |
|---|---|---|
| name | string | maxLength 120 · minLength 1 |
| description | string | null | maxLength 2000 |
- 200OK
- 404Not found
DELETE/v1/collections/{id}Delete a collection (member books are not deleted)
Delete a collection (member books are not deleted)
idstringreq
- 204No Content
- 404Not found
POST/v1/collections/{id}/books/{bookId}Add a book to a collection
Add a book to a collection
idstringreqbookIdstringreq
- 201Created
DELETE/v1/collections/{id}/books/{bookId}Remove a book from a collection
Remove a book from a collection
idstringreqbookIdstringreq
- 204No Content
curl -H "Authorization: Bearer $LIBRIS_KEY" \ "https://your-libris.example.com/v1/collections"
goals
Annual reading targets in books and/or pages, with year-to-date progress.
GET/v1/goals/{year}Get the goal for a year
Get the goal for a year
yearintegerreq
- 200OK
field type notes id string (uuid) year integer bookTarget integer | null pageTarget integer | null
PUT/v1/goals/{year}Set or update the annual goal
Set or update the annual goal
yearintegerreq
| field | type | notes |
|---|---|---|
| bookTarget | integer | null | min 0 |
| pageTarget | integer | null | min 0 |
- 200OK
GET/v1/goals/{year}/progressYear-to-date progress vs the annual goal
Year-to-date progress vs the annual goal
yearintegerreq
- 200OK
field type notes year integer bookTarget integer | null pageTarget integer | null booksCompleted integer pagesRead integer
curl -H "Authorization: Bearer $LIBRIS_KEY" \ "https://your-libris.example.com/v1/goals/2026/progress"
activity
Read the unified mutation feed for the caller.
GET/v1/activityRead the caller's activity feed (reverse chronological)
Read the caller's activity feed (reverse chronological)
sincestring— ISO timestamp or duration like "7d", "1h"untilstringtypesstring— Comma-separated list of event_type values.limitinteger
- 200OK
field type notes items array of object nextCursor string | null
curl -H "Authorization: Bearer $LIBRIS_KEY" \ "https://your-libris.example.com/v1/activity?since=24h"
search
Look up books in Open Library before adding them. This endpoint never writes to the library.
GET/v1/search/booksPassthrough search against Open Library (does not add anything)
Passthrough search against Open Library (does not add anything)
qstringreqlimitinteger
- 200OK
curl -H "Authorization: Bearer $LIBRIS_KEY" \ "https://your-libris.example.com/v1/search/books?q=tolkien&limit=5"
users
GET/v1/users/{username}Public profile projection
Public profile projection
usernamestringreq
- 200OK
field type notes profilereq object Public projection of a user. Notes, email, and admin flags are never returned. The `libraryPublic` flag tells the caller whether the user has opened their library for browsing. currentlyReadingreq array of object publicCollectionsreq array of object - 404User does not exist or profile is private
GET/v1/users/{username}/libraryPublic library list for a user
Public library list for a user
usernamestringreq
qstringstatus"to_read" | "reading" | "completed" | "dnf" | "paused"collectionstringgenrestringfavoritebooleanformatstringownedbooleansort"date_added" | "date_started" | "date_completed" | "rating" | "title" | "re_read_count"direction"asc" | "desc"limitinteger
- 200OK
field type notes items array of object - 404User missing, profile private, or library private
DELETE/v1/users/me/avatarClear the authenticated user's avatar
Clear the authenticated user's avatar
- 200Avatar cleared
field type notes okreq boolean - 401Not authenticated
uploads
POST/v1/uploads/avatarMint a Vercel Blob upload token for an avatar image
Mint a Vercel Blob upload token for an avatar image
(empty object)
- 200Blob upload token
(empty object)
- 400Invalid request body
- 401Not authenticated
MCP server
Libris ships a Model Context Protocol server with two transports. Both expose the same tool set; the only difference is wiring.
stdio — Claude Desktop / Claude Code
Run the server as a child process; the agent client speaks JSON-RPC over stdin/stdout. The server reads LIBRIS_API_KEYfrom the environment and scopes every call to that key's user. One user per process.
{
"mcpServers": {
"libris": {
"command": "pnpm",
"args": ["mcp:stdio"],
"cwd": "/path/to/librisnew",
"env": {
"LIBRIS_API_KEY": "libris_xxx"
}
}
}
}For Claude Code, add the same entry to your project- or user-scoped MCP config, or run pnpm mcp:stdio directly.
Streamable HTTP — remote agents
For agents that don't live on the same machine as Libris (most of them), run the HTTP transport. Each request carries its own API key, so a single deployment serves multiple users.
curl -X POST https://your-mcp-host.example.com/mcp \
-H "Authorization: Bearer libris_xxx" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}'The HTTP transport is stateless — there are no MCP sessions to resume. Each POST /mcp spins up a fresh per-user server scoped to the bearer key, runs the request, and tears down. Headers GET /mcp and DELETE /mcp return 405 by spec.
Tools
Click any tool to see its argument schema. Names mirror the conceptual action; bookRef arguments accept the UUID / ISBN-13 / fuzzy-title forms described in Conventions.
add_bookAdd a book to the caller's library. Supply either a free-text query or an ISBN. Optionally set the initial status, add to a collection, tag with genres, or record who recommended it.
Add a book to the caller's library. Supply either a free-text query or an ISBN. Optionally set the initial status, add to a collection, tag with genres, or record who recommended it.
| field | type | notes |
|---|---|---|
| query | string | Title/author search string |
| isbn | string | |
| status | enum: "to_read" | "reading" | "completed" | "dnf" | "paused" | |
| collection | string | |
| genres | array of string | |
| recommendedBy | string | |
| recommendedSourceUrl | string (uri) |
remove_bookRemove a book from the caller's library. Deletes the user_books row, this user's reading sessions and any live timer for it, the book's membership in this user's collections, and mentions sourced from this user's review/notes/sessions for the book. The global book record (and other users' state) is untouched. Activity history is preserved with a final book_removed event. bookRef may be a uuid, isbn13, or fuzzy title match scoped to the library.
Remove a book from the caller's library. Deletes the user_books row, this user's reading sessions and any live timer for it, the book's membership in this user's collections, and mentions sourced from this user's review/notes/sessions for the book. The global book record (and other users' state) is untouched. Activity history is preserved with a final book_removed event. bookRef may be a uuid, isbn13, or fuzzy title match scoped to the library.
| field | type | notes |
|---|---|---|
| bookRefreq | string |
update_statusChange the reading status of a book in the caller's library. bookRef may be a uuid, isbn13, or fuzzy title match scoped to the library.
Change the reading status of a book in the caller's library. bookRef may be a uuid, isbn13, or fuzzy title match scoped to the library.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| statusreq | enum: "to_read" | "reading" | "completed" | "dnf" | "paused" |
log_sessionLog a completed reading session (retroactive). Page range is required; duration is optional.
Log a completed reading session (retroactive). Page range is required; duration is optional.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| sessionDate | string (date-time) | |
| startPagereq | integer | min 0 |
| endPagereq | integer | min 0 |
| durationMinutes | integer | min 0 |
| notes | string |
start_sessionStart a live reading timer for a book. Only one active session per user at a time; starting a new one replaces any existing live session.
Start a live reading timer for a book. Only one active session per user at a time; starting a new one replaces any existing live session.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| startPage | integer | min 0 |
stop_sessionStop the caller's live reading timer and promote it to a completed session.
Stop the caller's live reading timer and promote it to a completed session.
| field | type | notes |
|---|---|---|
| endPage | integer | min 0 |
rate_bookRate a book 1-5 stars and optionally attach a review.
Rate a book 1-5 stars and optionally attach a review.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| starsreq | integer | min 1 · max 5 |
| review | string |
set_timelineUpdate one or more timeline dates (dateAdded, dateStarted, dateCompleted) on a book in the caller's library. Each field is optional: omit to leave unchanged, or pass null/empty string to clear. Accepts ISO 8601 datetimes or YYYY-MM-DD date-only strings. dateStarted must not be after dateCompleted.
Update one or more timeline dates (dateAdded, dateStarted, dateCompleted) on a book in the caller's library. Each field is optional: omit to leave unchanged, or pass null/empty string to clear. Accepts ISO 8601 datetimes or YYYY-MM-DD date-only strings. dateStarted must not be after dateCompleted.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| dateAdded | any | |
| dateStarted | any | |
| dateCompleted | any |
set_current_pageSet the reader's current page in a book. Use this to record where you left off between sessions. Pass null to clear. The value is automatically advanced when sessions are logged — only call this to make a manual correction. Values above the book's known page count are silently clamped; the response returns the value actually written, which may differ from what was sent.
Set the reader's current page in a book. Use this to record where you left off between sessions. Pass null to clear. The value is automatically advanced when sessions are logged — only call this to make a manual correction. Values above the book's known page count are silently clamped; the response returns the value actually written, which may differ from what was sent.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| pagereq | any |
set_favoriteToggle favorite status on a book in the caller's library.
Toggle favorite status on a book in the caller's library.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| favoritereq | boolean |
list_cover_candidatesList candidate cover-image URLs for a book in the caller's library, aggregated from OpenLibrary editions and Google Books. Returns an ordered, deduped list with each entry tagged by source.
List candidate cover-image URLs for a book in the caller's library, aggregated from OpenLibrary editions and Google Books. Returns an ordered, deduped list with each entry tagged by source.
| field | type | notes |
|---|---|---|
| bookRefreq | string |
set_cover_overrideSet the caller's per-book cover override URL for a book in their library. Accepts any HTTPS URL (third-party image, Vercel Blob URL, etc.). HTTP and other schemes are rejected. Replaces any prior override; cleans up the previous Vercel Blob upload if applicable.
Set the caller's per-book cover override URL for a book in their library. Accepts any HTTPS URL (third-party image, Vercel Blob URL, etc.). HTTP and other schemes are rejected. Replaces any prior override; cleans up the previous Vercel Blob upload if applicable.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| urlreq | string (uri) |
clear_cover_overrideClear the caller's per-book cover override for a book in their library. Cover rendering falls back to the global books.image_url.
Clear the caller's per-book cover override for a book in their library. Cover rendering falls back to the global books.image_url.
| field | type | notes |
|---|---|---|
| bookRefreq | string |
add_to_collectionAdd a book to a collection. Creates the collection if it doesn't exist.
Add a book to a collection. Creates the collection if it doesn't exist.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| collectionreq | string |
set_goalSet or update the caller's annual reading goal.
Set or update the caller's annual reading goal.
| field | type | notes |
|---|---|---|
| yearreq | integer | min 1900 · max 3000 |
| bookTarget | integer | min 0 |
| pageTarget | integer | min 0 |
search_librarySearch the caller's library by free-text query, status, collection, genre, or favorite filter. Sort by date_added, date_started, date_completed, rating, or title.
Search the caller's library by free-text query, status, collection, genre, or favorite filter. Sort by date_added, date_started, date_completed, rating, or title.
| field | type | notes |
|---|---|---|
| query | string | |
| status | enum: "to_read" | "reading" | "completed" | "dnf" | "paused" | |
| collection | string | |
| genre | string | |
| favorite | boolean | |
| sort | enum: "date_added" | "date_started" | "date_completed" | "rating" | "title" | "re_read_count" | |
| direction | enum: "asc" | "desc" | |
| limit | integer | min 1 · max 200 |
get_bookFetch one book with global metadata and the caller's user_books state.
Fetch one book with global metadata and the caller's user_books state.
| field | type | notes |
|---|---|---|
| bookRefreq | string |
get_authorFetch one author with their full catalog bibliography (capped at 200 books, sorted publishYear DESC, NULLS LAST) and the caller's mentions of that author grouped by source type. Returns { author, books: [{book, userBook}], mentions: { reviews, notes, collections, fromBookMetadata } }. Books include the caller's user_books row when shelved, null otherwise.
Fetch one author with their full catalog bibliography (capped at 200 books, sorted publishYear DESC, NULLS LAST) and the caller's mentions of that author grouped by source type. Returns { author, books: [{book, userBook}], mentions: { reviews, notes, collections, fromBookMetadata } }. Books include the caller's user_books row when shelved, null otherwise.
| field | type | notes |
|---|---|---|
| authorIdreq | string (uuid) |
recent_activityReturn the caller's activity feed (book adds, status changes, sessions, ratings, goals…) in reverse chronological order. `since` accepts durations like '1h', '7d' or ISO timestamps.
Return the caller's activity feed (book adds, status changes, sessions, ratings, goals…) in reverse chronological order. `since` accepts durations like '1h', '7d' or ISO timestamps.
| field | type | notes |
|---|---|---|
| since | string | |
| types | array of string | |
| limit | integer | min 1 · max 200 |
goal_progressReturn year-to-date books completed and pages read vs the caller's goal for a given year.
Return year-to-date books completed and pages read vs the caller's goal for a given year.
| field | type | notes |
|---|---|---|
| year | integer | min 1900 · max 3000 |
get_goalFetch the raw goal row for a year (null fields mean unset).
Fetch the raw goal row for a year (null fields mean unset).
| field | type | notes |
|---|---|---|
| yearreq | integer | min 1900 · max 3000 |
list_collectionsList all collections owned by the caller.
List all collections owned by the caller.
No arguments.
rename_collectionRename a collection and/or update its description. Pass only the fields to change.
Rename a collection and/or update its description. Pass only the fields to change.
| field | type | notes |
|---|---|---|
| collectionIdreq | string (uuid) | |
| name | string | maxLength 120 · minLength 1 |
| description | any |
delete_collectionDelete a collection. Member books remain in the library; only the collection and its memberships are removed.
Delete a collection. Member books remain in the library; only the collection and its memberships are removed.
| field | type | notes |
|---|---|---|
| collectionIdreq | string (uuid) |
search_open_librarySearch Open Library for books to potentially add. Does not touch the library — call add_book once you've chosen one.
Search Open Library for books to potentially add. Does not touch the library — call add_book once you've chosen one.
| field | type | notes |
|---|---|---|
| queryreq | string | |
| limit | integer | min 1 · max 20 |
get_public_profileRead another user's public profile by username. Returns the same projection a signed-out browser would see at /u/[username]: header info, currently reading, and public collections. Returns null when the user does not exist or has set their profile to private. Does not require the target user to be the caller.
Read another user's public profile by username. Returns the same projection a signed-out browser would see at /u/[username]: header info, currently reading, and public collections. Returns null when the user does not exist or has set their profile to private. Does not require the target user to be the caller.
| field | type | notes |
|---|---|---|
| usernamereq | string | minLength 1 |
set_avatar_urlSet the caller's avatar to a publicly accessible HTTPS image URL. HTTP and other schemes are rejected. Replaces any prior avatar; cleans up the previous Vercel Blob upload if applicable. To upload a file directly, use POST /v1/uploads/avatar via the REST API.
Set the caller's avatar to a publicly accessible HTTPS image URL. HTTP and other schemes are rejected. Replaces any prior avatar; cleans up the previous Vercel Blob upload if applicable. To upload a file directly, use POST /v1/uploads/avatar via the REST API.
| field | type | notes |
|---|---|---|
| urlreq | string (uri) |
clear_avatarClear the caller's avatar. Avatar rendering falls back to initials. Cleans up the previous Vercel Blob upload if applicable. Idempotent — safe to call when no avatar is set.
Clear the caller's avatar. Avatar rendering falls back to initials. Cleans up the previous Vercel Blob upload if applicable. Idempotent — safe to call when no avatar is set.
No arguments.
Activity events
Every mutation writes one row to activity_events. Reading the feed is how an agent reconstructs "what changed since the last time I looked" without polling individual resources. Filter by type with ?types=book_completed,session_logged, or timebox with ?since=24h.
| event_type | meaning |
|---|---|
| book_added | A book entered the caller's library (via add_book or import). |
| status_changed | The reading_status moved between values. Specific transitions also emit one of the book_completed / book_dnfed / book_paused / book_resumed shorthands below. |
| session_logged | A reading_sessions row was inserted — either retroactively (log_session) or by stopping a live timer. |
| rated | Star rating set or changed (1-5). |
| reviewed | Review body set or changed. |
| favorited | favorite flag turned on. |
| unfavorited | favorite flag turned off. |
| collection_added | Book joined a collection. |
| collection_removed | Book left a collection (collection itself untouched). |
| goal_set | An annual goal was created for the first time for that year. |
| goal_updated | An existing annual goal had its targets adjusted. |
| book_completed | Status transitioned to completed. Fires alongside status_changed. |
| book_dnfed | Status transitioned to dnf. Fires alongside status_changed. |
| book_paused | Status transitioned to paused. Fires alongside status_changed. |
| book_resumed | Status moved out of paused back to reading. Fires alongside status_changed. |
Each event carries a payload object with type-specific detail (e.g. session_logged includes startPage, endPage, durationMinutes; status_changed includes from and to). The exact shape is intentionally not part of the v1 contract — read what's there, treat unknown keys as informational.
Errors
REST errors are JSON with an errorfield and an HTTP status that's actually meaningful. MCP errors come back through JSON-RPC with an error.code and error.message.
401 — missing or invalid key
Returned when the request has no recognized credential, or the key has been revoked.
{ "error": "Unauthorized" } {
"jsonrpc": "2.0",
"error": { "code": -32000, "message": "Invalid API key" },
"id": null
}404 — resource not found
The book, collection, goal, or session ID isn't in the caller's scope. Libris doesn't leak existence — the same 404 is returned whether the row doesn't exist or belongs to a different user.
{ "error": "Not found" }400 / 422 — validation failure
Request body or query parameters didn't match the expected schema. The error includes a details object with the failing field path so an agent can self-correct.
{
"error": "Validation failed",
"details": [
{ "path": ["stars"], "message": "Number must be greater than or equal to 1" }
]
}409 — conflict
Returned when a write would violate a uniqueness or state invariant — e.g. starting a live timer when one is already running for the user (the existing one is replaced; this is informational, not a hard error in current implementation).
500 — internal error
Something on the server side broke. Retry once with the same payload; if it persists, check the server logs (or the Sentry dashboard once Phase 4 lands). MCP wraps the underlying message in JSON-RPC code -32603.
{
"jsonrpc": "2.0",
"error": { "code": -32603, "message": "Internal error" },
"id": null
}