Monolithic api-reference.md: - New 'System Settings & Configuration' and 'Hash Conflicts' sections (endpoints, examples, response shapes) with TOC entries - Device Management: add the sidecar config/download endpoints - Fix stale registration flow: correct auth_url path, drop phantom device_id, add poll_interval/setup_instructions, status endpoint is POST /api/devices/register/status, and sync_endpoints point at /api/sync/koreader/* - Mark PUT /api/libraries/scan-settings as legacy/superseded - Repair Additional Resources and Collections links (dead COLLECTIONS_API.md / KOBO*_SETUP.md / missing-guide references) Split api-reference.md index: - Quick links and sections for System (settings + config) and the admin hash-conflict endpoints; device sidecar endpoints under Device Management; browse + legacy scan-settings routes under Libraries
36 KiB
Bookhoard API Reference
⚠️ Legacy Document: This is a monolithic API reference (1,600+ lines). For updated, split endpoint documentation with interactive API explorer, see API Documentation Portal.
Use the split docs for:
- Easier navigation by category
- Interactive API explorer
- Endpoint-specific examples
- Latest updates
Complete API documentation for Bookhoard v1.0 with Universal Cross-Platform Sync support.
Table of Contents
- Authentication
- Users & Profiles
- Libraries
- Media Items
- Reading Progress
- Notes & Highlights
- Ratings
- Device Management
- Analytics
- Book Matching & Linking
- Collections → See Collections API
- OPDS
- Sync Protocol - KOReader
- Sync Protocol - Kobo
- Universal Progress
- Conflicts
- Sync Queue
- System Settings & Configuration
- Hash Conflicts
- WebSocket
Base URL
Production: https://your-domain.com/api
Development: http://localhost:8765/api
Authentication
Most endpoints require authentication. Include your JWT token in the Authorization header:
Authorization: Bearer <your-jwt-token>
Register User
POST /api/auth/register
Content-Type: application/json
{
"email": "user@example.com",
"username": "john",
"password": "SecureP@ss123!",
"first_name": "John",
"last_name": "Doe"
}
Response (201):
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "d4f5g6h7...",
"user": {
"id": "uuid-here",
"email": "user@example.com",
"username": "john",
"role": "user",
"theme": "tokyo-night",
"created_at": "2026-01-31T10:00:00Z"
}
}
Login
POST /api/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "SecureP@ss123!"
}
Response (200):
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "d4f5g6h7...",
"user": {
"id": "uuid-here",
"email": "user@example.com",
"username": "john",
"role": "user"
}
}
Refresh Token
POST /api/auth/refresh
Content-Type: application/json
{
"refresh_token": "d4f5g6h7..."
}
Response (200):
{
"token": "new-jwt-token",
"refresh_token": "new-refresh-token"
}
Logout
POST /api/auth/logout
Authorization: Bearer <token>
Response (204): No Content
Users & Profiles
Get Current User
GET /api/users/me
Authorization: Bearer <token>
Response (200):
{
"id": "uuid",
"email": "user@example.com",
"username": "john",
"first_name": "John",
"last_name": "Doe",
"theme": "tokyo-night",
"role": "user",
"max_devices": 10,
"created_at": "2026-01-31T10:00:00Z"
}
Update Profile
PUT /api/users/me/profile
Authorization: Bearer <token>
Content-Type: application/json
{
"first_name": "John",
"last_name": "Smith"
}
Update Theme
PUT /api/users/me/theme
Authorization: Bearer <token>
Content-Type: application/json
{
"theme": "dracula"
}
Change Password
PUT /api/users/me/password
Authorization: Bearer <token>
Content-Type: application/json
{
"current_password": "oldPassword",
"new_password": "NewSecureP@ss123!"
}
Update Scan Settings (Legacy)
Superseded by
PUT /api/system/settings(see System Settings & Configuration); kept for backward compatibility.
PUT /api/libraries/scan-settings
Authorization: Bearer <token>
Content-Type: application/json
{
"scan_poll_interval_seconds": 60,
"auto_scan_enabled": true
}
Libraries
Get Visible Libraries
GET /api/libraries/visible
Authorization: Bearer <token>
Response (200):
{
"libraries": [
{
"id": "uuid",
"name": "My Ebooks",
"description": "Ebook collection",
"type_name": "ebooks",
"is_visible": true
}
]
}
Get Library Details
GET /api/libraries/{library_id}
Authorization: Bearer <token>
Create Library (Admin Only)
POST /api/libraries
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "Comics Collection",
"description": "Digital comics",
"type": "comics"
}
Add Library Folder (Admin Only)
POST /api/libraries/{library_id}/folders
Authorization: Bearer <token>
Content-Type: application/json
{
"folder_path": "/path/to/comics"
}
Set Library Visibility (Admin Only)
POST /api/libraries/visibility
Authorization: Bearer <token>
Content-Type: application/json
{
"user_id": "user-uuid",
"library_id": "library-uuid",
"is_visible": true
}
Media Items
List Media Items
GET /api/media-items?library_id={library_id}&limit=20&offset=0
Authorization: Bearer <token>
Query Parameters:
library_id(required): UUID of librarylimit: Number of items to return (max 100, default 20)offset: Number of items to skip
Response (200):
{
"media_items": [
{
"id": "uuid",
"library_id": "uuid",
"title": "Book Title",
"author": "Author Name",
"description": "Book description",
"file_path": "/path/to/book.epub",
"file_size": 1024000,
"mime_type": "application/epub+zip",
"cover_image_path": "/path/to/cover.jpg",
"series": "Series Name",
"series_number": 1,
"tags": "sci-fi, space opera",
"language": "en",
"page_count": 350,
"genre": "Science Fiction",
"copyright_year": 2023,
"created_at": "2026-01-31T10:00:00Z"
}
],
"total": 100
}
Get Media Item
GET /api/media-items/{media_id}
Authorization: Bearer <token>
Search Media Items
GET /api/media-items/search?q={query}&limit=20&offset=0
Authorization: Bearer <token>
Query Parameters:
q(required): Search query (minimum 2 characters)limit: Number of results (default 20)offset: Number to skip
Response (200):
{
"results": [
{
"id": "uuid",
"title": "Book Title",
"author": "Author Name",
"match_score": 0.95
}
]
}
Update Media Item (Admin Only)
PUT /api/media-items/{media_id}
Authorization: Bearer <token>
Content-Type: application/json
{
"title": "Updated Title",
"author": "Updated Author",
"description": "Updated description",
"series": "Series",
"series_number": 2
}
Delete Media Item (Admin Only)
DELETE /api/media-items/{media_id}
Authorization: Bearer <token>
Reading Progress
Get Reading Progress
GET /api/media-items/{media_id}/progress
Authorization: Bearer <token>
Response (200):
{
"media_item_id": "uuid",
"user_id": "uuid",
"current_page": 45,
"total_pages": 200,
"percentage": 0.225,
"character_offset": 15432,
"epubcfi": "epubcfi(/6/4/2:15)",
"chapter": 3,
"chapter_progress": 0.5,
"last_read_at": "2026-01-31T10:00:00Z",
"format_group": "reflowable",
"viewport_y": 0.12,
"zoom_level": 1.0
}
Update Reading Progress
PUT /api/media-items/{media_id}/progress
Authorization: Bearer <token>
Content-Type: application/json
{
"source": "web",
"location": {
"percentage": 0.45678,
"epubcfi": "epubcfi(/6/4/2:15)",
"character": 15432,
"chapter": 3,
"page": 89,
"total_pages": 200
},
"device_metadata": {
"device_type": "web",
"user_agent": "Mozilla/5.0..."
}
}
Response (200):
{
"sync_status": "success",
"progress_updated": true,
"devices_notified": ["device-1", "device-2"],
"broadcast": true
}
Delete Reading Progress
DELETE /api/media-items/{media_id}/progress
Authorization: Bearer <token>
Notes & Highlights
Get Notes
GET /api/media-items/{media_id}/notes
Authorization: Bearer <token>
Response (200):
{
"notes": [
{
"id": "uuid",
"media_item_id": "uuid",
"user_id": "uuid",
"content": "This is an interesting passage...",
"position": "epubcfi(/6/4/2:15)",
"percentage_location": 0.45,
"character_start": 15432,
"character_end": 15480,
"epubcfi_location": "epubcfi(/6/4/2:15)",
"created_at": "2026-01-31T10:00:00Z",
"updated_at": "2026-01-31T10:00:00Z"
}
]
}
Create Note
POST /api/media-items/{media_id}/notes
Authorization: Bearer <token>
Content-Type: application/json
{
"content": "This is a note",
"position": "epubcfi(/6/4/2:15)",
"percentage_location": 0.45,
"epubcfi_location": "epubcfi(/6/4/2:15)"
}
Update Note
PUT /api/media-items/notes/{note_id}
Authorization: Bearer <token>
Content-Type: application/json
{
"content": "Updated note content",
"position": "epubcfi(/6/4/2:20)"
}
Delete Note
DELETE /api/media-items/notes/{note_id}
Authorization: Bearer <token>
Get Highlights
GET /api/media-items/{media_id}/highlights
Authorization: Bearer <token>
Response (200):
{
"highlights": [
{
"id": "uuid",
"media_item_id": "uuid",
"user_id": "uuid",
"selection_text": "Highlighted text passage...",
"start_position": "epubcfi(/6/4/2:15)",
"end_position": "epubcfi(/6/4/2:20)",
"color": "#ffff00",
"percentage_start": 0.45,
"percentage_end": 0.47,
"character_start": 15432,
"character_end": 15480,
"epubcfi_start": "epubcfi(/6/4/2:15)",
"epubcfi_end": "epubcfi(/6/4/2:20)",
"created_at": "2026-01-31T10:00:00Z"
}
]
}
Create Highlight
POST /api/media-items/{media_id}/highlights
Authorization: Bearer <token>
Content-Type: application/json
{
"selection_text": "Highlighted text...",
"start_position": "epubcfi(/6/4/2:15)",
"end_position": "epubcfi(/6/4/2:20)",
"color": "#ffff00",
"percentage_start": 0.45,
"percentage_end": 0.47
}
Update Highlight
PUT /api/media-items/highlights/{highlight_id}
Authorization: Bearer <token>
Content-Type: application/json
{
"selection_text": "Updated text",
"color": "#00ff00"
}
Delete Highlight
DELETE /api/media-items/highlights/{highlight_id}
Authorization: Bearer <token>
Ratings
Get Rating
GET /api/media-items/{media_id}/rating
Authorization: Bearer <token>
Response (200):
{
"rating": 8,
"user_id": "uuid",
"media_item_id": "uuid"
}
Set Rating
POST /api/media-items/{media_id}/rating
Authorization: Bearer <token>
Content-Type: application/json
{
"rating": 8
}
Rating Scale: 1-10 (odd numbers = half-stars: 1=0.5★, 2=1★, 3=1.5★, ..., 9=4.5★, 10=5★)
Update Rating
PUT /api/media-items/{media_id}/rating
Authorization: Bearer <token>
Content-Type: application/json
{
"rating": 9
}
Delete Rating
DELETE /api/media-items/{media_id}/rating
Authorization: Bearer <token>
Device Management
Register Device
POST /api/devices/register
Content-Type: application/json
{
"device_name": "My Kobo Clara",
"device_type": "kobo|koreader|web|mobile",
"device_identifier": "hardware-specific-id"
}
Response (201):
{
"registration_id": "registration-uuid",
"auth_url": "https://bookhoard.com/devices/approve/abc123",
"qr_code": "data:image/png;base64,iVBORw0KG...",
"expires_in": 300,
"poll_interval": 3,
"setup_instructions": {
"koreader": "Calibre URL: https://bookhoard.com/api/sync/koreader"
}
}
Open auth_url (or scan the QR code) while logged in to approve; the registration expires after 5 minutes.
Check Registration Status
POST /api/devices/register/status
Content-Type: application/json
{
"registration_id": "registration-uuid"
}
Response (200):
{
"status": "pending|approved",
"auth_token": "device-bearer-token...",
"device_id": "uuid",
"sync_endpoints": {
"progress": "https://bookhoard.com/api/sync/koreader/progress",
"metadata": "https://bookhoard.com/api/sync/koreader/metadata",
"bookmarks": "https://bookhoard.com/api/sync/koreader/bookmarks"
}
}
List User Devices
GET /api/devices
Authorization: Bearer <token>
Response (200):
{
"devices": [
{
"id": "uuid",
"device_name": "My Kobo Clara",
"device_type": "kobo",
"last_sync": "2026-01-31T10:00:00Z",
"last_seen": "2026-01-31T10:05:00Z",
"sync_enabled": true,
"auto_sync": true,
"sync_frequency_minutes": 5
}
]
}
Update Device Settings
PUT /api/devices/{device_id}
Authorization: Bearer <token>
Content-Type: application/json
{
"device_name": "Updated Name",
"sync_enabled": true,
"auto_sync": true,
"sync_frequency_minutes": 5
}
Revoke Device
DELETE /api/devices/{device_id}
Authorization: Bearer <token>
Get Device Sidecar Config
Returns the .bookhoard.json sidecar config for a device (server endpoints, books keyed by per-format SHA-256, collections) used by the KOReader plugin to self-configure.
GET /api/devices/{device_id}/sidecar
Authorization: Bearer <token>
Also available as a file download:
GET /api/devices/{device_id}/sidecar/download
Authorization: Bearer <token>
Analytics
Get Reading Statistics
GET /api/analytics/reading-stats?start_date={date}&end_date={date}
Authorization: Bearer <token>
Query Parameters:
start_date(optional): Start date (ISO 8601 format)end_date(optional): End date (ISO 8601 format)
Response (200):
{
"pages_read": 1250,
"books_completed": 5,
"reading_time_hours": 42.5,
"sessions_count": 28,
"average_session_minutes": 91
}
Get Device Usage Statistics
GET /api/analytics/device-usage
Authorization: Bearer <token>
Response (200):
{
"devices": [
{
"device_id": "uuid",
"device_name": "My Kobo Clara",
"device_type": "kobo",
"sync_count": 145,
"last_sync": "2026-02-01T10:00:00Z",
"pages_synced": 890
}
]
}
Get Popular Books
GET /api/analytics/popular-books?limit={limit}
Authorization: Bearer <token>
Query Parameters:
limit(optional): Number of results (default: 10)
Response (200):
{
"books": [
{
"media_item_id": "uuid",
"title": "Book Title",
"author": "Author Name",
"cover_image_path": "/covers/book.jpg",
"read_count": 15,
"average_rating": 8.5
}
]
}
Book Matching & Linking
Query Books for Matching
POST /api/sync/books/query
Authorization: Bearer <token>
Content-Type: application/json
{
"identifiers": ["isbn:978-0345391802", "uuid:abc-123"],
"sha256": "a1b2c3d4e5f6abc123...",
"title": "The Hobbit",
"author": "J.R.R. Tolkien",
"file_size": 2456789
}
Response (200):
{
"matches": [
{
"media_item_id": "uuid-123",
"bookhoard_uuid": "uuid-123",
"confidence": 1.0,
"match_method": "uuid_match"
}
],
"action": "auto_link"
}
Bulk Link Books
POST /api/sync/bulk-link-books
Authorization: Bearer <token>
Content-Type: application/json
{
"links": [
{
"unlinked_book_id": "uuid-1",
"media_item_id": "uuid-2",
"confidence_score": 1.0
}
]
}
Response (200):
{
"results": [
{
"unlinked_book_id": "uuid-1",
"status": "success",
"media_item_id": "uuid-2"
}
],
"total": 1,
"successful": 1,
"failed": 0
}
Auto-Link Books
POST /api/sync/auto-link-books
Authorization: Bearer <token>
Content-Type: application/json
{
"confidence_threshold": 0.8,
"limit": 50
}
Response (200):
{
"auto_linked": 15,
"results": [
{
"unlinked_book_id": "uuid-1",
"title": "The Hobbit",
"matched_media_item_id": "uuid-2",
"confidence": 0.95,
"match_method": "sha256_match"
}
]
}
Get Unlinked Book Suggestions
GET /api/sync/unlinked-books/{id}/suggestions
Authorization: Bearer <token>
Response (200):
{
"unlinked_book_id": "uuid-1",
"title_from_device": "The Hobbit",
"sha256": "",
"suggestions": [
{
"media_item_id": "uuid-2",
"bookhoard_uuid": "uuid-2",
"confidence": 0.95,
"match_method": "sha256_match"
}
],
"total_suggestions": 1,
"action": "auto_link"
}
Collections
For complete collection management documentation, see Collections API.
Quick Reference:
GET /api/collections- List all collectionsPOST /api/collections- Create new collectionGET /api/collections/{id}- Get collection detailsPUT /api/collections/{id}- Update collectionDELETE /api/collections/{id}- Delete collectionPOST /api/collections/test-rules- Test auto-assign rulesPOST /api/collections/bulk-add-books- Bulk add books to collectionsGET /api/collections/{id}/books- Get books in collection
Features:
- Auto-assign rules based on genre, author, series, tags, language, publisher, year
- Device shelf mappings (Kobo shelves, KOReader categories)
- Test rules before applying
- Bulk book operations
OPDS (Open Publication Distribution System)
Get Device Catalog
GET /opds/devices/{deviceId}/catalog?page={page}&per_page={per_page}
Query Parameters:
page(optional): Page number (default: 1)per_page(optional): Items per page (default: 50, max: 200)
The feed is paginated via standard OPDS link relations. Clients (e.g. KOReader)
walk pages by following the rel="next" link until it is absent. OpenSearch
paging metadata (totalResults, itemsPerPage, startIndex) is also included.
Response (200 - OPDS 1.2 XML):
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:opds="http://opds-spec.org/2010/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">
<id>urn:uuid:device-id</id>
<title>Bookhoard Library</title>
<updated>2026-02-01T12:00:00Z</updated>
<link rel="self" href="http://localhost:8765/opds/devices/kobo-id/catalog?page=2&per_page=50"/>
<link rel="start" href="http://localhost:8765/opds/devices/kobo-id/catalog?page=1&per_page=50"/>
<link rel="first" href="http://localhost:8765/opds/devices/kobo-id/catalog?page=1&per_page=50"/>
<link rel="previous" href="http://localhost:8765/opds/devices/kobo-id/catalog?page=1&per_page=50"/>
<link rel="next" href="http://localhost:8765/opds/devices/kobo-id/catalog?page=3&per_page=50"/>
<link rel="last" href="http://localhost:8765/opds/devices/kobo-id/catalog?page=37&per_page=50"/>
<link rel="search" type="application/opensearchdescription+xml"
href="http://localhost:8765/opds/devices/kobo-id/search"/>
<opensearch:totalResults>1814</opensearch:totalResults>
<opensearch:itemsPerPage>50</opensearch:itemsPerPage>
<opensearch:startIndex>51</opensearch:startIndex>
<entry>
<id>urn:uuid:bookhoard-uuid-123</id>
<title>The Hobbit</title>
<author><name>J.R.R. Tolkien</name></author>
<updated>2026-02-01T10:00:00Z</updated>
<link href="http://localhost:8765/opds/devices/kobo-id/download/uuid-123"
type="application/epub+zip"
rel="http://opds-spec.org/acquisition/open-access"/>
<link href="http://localhost:8765/opds/devices/kobo-id/download/uuid-123?format=kepub"
type="application/vnd.kobo+xml+zip"
rel="alternate"/>
<dc:identifier id="bookhoard">uuid-123</dc:identifier>
<meta property="bookhoard:sha256">abc123...</meta>
</entry>
</feed>
Download Book with Format Conversion
GET /opds/devices/{deviceId}/download/{bookId}?format={format}
Query Parameters:
format(optional): Book format -epub(default),kepub
Response (200):
- Headers:
Content-Type:application/epub+ziporapplication/vnd.kobo+xml+zipContent-Disposition: attachment; filename="The Hobbit.epub"X-Bookhoard-UUID: uuid-123X-Bookhoard-SHA256: abc123... (original hash)X-Bookhoard-KEPUB-SHA256: xyz789... (KEPUB hash if format=kepub)
Search OPDS Catalog
GET /opds/devices/{deviceId}/search # OpenSearch description
GET /opds/devices/{deviceId}/search?q={query} # search results feed
When called without a q parameter, returns an OpenSearch description
document (application/opensearchdescription+xml). OPDS clients fetch this to
learn the search URL template, then substitute {searchTerms}:
<?xml version="1.0" encoding="UTF-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>Bookhoard</ShortName>
<Description>Search the Bookhoard library</Description>
<InputEncoding>UTF-8</InputEncoding>
<OutputEncoding>UTF-8</OutputEncoding>
<Url type="application/atom+xml;profile=opds-catalog;kind=acquisition"
template="http://localhost:8765/opds/devices/kobo-id/search?q={searchTerms}"/>
</OpenSearchDescription>
When called with a q parameter, Response (200 - OPDS 1.2 XML with
search results, including opensearch:totalResults).
List Available Formats
GET /opds/devices/{deviceId}/formats/{bookId}
Response (200):
{
"media_item_id": "uuid-123",
"formats": [
{
"format_type": "epub",
"file_path": "/path/to/book.epub",
"file_sha256": "abc123...",
"file_size_bytes": 2456789,
"mime_type": "application/epub+zip",
"available": true
},
{
"format_type": "kepub",
"file_path": "/cache/book.kepub.epub",
"file_sha256": "xyz789...",
"file_size_bytes": 2478932,
"mime_type": "application/vnd.kobo+xml+zip",
"available": true
}
]
}
Sync Protocol - KOReader
KOReader Progress Sync
POST /api/sync/koreader/progress
Authorization: Bearer <device_token>
Content-Type: application/json
{
"library_id": "optional-uuid",
"books": [
{
"uuid": "book-uuid",
"title": "Book Title",
"authors": ["Author Name"],
"progress": 0.45,
"percentage": 0.45,
"last_read": "2026-01-30T20:00:00Z",
"chapter": 3,
"epubcfi": "epubcfi(/6/4/2:15)",
"character": 15432,
"bookmarks": [
{
"chapter": 3,
"datetime": "2026-01-30T19:55:00Z",
"notes": "highlighted text",
"pos0": "epubcfi(/6/4/2:15)",
"pos1": "epubcfi(/6/4/2:20)",
"page": 45,
"text": "highlighted text excerpt",
"type": "highlight"
}
],
"highlights": [],
"notes": []
}
]
}
Response (202):
{
"sync_status": "accepted",
"books_synced": 1,
"conflicts": [
{
"book_uuid": "book-uuid",
"conflict_type": "progress_mismatch",
"device_progress": 0.45,
"server_progress": 0.42,
"resolution": "device_wins"
}
]
}
KOReader Metadata Fetch
GET /api/sync/koreader/metadata/{book_uuid}
Authorization: Bearer <device_token>
Response (200):
{
"uuid": "book-uuid",
"title": "Book Title",
"authors": ["Author Name"],
"progress": {
"percentage": 0.42,
"character": 15432,
"epubcfi": "epubcfi(/6/4/2:15)",
"chapter": 3,
"chapter_progress": 0.234
},
"annotations": {
"highlights": [...],
"notes": [...],
"bookmarks": [...]
},
"last_sync": "2026-01-30T20:00:00Z"
}
Sync Protocol - Kobo
Status: Coming Soon — Native Kobo sync is implemented server-side but not yet supported on real devices. These endpoints are under active development and may change.
Kobo Markup Sync
POST /api/sync/kobo/markup
Authorization: Bearer <device_token>
x-kobo-device: {"DeviceId":"device-id","Model":"Kobo Clara"}
Content-Type: application/json
{
"ReadingSync": [
{
"ContentId": "book-uuid",
"PercentRead": 45.6,
"EntitlementId": "entitlement-id",
"RemainingTimeMinutes": 120,
"LastModified": "2026-01-30T20:00:00Z"
}
],
"BookmarkSync": [
{
"ContentId": "book-uuid",
"BookmarkText": "highlighted text",
"BookmarkType": "annotation",
"BookmarkTitle": "Chapter 3"
}
]
}
Response (200):
{
"Status": "Success",
"MarkupsSynced": 5,
"BookmarksSynced": 3
}
Kobo Library Fetch
GET /api/sync/kobo/library
Authorization: Bearer <device_token>
Response (200):
{
"library_sync": [
{
"ContentId": "book-uuid",
"ContentType": "6",
"Title": "Book Title",
"Author": "Author Name",
"PercentRead": 42.3,
"PagesRemaining": 115,
"BookmarkCount": 3,
"LastModified": "2026-01-30T20:00:00Z"
}
]
}
Universal Progress
Get Universal Progress
GET /api/progress/{book_uuid}
Authorization: Bearer <token>
Response (200):
{
"book_id": "book-uuid",
"format_group": "reflowable",
"universal_progress": 0.45678,
"location_references": {
"percentage": 0.45678,
"epubcfi": "epubcfi(/6/4/2:15)",
"character": 15432,
"chapter": 3,
"chapter_progress": 0.234,
"viewport_y": 0.12
},
"device_progress": {
"koreader": {
"percentage": 0.45678,
"last_sync": "2026-01-30T20:00:00Z"
},
"kobo": {
"percentage": 45.6,
"last_sync": "2026-01-30T19:55:00Z"
},
"web": {
"display_page": 89,
"total_pages": 200,
"last_sync": "2026-01-30T20:05:00Z"
}
},
"annotations": {
"highlights": [...],
"notes": [...],
"bookmarks": [...]
},
"conflicts": [
{
"id": "conflict-uuid",
"type": "progress",
"resolved": false,
"sources": ["koreader", "kobo"]
}
]
}
Update Universal Progress
POST /api/progress/{book_uuid}
Authorization: Bearer <token>
Content-Type: application/json
{
"source": "web|koreader|kobo|mobile",
"location": {
"percentage": 0.45678,
"epubcfi": "epubcfi(/6/4/2:15)",
"character": 15432,
"chapter": 3,
"page": 89,
"total_pages": 200
},
"device_metadata": {
"device_type": "web",
"user_agent": "..."
}
}
Conflicts
List Conflicts
GET /api/conflicts?status=unresolved&type=progress
Authorization: Bearer <token>
Query Parameters:
status: "unresolved|all" (default: "unresolved")type: "progress|note|highlight|all" (default: "all")
Response (200):
{
"conflicts": [
{
"id": "conflict-uuid",
"media_item_id": "book-uuid",
"media_item_title": "Book Title",
"conflict_type": "progress",
"conflict_data": {
"koreader": {
"source": "koreader",
"timestamp": "2026-01-30T20:10:00Z",
"data": {
"percentage": 0.45,
"epubcfi": "epubcfi(/6/4/2:15)",
"character": 15432
}
},
"kobo": {
"source": "kobo",
"timestamp": "2026-01-30T20:05:00Z",
"data": {
"percentage": 0.42
}
}
},
"resolution_status": "unresolved",
"created_at": "2026-01-30T20:10:05Z"
}
],
"total": 1,
"unresolved": 1
}
Get Conflict Details
GET /api/conflicts/{conflict_id}
Authorization: Bearer <token>
Resolve Conflict
POST /api/conflicts/{conflict_id}/resolve
Authorization: Bearer <token>
Content-Type: application/json
{
"winner": "koreader|kobo|web|manual",
"manual_data": {
"percentage": 0.43,
"epubcfi": "epubcfi(/6/4/2:20)",
"character": 15500
},
"apply_to_all_future_conflicts": false,
"reason": "user chose more recent progress"
}
Response (200):
{
"conflict_resolved": true,
"applied_to": {
"progress": true,
"annotations": false
},
"devices_synced": ["device-1", "device-2"]
}
Delete Conflict
DELETE /api/conflicts/{conflict_id}
Authorization: Bearer <token>
Dismiss All Resolved
DELETE /api/conflicts/dismiss-resolved
Authorization: Bearer <token>
Sync Queue
List Queue Items
GET /api/queue/items?limit=50&offset=0
Authorization: Bearer <token>
Response (200):
{
"items": [
{
"id": "uuid",
"device_id": "device-uuid",
"device_name": "My Kobo",
"media_item_id": "book-uuid",
"sync_type": "progress",
"sync_data": {},
"priority": 5,
"attempts": 0,
"max_attempts": 3,
"status": "pending",
"error_message": null,
"created_at": "2026-01-31T10:00:00Z"
}
],
"total": 100
}
Process Queue Item
POST /api/queue/items/{queue_item_id}/process
Authorization: Bearer <token>
Retry Queue Item
POST /api/queue/items/{queue_item_id}/retry
Authorization: Bearer <token>
Delete Queue Item
DELETE /api/queue/items/{queue_item_id}
Authorization: Bearer <token>
Clear Queue
DELETE /api/queue/clear
Authorization: Bearer <token>
Clear Failed Items
DELETE /api/queue/clear-failed
Authorization: Bearer <token>
Get Queue Stats
GET /api/queue/stats
Authorization: Bearer <token>
Response (200):
{
"pending": 15,
"processing": 2,
"failed": 3,
"completed": 100,
"total": 120
}
System Settings & Configuration
List All Settings
Returns every tunable setting with current value and metadata (type, range, category, group, description, requires_restart, is_default).
GET /api/system/settings
Authorization: Bearer <admin_token>
Response (200):
[
{
"key": "scan_poll_interval_seconds",
"value": "60",
"type": "int",
"min": "1",
"max": "3600",
"requires_restart": false,
"category": "scanner",
"group": "Scanning",
"description": "How often to scan all libraries (seconds)",
"is_default": true
}
]
Update a Setting
Type-aware validation (int range, bool parse, IANA timezone for default_timezone), persists the value, reloads the registry, and reports whether a restart is needed.
PUT /api/system/settings
Authorization: Bearer <admin_token>
Content-Type: application/json
{
"key": "scan_poll_interval_seconds",
"value": "30"
}
Response (200): the updated entry plus reload_required.
Setting categories: scanner (scan_poll_interval_seconds, auto_scan_enabled), general (default_timezone), security (session duration, password rules, auth rate limit, login lockout), api (OPDS page sizes, device rate limits), sync (annotation tombstone TTL, sync queue interval/batch), performance (conversion cache TTL, worker pool size/capacity). See System Settings API for the full catalog.
Get / Update Raw System Config
Flat key/value configuration (e.g. base_url), including keys without registry metadata.
GET /api/system/config
PUT /api/system/config
Authorization: Bearer <admin_token>
Hash Conflicts
Duplicate content discovered during hashing (import, rescan, or the startup backfill) is grouped into hash conflicts for an explicit keep/merge decision. Files on disk are never deleted.
List Hash Conflicts
GET /api/admin/hash-conflicts
Authorization: Bearer <admin_token>
Response (200): { "conflicts": [ { id, library_id, library_name, sha256, created_at, items: [ { id, title, author, file_path, file_size, created_at, progress_count, highlight_count, bookmark_count, note_count, collection_count } ] } ], "total": n }
Resolve Hash Conflict
POST /api/admin/hash-conflicts/:id/resolve
Authorization: Bearer <admin_token>
Content-Type: application/json
{
"action": "keep",
"keep_uuid": "media-item-uuid-to-keep"
}
action=keep— merge every other copy's child rows (progress, highlights, bookmarks, notes, collections) into the kept item, then delete the losersaction=keep_all— copies are intentional; dismiss the conflict
Errors: 400 (bad ID / missing keep_uuid), 404 (not found), 409 (already resolved).
WebSocket
Connect to WebSocket
WS /ws/sync?token=<token>
Message Format
Client → Server (Heartbeat):
{
"type": "ping"
}
Server → Client (Progress Update):
{
"type": "progress_update",
"timestamp": "2026-01-31T10:00:00Z",
"data": {
"book_id": "uuid",
"progress": {
"percentage": 0.45678,
"epubcfi": "epubcfi(/6/4/2:15)",
"chapter": 3
},
"annotations": {}
},
"source_device": {
"id": "device-uuid",
"name": "My Kobo",
"type": "kobo"
}
}
Server → Client (Conflict Detected):
{
"type": "conflict",
"timestamp": "2026-01-31T10:00:00Z",
"data": {
"book_id": "uuid",
"conflict_id": "uuid",
"conflict_type": "progress"
}
}
Server → Client (Pong):
{
"type": "pong"
}
Error Responses
All endpoints return standardized error responses:
{
"error": "Error message",
"message": "Detailed error information (if available)",
"code": "ERROR_CODE"
}
HTTP Status Codes
- 200: OK - Request successful
- 201: Created - Resource created successfully
- 204: No Content - Successful deletion or update with no content
- 400: Bad Request - Invalid request parameters
- 401: Unauthorized - Missing or invalid authentication
- 403: Forbidden - Insufficient permissions
- 404: Not Found - Resource does not exist
- 409: Conflict - Resource conflict (e.g., duplicate)
- 422: Unprocessable Entity - Validation error
- 429: Too Many Requests - Rate limit exceeded
- 500: Internal Server Error - Server error
Rate Limiting
Per-Device Limits:
- Sync requests: 60/minute
- Progress updates: 120/minute
- Metadata requests: 30/minute
Per-User Limits:
- All requests: 300/minute
- Conflict resolutions: 10/minute
- Device registrations: 5/hour
Rate Limit Headers:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 60
Bruno v3.0 Collections
Complete API test collections are available in the bruno/ directory:
bruno/
├── user/ # Authentication & profiles
├── admin/ # Admin operations
├── library/ # Library management
├── media-items/ # Media content
├── progress/ # Reading progress
├── notes/ # Notes API
├── highlights/ # Highlights API
├── ratings/ # Ratings API
├── devices/ # Device management
├── sync-koreader/ # KOReader sync protocol
├── sync-kobo/ # Kobo sync protocol
├── conflicts/ # Conflict resolution
├── queue/ # Sync queue management
└── collection.yml # Main collection file
Testing with Bruno OpenCollection YAML
Install Bruno CLI:
npm install -g @usebruno/cli
Run all tests:
bruno run
Run specific collection:
bruno run bruno/devices/
Additional Resources
- README.md - Getting started guide
- Sync Guide - Sync concepts and conflict resolution
- KOReader Setup - KOReader device setup
- Kobo Setup - Kobo device setup (native sync coming soon)
Document Version: 1.0
Last Updated: 2026-01-31
API Version: v1.0