API Reference
The Bookshelf API allows you to manage a home book library — scanning shelves, storing book metadata, tracking physical copies, and managing reading status.
All requests and responses use JSON. All endpoints require authentication except POST /auth/register/, POST /auth/login/, and GET /api/health/. Pass the access token as a Bearer header: Authorization: Bearer <access_token>. Tokens expire after 12 hours; use POST /auth/token/refresh/ to obtain a new one.
Response Format
All endpoints return JSON. List endpoints return a paginated envelope. Errors include an error key with a human-readable message.
Paginated Response
The Copy Object
A Copy represents a physical book on a shelf. Book metadata is inlined.
id, display, location_name, numberunread · reading · readexcellent · good · fair · poor{
"id": 42,
"book_id": 17,
"title": "The City & The City",
"authors": ["China Miéville"],
"isbn": "9780345524256",
"publisher": "Del Rey",
"published_year": 2009,
"genre": "Science Fiction",
"shelf": {
"id": 3,
"display": "Living Room - Shelf 2",
"location_name": "Living Room",
"number": 2
},
"reading_status": "read",
"on_loan": false,
"rating": 5,
"condition": "good",
"notes": null,
"date_added": "2026-06-06T10:30:00Z"
}
Auth
Creates a new account. Returns JWT tokens immediately — no email verification required in the current build.
Request Body
| Parameter | Type | Description |
|---|---|---|
| emailrequired | string | Used as the username — must be unique |
| passwordrequired | string | Minimum 8 characters |
Response Fields
Response Codes
curl -X POST /auth/register/ \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com", "password": "supersecret" }'
{
"access": "eyJ...",
"refresh": "eyJ...",
"user": {
"id": 1,
"email": "user@example.com",
"username": "user@example.com"
}
}
Authenticates an existing account and returns fresh JWT tokens.
Request Body
| Parameter | Type | Description |
|---|---|---|
| emailrequired | string | |
| passwordrequired | string |
Response Codes
curl -X POST /auth/login/ \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com", "password": "supersecret" }'
{
"access": "eyJ...",
"refresh": "eyJ...",
"user": { /* user object */ }
}
Exchanges a refresh token for a new access token. Refresh tokens rotate on use — the response includes a new refresh token as well.
Request Body
| Parameter | Type | Description |
|---|---|---|
| refreshrequired | string | The refresh token from login or register |
Response Codes
curl -X POST /auth/token/refresh/ \ -H "Content-Type: application/json" \ -d '{ "refresh": "eyJ..." }'
{
"access": "eyJ...",
"refresh": "eyJ..."
}
Returns the currently authenticated user's profile. Requires a valid access token.
Response Codes
curl /auth/me/ \ -H "Authorization: Bearer eyJ..."
{
"id": 1,
"email": "user@example.com",
"username": "user@example.com"
}
Changes the authenticated user's password. Returns fresh JWT tokens so the client stays logged in.
Request Body
| Parameter | Type | Description |
|---|---|---|
| current_passwordrequired | string | Must match the existing password |
| new_passwordrequired | string | Minimum 8 characters |
Response Codes
curl -X PATCH /auth/me/ \ -H "Authorization: Bearer eyJ..." \ -H "Content-Type: application/json" \ -d '{ "current_password": "supersecret", "new_password": "evenmoresecret" }'
{
"access": "eyJ...",
"refresh": "eyJ...",
"user": { /* user object */ }
}
Books
Returns a paginated list of all active (non-deleted) copies in the library, with book metadata inlined.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| searchoptional | string | Filter by title or author name (case-insensitive) |
| genreoptional | string | Filter by genre (partial match) |
| yearoptional | integer | Filter by published year (exact) |
| orderingoptional | string | title, -title, date_added, -date_added, rating, -rating |
| pageoptional | integer | Page number (default: 1) |
| page_sizeoptional | integer | Results per page (default: 20) |
Response Codes
GET /api/books/?search=mieville&ordering=-date_added
{
"count": 3,
"next": null,
"previous": null,
"results": [{ /* Copy objects */ }]
}
Adds a book to the library. Creates a canonical Book record (or matches an existing one by ISBN or title) and a new Copy record.
Request Body
| Parameter | Type | Description |
|---|---|---|
| titlerequired | string | Book title |
| authoroptional | string | Author name — found or created automatically |
| isbnoptional | string | ISBN-13 |
| publisheroptional | string | |
| published_yearoptional | integer | |
| genreoptional | string | |
| shelf_idoptional | integer | ID of the shelf to assign this copy to |
| reading_statusoptional | string | unread (default) · reading · read |
| conditionoptional | string | excellent · good · fair · poor |
| ratingoptional | integer | 1–5 |
| notesoptional | string |
Response Codes
curl -X POST /api/books/ \ -H "Content-Type: application/json" \ -d '{ "title": "The City & The City", "author": "China Miéville", "isbn": "9780345524256", "shelf_id": 3, "reading_status": "read", "rating": 5 }'
{ /* Copy object */ }
Returns a single copy by its ID.
Response Codes
GET /api/books/42/
{ /* Copy object */ }
Updates copy-level fields. Only fields provided are changed. Book metadata (title, authors, ISBN) cannot be changed via this endpoint.
Request Body
| Parameter | Type | Description |
|---|---|---|
| reading_statusoptional | string | unread · reading · read |
| on_loanoptional | boolean | |
| ratingoptional | integer | 1–5 |
| conditionoptional | string | excellent · good · fair · poor |
| shelf_idoptional | integer|null | Pass null to remove shelf assignment |
| notesoptional | string|null |
Response Codes
curl -X PATCH /api/books/42/ \ -H "Content-Type: application/json" \ -d '{ "reading_status": "read", "rating": 4, "on_loan": false }'
{ /* Updated Copy object */ }
Soft-deletes a copy by setting its deleted_at timestamp. The copy is excluded from all list results but can be restored. The underlying Book record is not affected.
Response Codes
curl -X DELETE /api/books/42/
204 No Content
Restores a previously soft-deleted copy by clearing its deleted_at timestamp.
Response Codes
curl -X PATCH /api/books/42/restore/
{ /* Restored Copy object */ }
Looks up book metadata by ISBN via Open Library. Does not save anything to the library. Use the result to pre-populate a POST /api/books/ request.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| isbnrequired | string | ISBN-10 or ISBN-13 |
Response Fields
false if no record found — remaining fields omittedResponse Codes
GET /api/books/lookup/?isbn=9780345524256
{
"matched": true,
"title": "The City & The City",
"authors": ["China Miéville"],
"author": "China Miéville",
"publisher": "Del Rey",
"published_year": "2009",
"cover_url": "https://covers.openlibrary.org/...",
"isbn": "9780345524256"
}
{ "matched": false }
Downloads the entire active library as a CSV file.
Response
Returns a text/csv file with columns: title, authors, isbn, publisher, published_year, genre, reading_status, on_loan, rating, condition, shelf, notes, date_added.
Response Codes
GET /api/books/export/ # Or directly in browser — triggers download
Content-Type: text/csv Content-Disposition: attachment; filename="library.csv"
Scans
Submits a bookshelf photo for processing. The image is analysed by Gemini Flash which reads every visible spine and extracts title and author. Returns a scan job with results — no books are saved to the library until POST /api/scans/{id}/confirm/ is called.
If GEMINI_VALIDATION_PASS is enabled in the server configuration, a second pass is automatically run to detect duplicate physical copies.
Request Body — multipart/form-data
| Parameter | Type | Description |
|---|---|---|
| imagerequired | file | JPEG or PNG bookshelf photo |
Response Fields
done · failedtitle and authorResponse Codes
curl -X POST /api/scans/shelf/ \ -F "image=@/path/to/shelf.jpg"
{
"id": 7,
"status": "done",
"book_count": 14,
"processing_time_ms": 3241,
"books": [
{
"title": "The City & The City",
"author": "China Miéville"
},
/* ... */
]
}
Retrieves the results of a previous scan job by its ID. Since scanning is synchronous, results are immediately available after POST /api/scans/shelf/. This endpoint is useful for retrieving a past scan's results.
Response Codes
GET /api/scans/7/results/
{ /* Same structure as POST /api/scans/shelf/ response */ }
Saves a selected subset of books from a completed scan to the library. For each book, a canonical Book record is found or created (matched by ISBN or title), and a new Copy record is created.
Pass only the books you want to save — this is the review step where incorrect detections are excluded.
Request Body
| Parameter | Type | Description |
|---|---|---|
| booksrequired | array | Array of book objects, each with title and optionally author |
| shelf_idoptional | integer|null | Assign all created copies to this shelf |
Response Codes
curl -X POST /api/scans/7/confirm/ \ -H "Content-Type: application/json" \ -d '{ "shelf_id": 3, "books": [ { "title": "The City & The City", "author": "China Miéville" }, { "title": "Perdido Street Station", "author": "China Miéville" } ] }'
[
{ /* Copy object */ },
{ /* Copy object */ }
]
Submits a single book cover photo for identification. Gemini extracts the title, author, ISBN, and other metadata visible on the cover. No book is saved — call POST /api/scans/cover/confirm/ after reviewing the result.
Request Body — multipart/form-data
| Parameter | Type | Description |
|---|---|---|
| imagerequired | file | JPEG or PNG cover photo |
Response Fields
Response Codes
curl -X POST /api/scans/cover/ \ -H "Authorization: Bearer eyJ..." \ -F "image=@/path/to/cover.jpg"
{
"title": "Perdido Street Station",
"author": "China Miéville",
"isbn": "9780345459404",
"publisher": "Del Rey",
"published_year": 2001,
"genre": "Science Fiction",
"description": null,
"language": "en"
}
Saves a single book to the library using data from a cover scan (or manually supplied). Creates a canonical Book record and a Copy for the authenticated user. Augmentation runs in the background to fill in any missing metadata.
Request Body
| Parameter | Type | Description |
|---|---|---|
| titlerequired | string | |
| authoroptional | string | |
| isbnoptional | string | |
| publisheroptional | string | |
| published_yearoptional | integer | |
| genreoptional | string | |
| descriptionoptional | string | |
| languageoptional | string | |
| cover_urloptional | string | |
| shelf_idoptional | integer | Assign the copy to a shelf |
| reading_statusoptional | string | unread (default) · reading · read |
Response Codes
curl -X POST /api/scans/cover/confirm/ \ -H "Authorization: Bearer eyJ..." \ -H "Content-Type: application/json" \ -d '{ "title": "Perdido Street Station", "author": "China Miéville", "isbn": "9780345459404", "shelf_id": 3 }'
{
/* Copy object */,
"_augmentation": { "status": "queued" }
}
Shelves & Locations
Returns all of the authenticated user's shelves ordered by location name and number.
Response Fields (per shelf)
"Living Room - Shelf 2"Response Codes
GET /api/shelves/
[
{
"id": 1,
"display": "Living Room - Shelf 1",
"location_name": "Living Room",
"number": 1,
"book_count": 14
}
]
Creates a shelf. Finds or creates a UserLocation for the authenticated user with the given name, then creates a shelf at that location.
Request Body
| Parameter | Type | Description |
|---|---|---|
| location_namerequired | string | Name of the location, e.g. "Living Room" |
| numberrequired | integer | Shelf number within the location |
Response Codes
curl -X POST /api/shelves/ \ -H "Authorization: Bearer eyJ..." \ -H "Content-Type: application/json" \ -d '{ "location_name": "Study", "number": 1 }'
{
"id": 4,
"display": "Study - Shelf 1",
"location_name": "Study",
"number": 1,
"book_count": 0
}
Returns a single shelf by ID.
Response Codes
GET /api/shelves/4/
{ /* Shelf object */ }
Deletes a shelf. All copies assigned to this shelf are unassigned (their shelf is set to null) — no copies are deleted.
Response Codes
curl -X DELETE /api/shelves/4/ \ -H "Authorization: Bearer eyJ..."
204 No Content
Returns all available location names — global defaults merged with the authenticated user's custom locations. Use to populate location dropdowns when creating shelves.
Response Fields
true if created by this user, false if a global defaultResponse Codes
GET /api/locations/
[
{ "name": "Living Room", "is_custom": false },
{ "name": "Study", "is_custom": true }
]
Utilities
Returns reading statistics for the authenticated user's library.
Response Fields
Response Codes
GET /api/stats/
{
"total": 142,
"unread": 87,
"reading": 3,
"read": 52,
"on_loan": 4
}
Returns the API status. Use this to verify the server is reachable.
Response Codes
GET /api/health/
{ "status": "ok" }