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.

http://127.0.0.1:8000

Response Format

All endpoints return JSON. List endpoints return a paginated envelope. Errors include an error key with a human-readable message.

Paginated Response

countintegerTotal number of records
nextstring|nullURL of the next page
previousstring|nullURL of the previous page
resultsarrayArray of objects for this page

The Copy Object

A Copy represents a physical book on a shelf. Book metadata is inlined.

idintegerUnique copy ID
book_idintegerID of the canonical Book record
titlestringBook title
authorsstring[]List of author names
isbnstring|nullISBN-13
publisherstring|null
published_yearinteger|null
genrestring|null
descriptionstring|null
cover_urlstring|nullExternal cover image URL
page_countinteger|null
languagestring|null
shelfobject|nullShelf object with id, display, location_name, number
reading_statusstringunread · reading · read
on_loanbooleanWhether the copy is currently on loan
ratinginteger|null1–5
conditionstring|nullexcellent · good · fair · poor
notesstring|null
date_addeddatetimeISO 8601
updated_atdatetimeISO 8601
Example Copy Object
{
  "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

POST /auth/register/

Creates a new account. Returns JWT tokens immediately — no email verification required in the current build.

Request Body

ParameterTypeDescription
emailrequiredstringUsed as the username — must be unique
passwordrequiredstringMinimum 8 characters

Response Fields

accessstringJWT access token — valid 12 hours
refreshstringJWT refresh token — valid 30 days
user.idinteger
user.emailstring
user.usernamestringSame as email

Response Codes

201 Created 400 Bad Request
Request
curl -X POST /auth/register/ \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "password": "supersecret"
  }'
Response — 201 Created
{
  "access": "eyJ...",
  "refresh": "eyJ...",
  "user": {
    "id": 1,
    "email": "user@example.com",
    "username": "user@example.com"
  }
}
POST /auth/login/

Authenticates an existing account and returns fresh JWT tokens.

Request Body

ParameterTypeDescription
emailrequiredstring
passwordrequiredstring

Response Codes

200 OK 401 Unauthorized
Request
curl -X POST /auth/login/ \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "password": "supersecret"
  }'
Response — 200 OK
{
  "access": "eyJ...",
  "refresh": "eyJ...",
  "user": { /* user object */ }
}
POST /auth/token/refresh/

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

ParameterTypeDescription
refreshrequiredstringThe refresh token from login or register

Response Codes

200 OK 401 Unauthorized
Request
curl -X POST /auth/token/refresh/ \
  -H "Content-Type: application/json" \
  -d '{ "refresh": "eyJ..." }'
Response — 200 OK
{
  "access": "eyJ...",
  "refresh": "eyJ..."
}
GET /auth/me/

Returns the currently authenticated user's profile. Requires a valid access token.

Response Codes

200 OK 401 Unauthorized
Request
curl /auth/me/ \
  -H "Authorization: Bearer eyJ..."
Response — 200 OK
{
  "id": 1,
  "email": "user@example.com",
  "username": "user@example.com"
}
PATCH /auth/me/

Changes the authenticated user's password. Returns fresh JWT tokens so the client stays logged in.

Request Body

ParameterTypeDescription
current_passwordrequiredstringMust match the existing password
new_passwordrequiredstringMinimum 8 characters

Response Codes

200 OK 400 Bad Request 401 Unauthorized
Request
curl -X PATCH /auth/me/ \
  -H "Authorization: Bearer eyJ..." \
  -H "Content-Type: application/json" \
  -d '{
    "current_password": "supersecret",
    "new_password": "evenmoresecret"
  }'
Response — 200 OK
{
  "access": "eyJ...",
  "refresh": "eyJ...",
  "user": { /* user object */ }
}

Books

GET /api/books/

Returns a paginated list of all active (non-deleted) copies in the library, with book metadata inlined.

Query Parameters

ParameterTypeDescription
searchoptionalstringFilter by title or author name (case-insensitive)
genreoptionalstringFilter by genre (partial match)
yearoptionalintegerFilter by published year (exact)
orderingoptionalstringtitle, -title, date_added, -date_added, rating, -rating
pageoptionalintegerPage number (default: 1)
page_sizeoptionalintegerResults per page (default: 20)

Response Codes

200 OK
Request
GET /api/books/?search=mieville&ordering=-date_added
Response
{
  "count": 3,
  "next": null,
  "previous": null,
  "results": [{ /* Copy objects */ }]
}
POST /api/books/

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

ParameterTypeDescription
titlerequiredstringBook title
authoroptionalstringAuthor name — found or created automatically
isbnoptionalstringISBN-13
publisheroptionalstring
published_yearoptionalinteger
genreoptionalstring
shelf_idoptionalintegerID of the shelf to assign this copy to
reading_statusoptionalstringunread (default) · reading · read
conditionoptionalstringexcellent · good · fair · poor
ratingoptionalinteger1–5
notesoptionalstring

Response Codes

201 Created 400 Bad Request
Request
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
  }'
Response — 201 Created
{ /* Copy object */ }
GET /api/books/{id}/

Returns a single copy by its ID.

Response Codes

200 OK 404 Not Found
Request
GET /api/books/42/
Response
{ /* Copy object */ }
PATCH /api/books/{id}/

Updates copy-level fields. Only fields provided are changed. Book metadata (title, authors, ISBN) cannot be changed via this endpoint.

Request Body

ParameterTypeDescription
reading_statusoptionalstringunread · reading · read
on_loanoptionalboolean
ratingoptionalinteger1–5
conditionoptionalstringexcellent · good · fair · poor
shelf_idoptionalinteger|nullPass null to remove shelf assignment
notesoptionalstring|null

Response Codes

200 OK 404 Not Found
Request
curl -X PATCH /api/books/42/ \
  -H "Content-Type: application/json" \
  -d '{
    "reading_status": "read",
    "rating": 4,
    "on_loan": false
  }'
Response
{ /* Updated Copy object */ }
DELETE /api/books/{id}/

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

204 No Content 404 Not Found
Request
curl -X DELETE /api/books/42/
Response
204 No Content
PATCH /api/books/{id}/restore/

Restores a previously soft-deleted copy by clearing its deleted_at timestamp.

Response Codes

200 OK 404 Not Found
Request
curl -X PATCH /api/books/42/restore/
Response
{ /* Restored Copy object */ }
GET /api/books/lookup/

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

ParameterTypeDescription
isbnrequiredstringISBN-10 or ISBN-13

Response Fields

matchedbooleanfalse if no record found — remaining fields omitted
titlestring
authorsstring[]All authors
authorstringFirst author (convenience field)
publisherstring|null
published_yearstring|nullAs returned by Open Library (may include month)
cover_urlstring|nullLarge cover image URL
isbnstringThe queried ISBN

Response Codes

200 OK 400 Bad Request
Request
GET /api/books/lookup/?isbn=9780345524256
Response — matched
{
  "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"
}
Response — not found
{ "matched": false }
GET /api/books/export/

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

200 OK
Request
GET /api/books/export/

# Or directly in browser — triggers download
Response Headers
Content-Type: text/csv
Content-Disposition: attachment; filename="library.csv"

Scans

POST /api/scans/shelf/

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

ParameterTypeDescription
imagerequiredfileJPEG or PNG bookshelf photo

Response Fields

idintegerScan job ID — used in subsequent requests
statusstringdone · failed
book_countintegerNumber of books detected
processing_time_msinteger
booksarrayDetected books — each with title and author

Response Codes

202 Accepted 400 Bad Request 500 Internal Server Error
Request
curl -X POST /api/scans/shelf/ \
  -F "image=@/path/to/shelf.jpg"
Response — 202 Accepted
{
  "id": 7,
  "status": "done",
  "book_count": 14,
  "processing_time_ms": 3241,
  "books": [
    {
      "title": "The City & The City",
      "author": "China Miéville"
    },
    /* ... */
  ]
}
GET /api/scans/{id}/results/

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

200 OK 404 Not Found
Request
GET /api/scans/7/results/
Response
{ /* Same structure as POST /api/scans/shelf/ response */ }
POST /api/scans/{id}/confirm/

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

ParameterTypeDescription
booksrequiredarrayArray of book objects, each with title and optionally author
shelf_idoptionalinteger|nullAssign all created copies to this shelf

Response Codes

201 Created 400 Bad Request 404 Not Found
Request
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"
      }
    ]
  }'
Response — 201 Created
[
  { /* Copy object */ },
  { /* Copy object */ }
]
POST /api/scans/cover/

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

ParameterTypeDescription
imagerequiredfileJPEG or PNG cover photo

Response Fields

titlestring|null
authorstring|null
isbnstring|null
publisherstring|null
published_yearinteger|null
genrestring|null
descriptionstring|nullBack-cover blurb if visible
languagestring|null

Response Codes

200 OK 400 Bad Request 500 Internal Server Error
Request
curl -X POST /api/scans/cover/ \
  -H "Authorization: Bearer eyJ..." \
  -F "image=@/path/to/cover.jpg"
Response — 200 OK
{
  "title": "Perdido Street Station",
  "author": "China Miéville",
  "isbn": "9780345459404",
  "publisher": "Del Rey",
  "published_year": 2001,
  "genre": "Science Fiction",
  "description": null,
  "language": "en"
}
POST /api/scans/cover/confirm/

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

ParameterTypeDescription
titlerequiredstring
authoroptionalstring
isbnoptionalstring
publisheroptionalstring
published_yearoptionalinteger
genreoptionalstring
descriptionoptionalstring
languageoptionalstring
cover_urloptionalstring
shelf_idoptionalintegerAssign the copy to a shelf
reading_statusoptionalstringunread (default) · reading · read

Response Codes

201 Created 400 Bad Request
Request
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
  }'
Response — 201 Created
{
  /* Copy object */,
  "_augmentation": { "status": "queued" }
}

Shelves & Locations

GET /api/shelves/

Returns all of the authenticated user's shelves ordered by location name and number.

Response Fields (per shelf)

idinteger
displaystringHuman-readable label e.g. "Living Room - Shelf 2"
location_namestring
numberinteger
book_countintegerNumber of active copies on this shelf

Response Codes

200 OK
Request
GET /api/shelves/
Response
[
  {
    "id": 1,
    "display": "Living Room - Shelf 1",
    "location_name": "Living Room",
    "number": 1,
    "book_count": 14
  }
]
POST /api/shelves/

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

ParameterTypeDescription
location_namerequiredstringName of the location, e.g. "Living Room"
numberrequiredintegerShelf number within the location

Response Codes

201 Created 200 OK 400 Bad Request
Request
curl -X POST /api/shelves/ \
  -H "Authorization: Bearer eyJ..." \
  -H "Content-Type: application/json" \
  -d '{
    "location_name": "Study",
    "number": 1
  }'
Response — 201 Created
{
  "id": 4,
  "display": "Study - Shelf 1",
  "location_name": "Study",
  "number": 1,
  "book_count": 0
}
GET /api/shelves/{id}/

Returns a single shelf by ID.

Response Codes

200 OK 404 Not Found
Request
GET /api/shelves/4/
Response
{ /* Shelf object */ }
DELETE /api/shelves/{id}/

Deletes a shelf. All copies assigned to this shelf are unassigned (their shelf is set to null) — no copies are deleted.

Response Codes

204 No Content 404 Not Found
Request
curl -X DELETE /api/shelves/4/ \
  -H "Authorization: Bearer eyJ..."
Response
204 No Content
GET /api/locations/

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

namestringLocation name
is_custombooleantrue if created by this user, false if a global default

Response Codes

200 OK
Request
GET /api/locations/
Response
[
  { "name": "Living Room", "is_custom": false },
  { "name": "Study", "is_custom": true }
]

Utilities

GET /api/stats/

Returns reading statistics for the authenticated user's library.

Response Fields

totalintegerTotal active copies
unreadinteger
readinginteger
readinteger
on_loanintegerCopies currently on loan

Response Codes

200 OK
Request
GET /api/stats/
Response
{
  "total": 142,
  "unread": 87,
  "reading": 3,
  "read": 52,
  "on_loan": 4
}
GET /api/health/

Returns the API status. Use this to verify the server is reachable.

Response Codes

200 OK
Request
GET /api/health/
Response
{ "status": "ok" }