fix(bruno): convert drifted body keys to body.data — CLI scenario runs now carry bodies

Follow-up to the sweep route audit, correcting its diagnosis: the CLI
DOES read .yml bodies — via OpenCollection's `body.data` string form
(the NewDevDBSetup script always worked because it used it). The
failing scenario POSTs used `body.json` (structured) / `body.jsonBody`
(string) — keys the CLI silently ignores, sending Content-Length: 0
with form-urlencoded content-type, so every server bind failed.

- 81 bodies converted to data-form (json maps serialized; jsonBody
  strings kept verbatim, {{vars}} intact).
- 6 pre-existing TRUNCATED payloads repaired: the original generator
  cut bodies at the first {{var}} (every SHA-256-identified scenario
  plus Sync Progress/Sync Bookmarks shipped mid-JSON and 400'd from
  day one). Rewritten whole, faithful to each file's documented
  purpose; book_sha256/book_uuid added to the environment.
- environments: opds_base_url correction from the sweep retained.

Verified live against the rebuilt dev container: the full koreader
scenario tree runs green through `bru run` — 14/14 (202s across
immediate/checkpoint/SHA-256 progress, 200s bookmarks, highlights,
notes; SHA-256 book identification exercised against a real
file_sha256).
This commit is contained in:
John O'Keefe committed 2026-10-17 12:22:35 -04:00
1 parent 63a20dcc95
commit 09b6693505
83 files changed
+386 -2769

No files matched your search

+9 -10
View File
@@ -2,37 +2,36 @@ info:
name: SetBaseUrl name: SetBaseUrl
type: http type: http
seq: 3 seq: 3
http: http:
method: PUT method: PUT
url: '{{base_url}}/api/system/config' url: '{{base_url}}/api/system/config'
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: |- data: "{\n \"base_url\": \"http://localhost:8765\"\n}"
{
"base_url": "http://localhost:8765"
}
headers: headers:
- key: Authorization - key: Authorization
value: Bearer {{token}} value: Bearer {{token}}
- key: Content-Type - key: Content-Type
value: application/json value: application/json
settings: settings:
encodeUrl: true encodeUrl: true
timeout: 0 timeout: 0
followRedirects: true followRedirects: true
maxRedirects: 5 maxRedirects: 5
docs: '## Set Base URL
docs: |-
## Set Base URL
Configures the server's base_url during initial dev database setup. Configures the server''s base_url during initial dev database setup.
Must be run after RegisterUser (which provides the auth token) and before Must be run after RegisterUser (which provides the auth token) and before
any library/device creation (which require setup to be complete). any library/device creation (which require setup to be complete).
**Method:** PUT **Method:** PUT
**Endpoint:** /api/system/config **Endpoint:** /api/system/config
**Auth:** Bearer token (from RegisterUser)
**Auth:** Bearer token (from RegisterUser)'
+2 -37
View File
@@ -8,43 +8,8 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"media_item_ids\": [\n \"{{bookId1" data: "{\n \"media_item_ids\": [\n \"{{bookId1"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
docs: "## Bulk Delete Media Items\n\n Deletes multiple media items in a single request.\n\n **Method:** POST\n\n **Endpoint:** /api/media-items/bulk-delete\n\n **Authentication:** Required (Bearer token)\n\n **Request Body:**\n - `media_item_ids` (array of strings): Array of media item UUIDs to delete\n\n **Response:**\n - `results` (array): Results for each deletion attempt\n - `total` (number): Total number of media items processed\n - `deleted` (number): Number of successfully deleted media items\n - `failed` (number): Number of failed deletions\n\n **Status Codes:**\n - 200: Success (with partial results if some failed)\n - 400: Invalid request data\n - 401: Unauthorized\n - 403: Forbidden\n - 500: Internal server error\n\n **Example:**\n ```json\n {\n \"media_item_ids\": [\n \"uuid-1\",\n \"uuid-2\",\n \"uuid-3\"\n ]"
docs: |-
## Bulk Delete Media Items
Deletes multiple media items in a single request.
**Method:** POST
**Endpoint:** /api/media-items/bulk-delete
**Authentication:** Required (Bearer token)
**Request Body:**
- `media_item_ids` (array of strings): Array of media item UUIDs to delete
**Response:**
- `results` (array): Results for each deletion attempt
- `total` (number): Total number of media items processed
- `deleted` (number): Number of successfully deleted media items
- `failed` (number): Number of failed deletions
**Status Codes:**
- 200: Success (with partial results if some failed)
- 400: Invalid request data
- 401: Unauthorized
- 403: Forbidden
- 500: Internal server error
**Example:**
```json
{
"media_item_ids": [
"uuid-1",
"uuid-2",
"uuid-3"
]
+3 -42
View File
@@ -8,48 +8,9 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"media_item_updates\": [\n {\n \"media_item_id\"\ data: "{\n \"media_item_updates\": [\n {\n \"media_item_id\": \"{{bookId1"
: \"{{bookId1"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
docs: "## Bulk Update Media Items\n\n Updates multiple media items in a single request with different fields for each item.\n\n **Method:** POST\n\n **Endpoint:** /api/media-items/bulk-update\n\n **Authentication:** Required (Bearer token)\n\n **Request Body:**\n - `media_item_updates` (array): Array of update objects\n - `media_item_id` (string): Media item UUID to update\n - `updates` (object): Fields to update (can include title, author, genre, tags, contributors, etc.)\n\n **Response:**\n - `results` (array): Results for each update attempt\n - `total` (number): Total number of media items processed\n - `updated` (number): Number of successfully updated media items\n - `failed` (number): Number of failed updates\n\n **Status Codes:**\n - 200: Success (with partial results if some failed)\n - 400: Invalid request data\n - 401: Unauthorized\n - 403: Forbidden\n - 500: Internal server error\n\n **Example:**\n ```json\n {\n \"media_item_updates\": [\n {\n\
docs: |- \ \"media_item_id\": \"uuid-1\",\n \"updates\": {\n \"title\": \"New Title\",\n \"genre\": \"Fiction\",\n \"tags\": [\"fiction\", \"adventure\"]"
## Bulk Update Media Items
Updates multiple media items in a single request with different fields for each item.
**Method:** POST
**Endpoint:** /api/media-items/bulk-update
**Authentication:** Required (Bearer token)
**Request Body:**
- `media_item_updates` (array): Array of update objects
- `media_item_id` (string): Media item UUID to update
- `updates` (object): Fields to update (can include title, author, genre, tags, contributors, etc.)
**Response:**
- `results` (array): Results for each update attempt
- `total` (number): Total number of media items processed
- `updated` (number): Number of successfully updated media items
- `failed` (number): Number of failed updates
**Status Codes:**
- 200: Success (with partial results if some failed)
- 400: Invalid request data
- 401: Unauthorized
- 403: Forbidden
- 500: Internal server error
**Example:**
```json
{
"media_item_updates": [
{
"media_item_id": "uuid-1",
"updates": {
"title": "New Title",
"genre": "Fiction",
"tags": ["fiction", "adventure"]
+2 -28
View File
@@ -8,31 +8,5 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"book_ids\": [\n \"{{book_id_1}}" data: "{\n \"book_ids\": [\n \"{{book_id_1}}"
docs: "## Add Books to Collection\n\n Adds multiple books to an existing collection.\n \n **Method:** POST\n \n **Endpoint:** /api/collections/{collection_id}/books\n \n **Authentication:** Required (Bearer token)\n \n **Path Parameters:**\n - `collection_id` (string): Collection ID\n \n **Request Body:**\n - `book_ids` (array of strings, required): List of book IDs to add\n \n **Response:**\n - `message` (string): Success message\n - `added_count` (integer): Number of books added\n \n **Status Codes:**\n - 200: Success\n - 400: Invalid request\n - 401: Unauthorized\n - 404: Collection not found"
docs: |-
## Add Books to Collection
Adds multiple books to an existing collection.
**Method:** POST
**Endpoint:** /api/collections/{collection_id}/books
**Authentication:** Required (Bearer token)
**Path Parameters:**
- `collection_id` (string): Collection ID
**Request Body:**
- `book_ids` (array of strings, required): List of book IDs to add
**Response:**
- `message` (string): Success message
- `added_count` (integer): Number of books added
**Status Codes:**
- 200: Success
- 400: Invalid request
- 401: Unauthorized
- 404: Collection not found
+3 -37
View File
@@ -8,40 +8,6 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"name\": \"Science Fiction\",\n \"description\": \"My favorite\ data: "{\n \"name\": \"Science Fiction\",\n \"description\": \"My favorite sci-fi books\",\n \"color\": \"#ff0000\",\n \"icon\": \"\U0001F680\",\n \"auto_assign_rules\": [\n {\n \"id\": \"rule-1\",\n \"field\": \"genre\",\n \"operator\": \"equals\",\n \"value\": \"Science Fiction\",\n \"priority\": 8"
\ sci-fi books\",\n \"color\": \"#ff0000\",\n \"icon\": \"\U0001F680\"\ docs: "## Create Collection\n\n Creates a new collection for organizing books.\n \n **Method:** POST\n \n **Endpoint:** /api/collections\n \n **Authentication:** Required (Bearer token)\n \n **Request Body:**\n - `name` (string, required): Collection name (max 100 chars)\n - `description` (string, optional): Collection description (max 500 chars)\n - `color` (string, optional): Hex color code (default #3498db)\n - `icon` (string, optional): Emoji icon (default \U0001F4DA)\n - `auto_assign_rules` (array, optional): Auto-assignment rules\n - `id` (string): Rule ID\n - `field` (string): Field to match (genre, author, series, etc.)\n - `operator` (string): Comparison operator (equals, contains, startsWith)\n - `value` (string): Value to match\n - `priority` (integer): Rule priority (1-10)\n \n **Response:**\n - Collection object with generated ID\n \n **Status Codes:**\n - 201: Created\n - 400: Invalid request\n - 401: Unauthorized\n - 409: Collection name\
,\n \"auto_assign_rules\": [\n {\n \"id\": \"rule-1\",\n \ \ already exists"
\ \"field\": \"genre\",\n \"operator\": \"equals\",\n \"value\"\
: \"Science Fiction\",\n \"priority\": 8"
docs: |-
## Create Collection
Creates a new collection for organizing books.
**Method:** POST
**Endpoint:** /api/collections
**Authentication:** Required (Bearer token)
**Request Body:**
- `name` (string, required): Collection name (max 100 chars)
- `description` (string, optional): Collection description (max 500 chars)
- `color` (string, optional): Hex color code (default #3498db)
- `icon` (string, optional): Emoji icon (default 📚)
- `auto_assign_rules` (array, optional): Auto-assignment rules
- `id` (string): Rule ID
- `field` (string): Field to match (genre, author, series, etc.)
- `operator` (string): Comparison operator (equals, contains, startsWith)
- `value` (string): Value to match
- `priority` (integer): Rule priority (1-10)
**Response:**
- Collection object with generated ID
**Status Codes:**
- 201: Created
- 400: Invalid request
- 401: Unauthorized
- 409: Collection name already exists
+2 -29
View File
@@ -8,32 +8,5 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"collection_id\": \"{{collection_id}}" data: "{\n \"collection_id\": \"{{collection_id}}"
docs: "## Create Device Mapping\n\n Maps a collection to a device shelf for synchronization.\n \n **Method:** POST\n \n **Endpoint:** /api/devices/{device_id}/collections\n \n **Authentication:** Required (Bearer token)\n \n **Path Parameters:**\n - `device_id` (string): Device ID\n \n **Request Body:**\n - `collection_id` (string, required): Collection ID to map\n - `device_shelf_name` (string, optional): Custom shelf name on device\n - `sync_direction` (string, optional): sync direction (book_to_device, device_to_book, bidirectional)\n \n **Response:**\n - Mapping object with generated ID\n \n **Status Codes:**\n - 201: Created\n - 400: Invalid request\n - 401: Unauthorized\n - 404: Device or collection not found"
docs: |-
## Create Device Mapping
Maps a collection to a device shelf for synchronization.
**Method:** POST
**Endpoint:** /api/devices/{device_id}/collections
**Authentication:** Required (Bearer token)
**Path Parameters:**
- `device_id` (string): Device ID
**Request Body:**
- `collection_id` (string, required): Collection ID to map
- `device_shelf_name` (string, optional): Custom shelf name on device
- `sync_direction` (string, optional): sync direction (book_to_device, device_to_book, bidirectional)
**Response:**
- Mapping object with generated ID
**Status Codes:**
- 201: Created
- 400: Invalid request
- 401: Unauthorized
- 404: Device or collection not found
+2 -36
View File
@@ -8,39 +8,5 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"name\": \"Sci-Fi Favorites\",\n \"description\": \"Updated\ data: "{\n \"name\": \"Sci-Fi Favorites\",\n \"description\": \"Updated description\",\n \"color\": \"#00ff00\",\n \"icon\": \"⭐\",\n \"auto_assign_rules\": [\n {\n \"id\": \"rule-2\",\n \"field\": \"series\",\n \"operator\": \"equals\",\n \"value\": \"Foundation\",\n \"priority\": 9"
\ description\",\n \"color\": \"#00ff00\",\n \"icon\": \"⭐\",\n \"\ docs: "## Update Collection\n\n Updates an existing collection's properties.\n \n **Method:** PUT\n \n **Endpoint:** /api/collections/{collection_id}\n \n **Authentication:** Required (Bearer token)\n \n **Path Parameters:**\n - `collection_id` (string): Collection ID\n \n **Request Body:**\n - `name` (string, optional): New collection name\n - `description` (string, optional): New description\n - `color` (string, optional): New hex color code\n - `icon` (string, optional): New emoji icon\n - `auto_assign_rules` (array, optional): Updated auto-assignment rules\n \n **Response:**\n - Updated collection object\n \n **Status Codes:**\n - 200: Success\n - 400: Invalid request\n - 401: Unauthorized\n - 404: Collection not found\n - 409: Collection name already exists"
auto_assign_rules\": [\n {\n \"id\": \"rule-2\",\n \"field\"\
: \"series\",\n \"operator\": \"equals\",\n \"value\": \"Foundation\"\
,\n \"priority\": 9"
docs: |-
## Update Collection
Updates an existing collection's properties.
**Method:** PUT
**Endpoint:** /api/collections/{collection_id}
**Authentication:** Required (Bearer token)
**Path Parameters:**
- `collection_id` (string): Collection ID
**Request Body:**
- `name` (string, optional): New collection name
- `description` (string, optional): New description
- `color` (string, optional): New hex color code
- `icon` (string, optional): New emoji icon
- `auto_assign_rules` (array, optional): Updated auto-assignment rules
**Response:**
- Updated collection object
**Status Codes:**
- 200: Success
- 400: Invalid request
- 401: Unauthorized
- 404: Collection not found
- 409: Collection name already exists
+2 -30
View File
@@ -8,33 +8,5 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"device_shelf_name\": \"Science Fiction\",\n \"sync_direction\"\ data: "{\n \"device_shelf_name\": \"Science Fiction\",\n \"sync_direction\": \"book_to_device\""
: \"book_to_device\"" docs: "## Update Device Mapping\n\n Updates settings for a collection-to-device mapping.\n \n **Method:** PUT\n \n **Endpoint:** /api/devices/{device_id}/collections/{mapping_id}\n \n **Authentication:** Required (Bearer token)\n \n **Path Parameters:**\n - `device_id` (string): Device ID\n - `mapping_id` (string): Mapping ID\n \n **Request Body:**\n - `device_shelf_name` (string, optional): Custom shelf name on device\n - `sync_direction` (string, optional): Sync direction (book_to_device, device_to_book, bidirectional)\n \n **Response:**\n - Updated mapping object\n \n **Status Codes:**\n - 200: Success\n - 400: Invalid request\n - 401: Unauthorized\n - 404: Device or mapping not found"
docs: |-
## Update Device Mapping
Updates settings for a collection-to-device mapping.
**Method:** PUT
**Endpoint:** /api/devices/{device_id}/collections/{mapping_id}
**Authentication:** Required (Bearer token)
**Path Parameters:**
- `device_id` (string): Device ID
- `mapping_id` (string): Mapping ID
**Request Body:**
- `device_shelf_name` (string, optional): Custom shelf name on device
- `sync_direction` (string, optional): Sync direction (book_to_device, device_to_book, bidirectional)
**Response:**
- Updated mapping object
**Status Codes:**
- 200: Success
- 400: Invalid request
- 401: Unauthorized
- 404: Device or mapping not found
@@ -8,7 +8,7 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"library_id\": \"{{library_id}}\",\n \"name\": \"My Favorites\",\n \"icon\": \"⭐\",\n \"description\": \"My favorite books\",\n \"show_on_dashboard\": true,\n \"auto_assign_rules\": \"\",\n \"manual_book_ids\": [\"{{media_item_id}}\"],\n \"match_type\": \"all\"\n}" data: "{\n \"library_id\": \"{{library_id}}\",\n \"name\": \"My Favorites\",\n \"icon\": \"⭐\",\n \"description\": \"My favorite books\",\n \"show_on_dashboard\": true,\n \"auto_assign_rules\": \"\",\n \"manual_book_ids\": [\"{{media_item_id}}\"],\n \"match_type\": \"all\"\n}"
runtime: runtime:
scripts: scripts:
- type: after-response - type: after-response
@@ -8,7 +8,7 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"library_id\": \"{{library_id}}\",\n \"icon\": \"\U0001F4DA\"\n}" data: "{\n \"library_id\": \"{{library_id}}\",\n \"icon\": \"\U0001F4DA\"\n}"
runtime: runtime:
scripts: scripts:
- type: after-response - type: after-response
@@ -8,7 +8,7 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"library_id\": \"{{library_id}}\",\n \"name\": \"Science Fiction Books\",\n \"icon\": \"\U0001F680\",\n \"description\": \"All my sci-fi books\",\n \"show_on_dashboard\": true,\n \"auto_assign_rules\": \"{\\\"rules\\\":[{\\\"id\\\":\\\"rule1\\\",\\\"field\\\":\\\"genre\\\",\\\"operator\\\":\\\"equals\\\",\\\"value\\\":\\\"Sci-Fi\\\",\\\"priority\\\":1}]}\",\n \"manual_book_ids\": [],\n \"match_type\": \"all\"\n}" data: "{\n \"library_id\": \"{{library_id}}\",\n \"name\": \"Science Fiction Books\",\n \"icon\": \"\U0001F680\",\n \"description\": \"All my sci-fi books\",\n \"show_on_dashboard\": true,\n \"auto_assign_rules\": \"{\\\"rules\\\":[{\\\"id\\\":\\\"rule1\\\",\\\"field\\\":\\\"genre\\\",\\\"operator\\\":\\\"equals\\\",\\\"value\\\":\\\"Sci-Fi\\\",\\\"priority\\\":1}]}\",\n \"manual_book_ids\": [],\n \"match_type\": \"all\"\n}"
runtime: runtime:
scripts: scripts:
- type: after-response - type: after-response
+1 -1
View File
@@ -8,7 +8,7 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"library_id\": \"{{library_id}}\",\n \"rules\": [\n{\n \"id\": \"rule1\",\n \"field\": \"genre\",\n\"operator\": \"equals\",\n \"value\": \"Fiction\",\n\"priority\": 1\n }\n ],\n \"manual_book_ids\": [],\n \"limit\": 20\n}" data: "{\n \"library_id\": \"{{library_id}}\",\n \"rules\": [\n{\n \"id\": \"rule1\",\n \"field\": \"genre\",\n\"operator\": \"equals\",\n \"value\": \"Fiction\",\n\"priority\": 1\n }\n ],\n \"manual_book_ids\": [],\n \"limit\": 20\n}"
runtime: runtime:
scripts: scripts:
- type: after-response - type: after-response
@@ -8,46 +8,11 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"operations\": [\n {\n \"collection_id\": \"{{collectionId1" data: "{\n \"operations\": [\n {\n \"collection_id\": \"{{collectionId1"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
- key: Authorization - key: Authorization
value: Bearer {{authToken value: Bearer {{authToken
docs: "## Bulk Add Books to Collections\n\n Adds multiple books to multiple collections in a single request. Each operation specifies a collection and a list of books to add.\n\n **Method:** POST\n\n **Endpoint:** /api/collections/bulk-add-books\n\n **Authentication:** Bearer token\n\n **Request Body:**\n - `operations` (array): Array of collection-book operations\n - `collection_id` (string): Collection UUID\n - `book_ids` (array): Array of book UUIDs to add to the collection\n\n **Response:**\n - `results` (array): Results for each operation\n - `total` (number): Total number of operations\n - `success` (number): Number of successful operations\n - `failed` (number): Number of failed operations\n\n **Status Codes:**\n - 200: Success (with partial results if some failed)\n - 400: Invalid request data\n - 401: Unauthorized\n - 403: Forbidden\n - 500: Internal server error\n\n **Example:**\n ```json\n {\n \"operations\": [\n {\n \"collection_id\":\
docs: |- \ \"collection-uuid-1\",\n \"book_ids\": [\"book-1\", \"book-2\"]"
## Bulk Add Books to Collections
Adds multiple books to multiple collections in a single request. Each operation specifies a collection and a list of books to add.
**Method:** POST
**Endpoint:** /api/collections/bulk-add-books
**Authentication:** Bearer token
**Request Body:**
- `operations` (array): Array of collection-book operations
- `collection_id` (string): Collection UUID
- `book_ids` (array): Array of book UUIDs to add to the collection
**Response:**
- `results` (array): Results for each operation
- `total` (number): Total number of operations
- `success` (number): Number of successful operations
- `failed` (number): Number of failed operations
**Status Codes:**
- 200: Success (with partial results if some failed)
- 400: Invalid request data
- 401: Unauthorized
- 403: Forbidden
- 500: Internal server error
**Example:**
```json
{
"operations": [
{
"collection_id": "collection-uuid-1",
"book_ids": ["book-1", "book-2"]
@@ -8,16 +8,8 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"book_ids\": [\n \"{{bookId1" data: "{\n \"book_ids\": [\n \"{{bookId1"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
docs: "## Bulk Remove Books from Collection\n\n Removes multiple books from a collection in a single request.\n\n **Method:** POST\n\n **Endpoint:** /api/collections/{collection_id"
docs: |-
## Bulk Remove Books from Collection
Removes multiple books from a collection in a single request.
**Method:** POST
**Endpoint:** /api/collections/{collection_id
@@ -8,20 +8,10 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"book_ids\": []" data: "{\n \"book_ids\": []"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
- key: Authorization - key: Authorization
value: Bearer {{authToken value: Bearer {{authToken
docs: "## Bulk Remove Books - Empty List Validation\n\n Tests validation behavior when providing an empty book_ids array.\n\n **Expected Result:** 400 Bad Request\n\n **Validation Rule:** book_ids array must contain at least one book UUID.\n\n **Purpose:** Ensures the API properly validates input and rejects empty removal requests."
docs: |-
## Bulk Remove Books - Empty List Validation
Tests validation behavior when providing an empty book_ids array.
**Expected Result:** 400 Bad Request
**Validation Rule:** book_ids array must contain at least one book UUID.
**Purpose:** Ensures the API properly validates input and rejects empty removal requests.
@@ -8,24 +8,10 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"book_ids\": [\n \"{{bookId1" data: "{\n \"book_ids\": [\n \"{{bookId1"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
- key: Authorization - key: Authorization
value: Bearer {{authToken value: Bearer {{authToken
docs: "## Bulk Remove Books - Invalid IDs\n\n Tests behavior when the book_ids array contains invalid UUID formats or non-existent books.\n\n **Expected Result:** 200 OK with partial success\n\n **Purpose:** Verifies that:\n - Invalid UUID formats don't crash the endpoint\n - Non-existent book IDs are handled gracefully\n - Valid IDs in the same request are still processed\n - Response includes detailed results showing which succeeded/failed\n\n **Note:** The endpoint should process all valid IDs and report failures for invalid ones, allowing clients to handle partial failures appropriately."
docs: |-
## Bulk Remove Books - Invalid IDs
Tests behavior when the book_ids array contains invalid UUID formats or non-existent books.
**Expected Result:** 200 OK with partial success
**Purpose:** Verifies that:
- Invalid UUID formats don't crash the endpoint
- Non-existent book IDs are handled gracefully
- Valid IDs in the same request are still processed
- Response includes detailed results showing which succeeded/failed
**Note:** The endpoint should process all valid IDs and report failures for invalid ones, allowing clients to handle partial failures appropriately.
@@ -8,20 +8,10 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"book_ids\": [\n \"{{bookId1" data: "{\n \"book_ids\": [\n \"{{bookId1"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
- key: Authorization - key: Authorization
value: Bearer {{authToken value: Bearer {{authToken
docs: "## Bulk Remove Books - Single Book\n\n Tests that bulk remove endpoint works correctly with a single book.\n\n **Expected Result:** 200 OK with removed: 1, total: 1\n\n **Purpose:** Verifies the bulk remove endpoint handles single-item arrays correctly, providing flexibility for clients to use the same endpoint for both single and multiple removals.\n\n **Note:** Using bulk remove for a single book is functionally equivalent to the single remove endpoint but allows for consistent error handling and response format."
docs: |-
## Bulk Remove Books - Single Book
Tests that bulk remove endpoint works correctly with a single book.
**Expected Result:** 200 OK with removed: 1, total: 1
**Purpose:** Verifies the bulk remove endpoint handles single-item arrays correctly, providing flexibility for clients to use the same endpoint for both single and multiple removals.
**Note:** Using bulk remove for a single book is functionally equivalent to the single remove endpoint but allows for consistent error handling and response format.
@@ -8,23 +8,10 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"rules\": [\n {\n \"field\": \"author\",\n \ data: "{\n \"rules\": [\n {\n \"field\": \"author\",\n \"operator\": \"contains\",\n \"value\": \"Asimov\""
\ \"operator\": \"contains\",\n \"value\": \"Asimov\""
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
- key: Authorization - key: Authorization
value: Bearer {{authToken value: Bearer {{authToken
docs: "## Test Collection Rules - Author Contains\n\n Tests the \"contains\" operator on the author field to find books by a specific author (partial match).\n\n **Example Use Case:** Finding all books by an author whose name contains \"Asimov\" (e.g., \"Isaac Asimov\").\n\n **Operator:** `contains` - Matches if the field contains the specified value as a substring (case-insensitive typically).\n\n **Expected Result:** Returns all books where the author field contains \"Asimov\".\n\n **Purpose:** Demonstrates text-based partial matching for author searches, useful when you don't need the exact author name or want to find books by authors with similar names."
docs: |-
## Test Collection Rules - Author Contains
Tests the "contains" operator on the author field to find books by a specific author (partial match).
**Example Use Case:** Finding all books by an author whose name contains "Asimov" (e.g., "Isaac Asimov").
**Operator:** `contains` - Matches if the field contains the specified value as a substring (case-insensitive typically).
**Expected Result:** Returns all books where the author field contains "Asimov".
**Purpose:** Demonstrates text-based partial matching for author searches, useful when you don't need the exact author name or want to find books by authors with similar names.
@@ -8,25 +8,10 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"rules\": [\n {\n \"field\": \"copyright_year\"\ data: "{\n \"rules\": [\n {\n \"field\": \"copyright_year\",\n \"operator\": \"greater_than\",\n \"value\": \"2000\""
,\n \"operator\": \"greater_than\",\n \"value\": \"2000\""
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
- key: Authorization - key: Authorization
value: Bearer {{authToken value: Bearer {{authToken
docs: "## Test Collection Rules - Copyright Year Greater Than\n\n Tests the \"greater_than\" operator on the copyright_year field to find books published after a specific year.\n\n **Example Use Case:** Creating a \"Modern Books\" collection with books published after 2000.\n\n **Operator:** `greater_than` - Matches if the field value is greater than the specified value (numeric comparison).\n\n **Field:** `copyright_year` - The year the book was copyrighted/published.\n\n **Expected Result:** Returns all books with copyright_year greater than 2000 (i.e., published in 2001 or later).\n\n **Purpose:** Demonstrates numeric comparison operators for creating date-based collections, useful for organizing books by publication era."
docs: |-
## Test Collection Rules - Copyright Year Greater Than
Tests the "greater_than" operator on the copyright_year field to find books published after a specific year.
**Example Use Case:** Creating a "Modern Books" collection with books published after 2000.
**Operator:** `greater_than` - Matches if the field value is greater than the specified value (numeric comparison).
**Field:** `copyright_year` - The year the book was copyrighted/published.
**Expected Result:** Returns all books with copyright_year greater than 2000 (i.e., published in 2001 or later).
**Purpose:** Demonstrates numeric comparison operators for creating date-based collections, useful for organizing books by publication era.
@@ -8,22 +8,10 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"rules\": []" data: "{\n \"rules\": []"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
- key: Authorization - key: Authorization
value: Bearer {{authToken value: Bearer {{authToken
docs: "## Test Collection Rules - Empty Rules Array\n\n Tests validation behavior when providing an empty rules array.\n\n **Expected Result:** 400 Bad Request\n\n **Validation Rule:** rules array must contain at least one rule object.\n\n **Purpose:** Ensures the API properly validates input and rejects empty rule sets, preventing accidental queries that would return all books or cause performance issues.\n\n **Use Case:** Client-side validation should prevent sending empty rules, but the API should also validate to catch malformed requests."
docs: |-
## Test Collection Rules - Empty Rules Array
Tests validation behavior when providing an empty rules array.
**Expected Result:** 400 Bad Request
**Validation Rule:** rules array must contain at least one rule object.
**Purpose:** Ensures the API properly validates input and rejects empty rule sets, preventing accidental queries that would return all books or cause performance issues.
**Use Case:** Client-side validation should prevent sending empty rules, but the API should also validate to catch malformed requests.
@@ -8,27 +8,10 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"rules\": [\n {\n \"field\": \"genre\",\n \ data: "{\n \"rules\": [\n {\n \"field\": \"genre\",\n \"operator\": \"equals\",\n \"value\": \"NonExistentGenre123456\""
\ \"operator\": \"equals\",\n \"value\": \"NonExistentGenre123456\""
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
- key: Authorization - key: Authorization
value: Bearer {{authToken value: Bearer {{authToken
docs: "## Test Collection Rules - No Matches\n\n Tests behavior when collection rules don't match any books in the library.\n\n **Example Use Case:** Validating that a new genre name doesn't exist before creating a collection for it, or testing edge cases.\n\n **Expected Result:** 200 OK with empty matches array and total: 0\n\n **Purpose:** Verifies that the API handles zero-match scenarios gracefully:\n - Returns 200 (success) not 404\n - Returns empty array, not null\n - Returns total: 0 for clarity\n - No errors thrown for no results\n\n **Note:** An empty result set is a valid response and doesn't indicate an error. This allows users to test rules confidently before creating collections."
docs: |-
## Test Collection Rules - No Matches
Tests behavior when collection rules don't match any books in the library.
**Example Use Case:** Validating that a new genre name doesn't exist before creating a collection for it, or testing edge cases.
**Expected Result:** 200 OK with empty matches array and total: 0
**Purpose:** Verifies that the API handles zero-match scenarios gracefully:
- Returns 200 (success) not 404
- Returns empty array, not null
- Returns total: 0 for clarity
- No errors thrown for no results
**Note:** An empty result set is a valid response and doesn't indicate an error. This allows users to test rules confidently before creating collections.
@@ -8,60 +8,11 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"rules\": [\n {\n \"field\": \"genre\",\n \ data: "{\n \"rules\": [\n {\n \"field\": \"genre\",\n \"operator\": \"equals\",\n \"value\": \"Science Fiction\""
\ \"operator\": \"equals\",\n \"value\": \"Science Fiction\""
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
- key: Authorization - key: Authorization
value: Bearer {{authToken value: Bearer {{authToken
docs: "## Test Collection Rules\n\n Tests collection rules against the library to see which books match, without creating a collection. Useful for previewing what books would be included in a collection with specific rules.\n\n **Method:** POST\n\n **Endpoint:** /api/collections/test-rules\n\n **Authentication:** Bearer token\n\n **Request Body:**\n - `rules` (array): Array of rule objects to test\n - `field` (string): Field to test (genre, author, copyright_year, tags, etc.)\n - `operator` (string): Comparison operator\n - `equals`: Exact match\n - `contains`: Contains substring (for text fields)\n - `greater_than`: Greater than (for numeric fields)\n - `less_than`: Less than (for numeric fields)\n - `not_equals`: Not equal to\n - `starts_with`: Starts with\n - `ends_with`: Ends with\n - `is_empty`: Field is empty or null\n - `is_not_empty`: Field is not empty and not null\n - `value` (string): Value to compare against (not required\
docs: |- \ for is_empty/is_not_empty)\n\n **Response:**\n - `matches` (array): Array of matching books\n - `id` (string): Book UUID\n - `title` (string): Book title\n - `author` (string): Book author\n - `genre` (string): Book genre\n - Additional book metadata\n - `total` (number): Total number of matching books\n\n **Status Codes:**\n - 200: Success - returns matching books\n - 400: Invalid request (empty rules array, invalid field/operator)\n - 401: Unauthorized\n - 500: Internal server error\n\n **Example Request:**\n ```json\n {\n \"rules\": [\n {\n \"field\": \"genre\",\n \"operator\": \"equals\",\n \"value\": \"Science Fiction\""
## Test Collection Rules
Tests collection rules against the library to see which books match, without creating a collection. Useful for previewing what books would be included in a collection with specific rules.
**Method:** POST
**Endpoint:** /api/collections/test-rules
**Authentication:** Bearer token
**Request Body:**
- `rules` (array): Array of rule objects to test
- `field` (string): Field to test (genre, author, copyright_year, tags, etc.)
- `operator` (string): Comparison operator
- `equals`: Exact match
- `contains`: Contains substring (for text fields)
- `greater_than`: Greater than (for numeric fields)
- `less_than`: Less than (for numeric fields)
- `not_equals`: Not equal to
- `starts_with`: Starts with
- `ends_with`: Ends with
- `is_empty`: Field is empty or null
- `is_not_empty`: Field is not empty and not null
- `value` (string): Value to compare against (not required for is_empty/is_not_empty)
**Response:**
- `matches` (array): Array of matching books
- `id` (string): Book UUID
- `title` (string): Book title
- `author` (string): Book author
- `genre` (string): Book genre
- Additional book metadata
- `total` (number): Total number of matching books
**Status Codes:**
- 200: Success - returns matching books
- 400: Invalid request (empty rules array, invalid field/operator)
- 401: Unauthorized
- 500: Internal server error
**Example Request:**
```json
{
"rules": [
{
"field": "genre",
"operator": "equals",
"value": "Science Fiction"
+2 -12
View File
@@ -8,18 +8,8 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"winner\": \"koreader\",\n \"manual_data\": null,\n \"\ data: "{\n \"winner\": \"koreader\",\n \"manual_data\": null,\n \"apply_to_all_future_conflicts\": false,\n \"reason\": \"User chose more recent progress\""
apply_to_all_future_conflicts\": false,\n \"reason\": \"User chose more recent\
\ progress\""
headers: headers:
- key: Authorization - key: Authorization
value: Bearer {{token value: Bearer {{token
docs: "## Resolve Conflict\n\n Resolves a sync conflict by choosing which source to use for the conflicting data.\n\n **Method:** POST\n\n **Endpoint:** /api/conflicts/{conflict_id"
docs: |-
## Resolve Conflict
Resolves a sync conflict by choosing which source to use for the conflicting data.
**Method:** POST
**Endpoint:** /api/conflicts/{conflict_id
@@ -8,42 +8,10 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"conflict_ids\": [\n \"{{conflictId1" data: "{\n \"conflict_ids\": [\n \"{{conflictId1"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
- key: Authorization - key: Authorization
value: Bearer {{authToken value: Bearer {{authToken
docs: "## Bulk Dismiss Conflicts\n\n Dismisses multiple sync conflicts without resolving them. This removes them from the conflict list while leaving the data unchanged.\n\n **Method:** POST\n\n **Endpoint:** /api/conflicts/bulk-dismiss\n\n **Authentication:** Bearer token\n\n **Request Body:**\n - `conflict_ids` (array): Array of conflict UUIDs to dismiss\n\n **Response:**\n - `results` (array): Results for each dismissal\n - `total` (number): Total number of conflicts processed\n - `success` (number): Number of successfully dismissed conflicts\n - `failed` (number): Number of failed dismissals\n\n **Status Codes:**\n - 200: Success\n - 400: Invalid request data\n - 401: Unauthorized\n - 403: Forbidden\n - 404: One or more conflicts not found\n - 500: Internal server error\n\n **Example:**\n ```json\n {\n \"conflict_ids\": [\"uuid-1\", \"uuid-2\"]"
docs: |-
## Bulk Dismiss Conflicts
Dismisses multiple sync conflicts without resolving them. This removes them from the conflict list while leaving the data unchanged.
**Method:** POST
**Endpoint:** /api/conflicts/bulk-dismiss
**Authentication:** Bearer token
**Request Body:**
- `conflict_ids` (array): Array of conflict UUIDs to dismiss
**Response:**
- `results` (array): Results for each dismissal
- `total` (number): Total number of conflicts processed
- `success` (number): Number of successfully dismissed conflicts
- `failed` (number): Number of failed dismissals
**Status Codes:**
- 200: Success
- 400: Invalid request data
- 401: Unauthorized
- 403: Forbidden
- 404: One or more conflicts not found
- 500: Internal server error
**Example:**
```json
{
"conflict_ids": ["uuid-1", "uuid-2"]
@@ -8,48 +8,11 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"conflict_ids\": [\n \"{{conflictId1" data: "{\n \"conflict_ids\": [\n \"{{conflictId1"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
- key: Authorization - key: Authorization
value: Bearer {{authToken value: Bearer {{authToken
docs: "## Bulk Resolve Conflicts\n\n Resolves multiple sync conflicts in a single request using a specified resolution strategy.\n\n **Method:** POST\n\n **Endpoint:** /api/conflicts/bulk-resolve\n\n **Authentication:** Bearer token\n\n **Request Body:**\n - `conflict_ids` (array): Array of conflict UUIDs to resolve\n - `strategy` (string): Resolution strategy\n - `most_recent`: Use the most recently updated progress\n - `highest_progress`: Use the reading progress with the highest percent read\n - `server`: Always prefer server-side data\n - `device`: Always prefer device-side data\n\n **Response:**\n - `results` (array): Results for each conflict resolution\n - `total` (number): Total number of conflicts processed\n - `success` (number): Number of successfully resolved conflicts\n - `failed` (number): Number of failed resolutions\n\n **Status Codes:**\n - 200: Success (with partial results if some failed)\n - 400: Invalid request data\n - 401: Unauthorized\n\
docs: |- \ - 403: Forbidden\n - 404: One or more conflicts not found\n - 500: Internal server error\n\n **Example:**\n ```json\n {\n \"conflict_ids\": [\"uuid-1\", \"uuid-2\", \"uuid-3\"],\n \"strategy\": \"most_recent\""
## Bulk Resolve Conflicts
Resolves multiple sync conflicts in a single request using a specified resolution strategy.
**Method:** POST
**Endpoint:** /api/conflicts/bulk-resolve
**Authentication:** Bearer token
**Request Body:**
- `conflict_ids` (array): Array of conflict UUIDs to resolve
- `strategy` (string): Resolution strategy
- `most_recent`: Use the most recently updated progress
- `highest_progress`: Use the reading progress with the highest percent read
- `server`: Always prefer server-side data
- `device`: Always prefer device-side data
**Response:**
- `results` (array): Results for each conflict resolution
- `total` (number): Total number of conflicts processed
- `success` (number): Number of successfully resolved conflicts
- `failed` (number): Number of failed resolutions
**Status Codes:**
- 200: Success (with partial results if some failed)
- 400: Invalid request data
- 401: Unauthorized
- 403: Forbidden
- 404: One or more conflicts not found
- 500: Internal server error
**Example:**
```json
{
"conflict_ids": ["uuid-1", "uuid-2", "uuid-3"],
"strategy": "most_recent"
@@ -8,39 +8,11 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"conflict_ids\": [\n \"{{conflictId1" data: "{\n \"conflict_ids\": [\n \"{{conflictId1"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
- key: Authorization - key: Authorization
value: Bearer {{authToken value: Bearer {{authToken
docs: "## Bulk Resolve with Highest Progress Strategy\n\n Resolves multiple sync conflicts using the \"highest_progress\" strategy, which keeps the reading progress with the highest percentage read.\n\n **Method:** POST\n\n **Endpoint:** /api/conflicts/bulk-resolve\n\n **Authentication:** Bearer token\n\n **Request Body:**\n - `conflict_ids` (array): Array of conflict UUIDs to resolve\n - `strategy` (string): Must be \"highest_progress\"\n\n **Response:**\n - `results` (array): Results for each conflict resolution\n - `total` (number): Total number of conflicts processed\n - `success` (number): Number of successfully resolved conflicts\n - `failed` (number): Number of failed resolutions\n\n **Status Codes:**\n - 200: Success\n - 400: Invalid request data\n - 401: Unauthorized\n - 403: Forbidden\n - 500: Internal server error\n\n **Note:** The highest progress strategy is ideal when you want to preserve the most reading progress across devices. Use this when you've been\
docs: |- \ reading on multiple devices and want to keep the furthest position."
## Bulk Resolve with Highest Progress Strategy
Resolves multiple sync conflicts using the "highest_progress" strategy, which keeps the reading progress with the highest percentage read.
**Method:** POST
**Endpoint:** /api/conflicts/bulk-resolve
**Authentication:** Bearer token
**Request Body:**
- `conflict_ids` (array): Array of conflict UUIDs to resolve
- `strategy` (string): Must be "highest_progress"
**Response:**
- `results` (array): Results for each conflict resolution
- `total` (number): Total number of conflicts processed
- `success` (number): Number of successfully resolved conflicts
- `failed` (number): Number of failed resolutions
**Status Codes:**
- 200: Success
- 400: Invalid request data
- 401: Unauthorized
- 403: Forbidden
- 500: Internal server error
**Note:** The highest progress strategy is ideal when you want to preserve the most reading progress across devices. Use this when you've been reading on multiple devices and want to keep the furthest position.
+3 -34
View File
@@ -8,40 +8,9 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"collection_name\": \"continue-reading\"\n}" data: "{\n \"collection_name\": \"continue-reading\"\n}"
runtime: runtime:
scripts: scripts:
- type: tests - type: tests
code: "test(\"status must be 200 with valid collection\", function() {\n expect(res.status).to.eql(200);\n\ code: "test(\"status must be 200 with valid collection\", function() {\n expect(res.status).to.eql(200);\n});\n\ntest(\"response has success message\", function() {\n expect(res.body.message).to.exist;\n});"
});\n\ntest(\"response has success message\", function() {\n expect(res.body.message).to.exist;\n\ docs: "Restore a system collection to its default state.\n\n**Endpoint**: POST /api/dashboard/restore-system-collection\n**Auth**: Required (Bearer token)\n\n## Request Body\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| collection_name | string | Yes | Name of system collection to restore |\n\nValid collection names:\n- `continue-reading`: Books you're currently reading (0 < progress < 1)\n- `recently-added`: Newly added items to this library\n- `recently-read`: Books you've finished (progress >= 1)\n- `not-started`: Books you haven't read yet (progress = 0 or no record)\n\n## Example Request\n\n```json\n{\n \"collection_name\": \"continue-reading\"\n}\n```\n\n## Response\n\nReturns success message on restore."
});"
docs: |-
Restore a system collection to its default state.
**Endpoint**: POST /api/dashboard/restore-system-collection
**Auth**: Required (Bearer token)
## Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| collection_name | string | Yes | Name of system collection to restore |
Valid collection names:
- `continue-reading`: Books you're currently reading (0 < progress < 1)
- `recently-added`: Newly added items to this library
- `recently-read`: Books you've finished (progress >= 1)
- `not-started`: Books you haven't read yet (progress = 0 or no record)
## Example Request
```json
{
"collection_name": "continue-reading"
}
```
## Response
Returns success message on restore.
+3 -35
View File
@@ -8,41 +8,9 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"library_id\": \"{{library_id}}\",\n \"hidden_collections\"\ data: "{\n \"library_id\": \"{{library_id}}\",\n \"hidden_collections\": [\"not-started\"],\n \"collection_order\": [\"recently-added\", \"continue-reading\", \"recently-read\"],\n \"items_per_section\": 20\n}"
: [\"not-started\"],\n \"collection_order\": [\"recently-added\", \"continue-reading\"\
, \"recently-read\"],\n \"items_per_section\": 20\n}"
runtime: runtime:
scripts: scripts:
- type: tests - type: tests
code: "test(\"status must be 200 with valid request\", function() {\n expect(res.status).to.eql(200);\n\ code: "test(\"status must be 200 with valid request\", function() {\n expect(res.status).to.eql(200);\n});"
});" docs: "Update dashboard preferences for the authenticated user.\n\n**Endpoint**: PUT /api/dashboard/preferences\n**Auth**: Required (Bearer token)\n\n## Request Body\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| library_id | string | Yes | Library UUID |\n| hidden_collections | array | No | Collection names to hide from dashboard |\n| collection_order | array | No | Collection names in custom display order |\n| items_per_section | int | No | Items to show per section (default: 20) |\n\n## Example Request\n\n```json\n{\n \"library_id\": \"cc23c3a7-f8fb-451a-a78d-2a16df1b725a\",\n \"hidden_collections\": [\"not-started\"],\n \"collection_order\": [\"recently-added\", \"continue-reading\", \"recently-read\"],\n \"items_per_section\": 20\n}\n```\n\n## Response\n\nReturns updated preferences object on success."
docs: |-
Update dashboard preferences for the authenticated user.
**Endpoint**: PUT /api/dashboard/preferences
**Auth**: Required (Bearer token)
## Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| library_id | string | Yes | Library UUID |
| hidden_collections | array | No | Collection names to hide from dashboard |
| collection_order | array | No | Collection names in custom display order |
| items_per_section | int | No | Items to show per section (default: 20) |
## Example Request
```json
{
"library_id": "cc23c3a7-f8fb-451a-a78d-2a16df1b725a",
"hidden_collections": ["not-started"],
"collection_order": ["recently-added", "continue-reading", "recently-read"],
"items_per_section": 20
}
```
## Response
Returns updated preferences object on success.
+6 -85
View File
@@ -2,95 +2,16 @@ info:
name: Get Analytics Tests name: Get Analytics Tests
type: http type: http
seq: 7 seq: 7
http: http:
method: POST method: POST
url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/v1/analytics/gettests' url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/v1/analytics/gettests'
auth: inherit auth: inherit
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
body: body:
type: json type: json
json: data: "{\n \"platform\": \"android\",\n \"firmware_version\": \"4.30.19023\"\n}"
platform: android docs: "## Get Kobo Analytics Tests\n\nRetrieves A/B testing configuration and feature flags for Kobo device.\n\n**Method:** POST\n\n**Endpoint:** /api/sync/kobo/v1/analytics/gettests\n\n**Authentication:** Bearer token\n\n**Request Body:**\n- `platform` (string): Device platform\n - `android`: Kobo Android app\n - `kobo`: Native Kobo firmware\n- `firmware_version` (string): Firmware version (e.g., \"4.30.19023\")\n- `device_model` (string, optional): Device model identifier\n- `locale` (string, optional): Device locale (e.g., \"en_US\")\n\n**Response:**\n- `tests` (array): Active A/B tests\n - `test_name` (string): Test identifier\n - `variant` (string): Assigned variant (A, B, C, etc.)\n - `enabled` (boolean): Whether test is active\n - `parameters` (object): Test-specific parameters\n- `features` (object): Feature flags\n - `feature_name` (boolean/string): Feature state\n- `configuration` (object): Device configuration\n - `sync_interval_minutes` (integer): Recommended sync frequency\n\
firmware_version: 4.30.19023 \ - `batch_size` (integer): Max items per batch sync\n - `timeout_seconds` (integer): Request timeout\n- `version` (string): Configuration version\n\n**Status Codes:**\n- 200: Success\n- 401: Unauthorized\n- 400: Invalid request parameters\n\n**Analytics Testing Purpose:**\n- Kobo uses A/B testing for UX features\n- Feature flags for gradual rollout\n- Performance monitoring configuration\n- Sync behavior optimization\n- Device-specific tuning\n\n**Common Tests:**\n- Sync frequency optimization\n- UI/UX variations\n- Network usage patterns\n- Battery life improvements\n- Feature set variations by model\n\n**Feature Flags:**\n- New sync features\n- Beta functionality\n- Platform-specific capabilities\n- Experimental features\n\n**Configuration Parameters:**\n- Optimal sync intervals for device\n- Batch size limits based on device capabilities\n- Timeout values for network conditions\n- Retry logic configuration\n- Cache policy settings\n\n**Usage:**\n- Called during device initialization\n\
- Refreshed daily or weekly\n- Cached on device\n- Affects sync behavior\n- Can be overridden by server\n\n**Use Cases:**\n- Device initialization\n- Feature rollout testing\n- Performance optimization\n- UX experiment participation\n- Configuration management"
docs: |-
## Get Kobo Analytics Tests
Retrieves A/B testing configuration and feature flags for Kobo device.
**Method:** POST
**Endpoint:** /api/sync/kobo/v1/analytics/gettests
**Authentication:** Bearer token
**Request Body:**
- `platform` (string): Device platform
- `android`: Kobo Android app
- `kobo`: Native Kobo firmware
- `firmware_version` (string): Firmware version (e.g., "4.30.19023")
- `device_model` (string, optional): Device model identifier
- `locale` (string, optional): Device locale (e.g., "en_US")
**Response:**
- `tests` (array): Active A/B tests
- `test_name` (string): Test identifier
- `variant` (string): Assigned variant (A, B, C, etc.)
- `enabled` (boolean): Whether test is active
- `parameters` (object): Test-specific parameters
- `features` (object): Feature flags
- `feature_name` (boolean/string): Feature state
- `configuration` (object): Device configuration
- `sync_interval_minutes` (integer): Recommended sync frequency
- `batch_size` (integer): Max items per batch sync
- `timeout_seconds` (integer): Request timeout
- `version` (string): Configuration version
**Status Codes:**
- 200: Success
- 401: Unauthorized
- 400: Invalid request parameters
**Analytics Testing Purpose:**
- Kobo uses A/B testing for UX features
- Feature flags for gradual rollout
- Performance monitoring configuration
- Sync behavior optimization
- Device-specific tuning
**Common Tests:**
- Sync frequency optimization
- UI/UX variations
- Network usage patterns
- Battery life improvements
- Feature set variations by model
**Feature Flags:**
- New sync features
- Beta functionality
- Platform-specific capabilities
- Experimental features
**Configuration Parameters:**
- Optimal sync intervals for device
- Batch size limits based on device capabilities
- Timeout values for network conditions
- Retry logic configuration
- Cache policy settings
**Usage:**
- Called during device initialization
- Refreshed daily or weekly
- Cached on device
- Affects sync behavior
- Can be overridden by server
**Use Cases:**
- Device initialization
- Feature rollout testing
- Performance optimization
- UX experiment participation
- Configuration management
@@ -8,38 +8,10 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"confidence_threshold\": 0.8,\n \"limit\": 50" data: "{\n \"confidence_threshold\": 0.8,\n \"limit\": 50"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
- key: Authorization - key: Authorization
value: Bearer {{authToken value: Bearer {{authToken
docs: "## Auto-Link Unlinked Books\n\n Automatically links unlinked books to media items based on title and author matching with a configurable confidence threshold.\n\n **Method:** POST\n\n **Endpoint:** /sync/auto-link-books\n\n **Authentication:** Bearer token\n\n **Request Body:**\n - `confidence_threshold` (number, optional): Minimum confidence score for auto-linking (0-1, default: 0.8)\n - `limit` (number, optional): Maximum number of books to auto-link (default: 50)\n\n **Response:**\n - `results` (array): Results for each auto-link attempt\n - `total` (number): Total number of books processed\n - `success` (number): Number of successful links\n - `failed` (number): Number of failed links\n\n **Status Codes:**\n - 200: Success\n - 400: Invalid request data\n - 401: Unauthorized\n - 500: Internal server error\n\n **Note:** Higher confidence thresholds produce fewer but more accurate matches. Consider the tradeoff between automation and accuracy."
docs: |-
## Auto-Link Unlinked Books
Automatically links unlinked books to media items based on title and author matching with a configurable confidence threshold.
**Method:** POST
**Endpoint:** /sync/auto-link-books
**Authentication:** Bearer token
**Request Body:**
- `confidence_threshold` (number, optional): Minimum confidence score for auto-linking (0-1, default: 0.8)
- `limit` (number, optional): Maximum number of books to auto-link (default: 50)
**Response:**
- `results` (array): Results for each auto-link attempt
- `total` (number): Total number of books processed
- `success` (number): Number of successful links
- `failed` (number): Number of failed links
**Status Codes:**
- 200: Success
- 400: Invalid request data
- 401: Unauthorized
- 500: Internal server error
**Note:** Higher confidence thresholds produce fewer but more accurate matches. Consider the tradeoff between automation and accuracy.
@@ -8,40 +8,10 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"links\": [\n {\n \"unlinked_book_id\": \"{{unlinkedBookId1" data: "{\n \"links\": [\n {\n \"unlinked_book_id\": \"{{unlinkedBookId1"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
- key: Authorization - key: Authorization
value: Bearer {{authToken value: Bearer {{authToken
docs: "## Bulk Link Unlinked Books\n\n Links multiple unlinked books to media items in a single request.\n\n **Method:** POST\n\n **Endpoint:** /sync/bulk-link-books\n\n **Authentication:** Bearer token\n\n **Request Body:**\n - `links` (array): Array of link objects\n - `unlinked_book_id` (string): Unlinked book UUID\n - `media_item_id` (string): Media item UUID to link to\n - `confidence_score` (number): Match confidence (0-1)\n\n **Response:**\n - `results` (array): Results for each link attempt\n - `total` (number): Total number of links processed\n - `success` (number): Number of successful links\n - `failed` (number): Number of failed links\n\n **Status Codes:**\n - 200: Success\n - 400: Invalid request data\n - 401: Unauthorized\n - 500: Internal server error\n\n **Note:** Use this endpoint after reviewing suggestions from the Get Unlinked Book Suggestions endpoint."
docs: |-
## Bulk Link Unlinked Books
Links multiple unlinked books to media items in a single request.
**Method:** POST
**Endpoint:** /sync/bulk-link-books
**Authentication:** Bearer token
**Request Body:**
- `links` (array): Array of link objects
- `unlinked_book_id` (string): Unlinked book UUID
- `media_item_id` (string): Media item UUID to link to
- `confidence_score` (number): Match confidence (0-1)
**Response:**
- `results` (array): Results for each link attempt
- `total` (number): Total number of links processed
- `success` (number): Number of successful links
- `failed` (number): Number of failed links
**Status Codes:**
- 200: Success
- 400: Invalid request data
- 401: Unauthorized
- 500: Internal server error
**Note:** Use this endpoint after reviewing suggestions from the Get Unlinked Book Suggestions endpoint.
@@ -8,46 +8,9 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "[\n {\n \"ContentId\": \"{{bookUUID" data: "[\n {\n \"ContentId\": \"{{bookUUID"
headers: headers:
- key: Authorization - key: Authorization
value: Bearer {{koboToken value: Bearer {{koboToken
docs: "## Sync from Bookhoard to Kobo\n\n Server-initiated sync pushing progress, bookmarks, and highlights from Bookhoard to Kobo device. Two-way sync endpoint.\n\n **Method:** POST\n\n **Endpoint:** /api/sync/kobo/sync-from-server\n\n **Authentication:** Bearer token with Kobo device identification\n\n **Headers:**\n - `x-kobo-device` (string): JSON string containing Kobo device info\n - `DeviceId`: Kobo device ID\n - `Model`: Kobo device model\n - `SerialNumber`: Kobo device serial number\n\n **Request Body:** Array of sync data objects\n - `ContentId` (string): Book UUID\n - `PercentRead` (number): Reading progress percentage (0-100)\n - `LastModified` (string): ISO 8601 timestamp\n - `Bookmarks` (array, optional): Array of bookmark objects\n - `BookmarkId`: Unique bookmark ID\n - `ContentId`: Book UUID\n - `BookmarkText`: Bookmark text/note\n - `BookmarkType`: Type (bookmark, annotation, etc.)\n - `BookmarkTitle`: Bookmark title\n - `Highlights` (array,\
docs: |- \ optional): Array of highlight objects (same structure as bookmarks)\n\n **Response:**\n - Sync result confirmation\n\n **Status Codes:**\n - 200: Success\n - 401: Unauthorized\n - 500: Internal server error\n\n **Note:** Allows Bookhoard server to push updates to Kobo device, including reading progress, bookmarks, and highlights."
## Sync from Bookhoard to Kobo
Server-initiated sync pushing progress, bookmarks, and highlights from Bookhoard to Kobo device. Two-way sync endpoint.
**Method:** POST
**Endpoint:** /api/sync/kobo/sync-from-server
**Authentication:** Bearer token with Kobo device identification
**Headers:**
- `x-kobo-device` (string): JSON string containing Kobo device info
- `DeviceId`: Kobo device ID
- `Model`: Kobo device model
- `SerialNumber`: Kobo device serial number
**Request Body:** Array of sync data objects
- `ContentId` (string): Book UUID
- `PercentRead` (number): Reading progress percentage (0-100)
- `LastModified` (string): ISO 8601 timestamp
- `Bookmarks` (array, optional): Array of bookmark objects
- `BookmarkId`: Unique bookmark ID
- `ContentId`: Book UUID
- `BookmarkText`: Bookmark text/note
- `BookmarkType`: Type (bookmark, annotation, etc.)
- `BookmarkTitle`: Bookmark title
- `Highlights` (array, optional): Array of highlight objects (same structure as bookmarks)
**Response:**
- Sync result confirmation
**Status Codes:**
- 200: Success
- 401: Unauthorized
- 500: Internal server error
**Note:** Allows Bookhoard server to push updates to Kobo device, including reading progress, bookmarks, and highlights.
@@ -2,105 +2,16 @@ info:
name: Sync Books from Server to Kobo name: Sync Books from Server to Kobo
type: http type: http
seq: 9 seq: 9
http: http:
method: POST method: POST
url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/sync-from-server' url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/sync-from-server'
auth: inherit auth: inherit
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
body: body:
type: json type: json
json: data: "{\n \"force_sync\": true,\n \"books\": [\n \"book-uuid-1\",\n \"book-uuid-2\"\n ]\n}"
force_sync: true docs: "## Sync Books from Server to Kobo\n\nPulls reading progress, annotations, and bookmarks from server to Kobo device.\n\n**Method:** POST\n\n**Endpoint:** /api/sync/kobo/sync-from-server\n\n**Authentication:** Bearer token\n\n**Request Body:**\n- `force_sync` (boolean): Force full sync (ignore last sync time)\n- `books` (array, optional): List of book UUIDs to sync\n - If empty, syncs all books with server data\n - If specified, only syncs listed books\n- `sync_options` (object, optional):\n - `since_date` (string): ISO 8601 timestamp for incremental sync\n - `include_annotations` (boolean): Include highlights/notes\n - `include_progress` (boolean): Include reading progress\n - `conflict_resolution` (string): `most_recent`, `server_wins`, `client_wins`\n\n**Response:**\n- `synced` (integer): Number of books synced\n- `skipped` (integer): Books with no server changes\n- `failed` (integer): Books that failed to sync\n- `results` (array): Per-book sync results\n - `book_id` (string):\
books: \ Book UUID\n - `status` (string): `synced`, `skipped`, `failed`\n - `progress_pulled` (boolean): Whether progress was downloaded\n - `annotations_pulled` (integer): Number of annotations downloaded\n - `error` (string, optional): Error message if failed\n- `server_timestamp` (string): Server timestamp of sync\n\n**Status Codes:**\n- 200: Sync completed\n- 207: Multi-status (some succeeded, some failed)\n- 401: Unauthorized\n- 400: Invalid request\n\n**Sync Direction:**\n- **Server → Device** (this endpoint)\n- Device → Server: Use `/sync/kobo/markup` endpoint\n- Bidirectional sync achieved by calling both\n\n**Pull Sync Use Cases:**\n- New device setup (download all progress)\n- Device replacement (restore from server)\n- Multi-device sync (pull changes from other devices)\n- Conflict resolution (server wins)\n- Manual \"Download from server\" operation\n\n**Conflict Resolution:**\n- `most_recent`: Latest timestamp wins (default)\n- `server_wins`: Server data always used\n- `client_wins`:\
- book-uuid-1 \ Device data preserved\n- Applied per-book, per-item\n\n**Sync Optimization:**\n- Incremental sync by default (since last sync)\n- Force sync does full comparison\n- Book-level batching (10 books per batch)\n- Delta transfer (only changed items)\n- Compression for large annotation sets\n\n**Kobo Device Behavior:**\n- Device updates local database\n- Progress reflected in reading view\n- Annotations appear in Notebook\n- Bookmarks updated in navigation\n- Conflict warnings shown to user\n- Sync progress displayed on screen\n\n**Performance:**\n- Small sync (1-10 books): 2-5 seconds\n- Medium sync (10-50 books): 5-15 seconds\n- Large sync (50-200 books): 15-45 seconds\n- Timeout: 120 seconds\n\n**Use Cases:**\n- Initial device sync\n- After firmware update\n- From another device's changes\n- Manual sync request\n- Conflict recovery\n- Data restoration"
- book-uuid-2
docs: |-
## Sync Books from Server to Kobo
Pulls reading progress, annotations, and bookmarks from server to Kobo device.
**Method:** POST
**Endpoint:** /api/sync/kobo/sync-from-server
**Authentication:** Bearer token
**Request Body:**
- `force_sync` (boolean): Force full sync (ignore last sync time)
- `books` (array, optional): List of book UUIDs to sync
- If empty, syncs all books with server data
- If specified, only syncs listed books
- `sync_options` (object, optional):
- `since_date` (string): ISO 8601 timestamp for incremental sync
- `include_annotations` (boolean): Include highlights/notes
- `include_progress` (boolean): Include reading progress
- `conflict_resolution` (string): `most_recent`, `server_wins`, `client_wins`
**Response:**
- `synced` (integer): Number of books synced
- `skipped` (integer): Books with no server changes
- `failed` (integer): Books that failed to sync
- `results` (array): Per-book sync results
- `book_id` (string): Book UUID
- `status` (string): `synced`, `skipped`, `failed`
- `progress_pulled` (boolean): Whether progress was downloaded
- `annotations_pulled` (integer): Number of annotations downloaded
- `error` (string, optional): Error message if failed
- `server_timestamp` (string): Server timestamp of sync
**Status Codes:**
- 200: Sync completed
- 207: Multi-status (some succeeded, some failed)
- 401: Unauthorized
- 400: Invalid request
**Sync Direction:**
- **Server → Device** (this endpoint)
- Device → Server: Use `/sync/kobo/markup` endpoint
- Bidirectional sync achieved by calling both
**Pull Sync Use Cases:**
- New device setup (download all progress)
- Device replacement (restore from server)
- Multi-device sync (pull changes from other devices)
- Conflict resolution (server wins)
- Manual "Download from server" operation
**Conflict Resolution:**
- `most_recent`: Latest timestamp wins (default)
- `server_wins`: Server data always used
- `client_wins`: Device data preserved
- Applied per-book, per-item
**Sync Optimization:**
- Incremental sync by default (since last sync)
- Force sync does full comparison
- Book-level batching (10 books per batch)
- Delta transfer (only changed items)
- Compression for large annotation sets
**Kobo Device Behavior:**
- Device updates local database
- Progress reflected in reading view
- Annotations appear in Notebook
- Bookmarks updated in navigation
- Conflict warnings shown to user
- Sync progress displayed on screen
**Performance:**
- Small sync (1-10 books): 2-5 seconds
- Medium sync (10-50 books): 5-15 seconds
- Large sync (50-200 books): 15-45 seconds
- Timeout: 120 seconds
**Use Cases:**
- Initial device sync
- After firmware update
- From another device's changes
- Manual sync request
- Conflict recovery
- Data restoration
@@ -2,90 +2,17 @@ info:
name: Sync Multiple Books Progress name: Sync Multiple Books Progress
type: http type: http
seq: 3 seq: 3
http: http:
method: POST method: POST
url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/markup' url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/markup'
auth: inherit auth: inherit
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
- key: x-kobo-device - key: x-kobo-device
value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Aura"}' value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Aura"}'
body: body:
type: json type: json
json: data: "{\n \"ReadingSync\": [\n {\n \"ContentId\": \"book-1-uuid\",\n \"PercentRead\": 25.0,\n \"EntitlementId\": \"entitlement-1\",\n \"RemainingTimeMinutes\": 240,\n \"FirstReadTime\": \"2026-01-25T10:00:00Z\",\n \"LastModified\": \"2026-01-30T18:00:00Z\"\n },\n {\n \"ContentId\": \"book-2-uuid\",\n \"PercentRead\": 78.5,\n \"EntitlementId\": \"entitlement-2\",\n \"RemainingTimeMinutes\": 45,\n \"FirstReadTime\": \"2026-01-25T14:00:00Z\",\n \"LastModified\": \"2026-01-30T20:00:00Z\"\n }\n ],\n \"BookmarkSync\": []\n}"
ReadingSync: docs: "## Sync Multiple Books Progress\n\nSynchronizes reading progress for multiple books in a single request.\n\n**Method:** POST\n\n**Endpoint:** /api/sync/kobo/markup\n\n**Authentication:** Bearer token\n\n**Headers:**\n- `Authorization`: Bearer {{kobo_device_token}}\n- `x-kobo-device`: Device information (Model: \"Kobo Aura\")\n\n**Request Body:**\n- `ReadingSync` (array): Multiple progress items\n - Each item contains: ContentId, PercentRead, EntitlementId, etc.\n- `BookmarkSync` (array): Empty for progress-only batch\n\n**Response:**\n- `total` (integer): Total items in request\n- `synced` (integer): Successfully synced\n- `failed` (integer): Failed items\n- `results` (array): Per-item results\n - `ContentId` (string): Book UUID\n - `status` (string): `synced`, `failed`, `skipped`\n - `error` (string, optional): Error message if failed\n\n**Status Codes:**\n- 200: Batch sync completed\n- 207: Multi-status (some succeeded, some failed)\n- 401: Unauthorized\n- 413: Payload too large\
- ContentId: book-1-uuid \ (>1MB)\n\n**Batch Sync Advantages:**\n- Efficient sync of entire library\n- Reduces HTTP overhead\n- Faster for devices with many books\n- Atomic operation (all or nothing by default)\n\n**Kobo Batch Sync Behavior:**\n- Triggered when device connects after being offline\n- Occurs during manual \"Sync now\" operation\n- Limited to 100 books per request\n- Progress updates shown on device\n- Failed items retried individually\n\n**Performance:**\n- Typical batch: 10-50 books in 1-3 seconds\n- Large batch: 50-100 books in 3-8 seconds\n- Timeout: 30 seconds\n- Rate limit: 10 batches per minute per device\n\n**Use Cases:**\n- Initial device sync after registration\n- Catch-up sync after extended offline period\n- Library-wide progress update\n- Pre-sync before device firmware update"
PercentRead: 25.0
EntitlementId: entitlement-1
RemainingTimeMinutes: 240
FirstReadTime: '2026-01-25T10:00:00Z'
LastModified: '2026-01-30T18:00:00Z'
- ContentId: book-2-uuid
PercentRead: 78.5
EntitlementId: entitlement-2
RemainingTimeMinutes: 45
FirstReadTime: '2026-01-25T14:00:00Z'
LastModified: '2026-01-30T20:00:00Z'
BookmarkSync: []
docs: |-
## Sync Multiple Books Progress
Synchronizes reading progress for multiple books in a single request.
**Method:** POST
**Endpoint:** /api/sync/kobo/markup
**Authentication:** Bearer token
**Headers:**
- `Authorization`: Bearer {{kobo_device_token}}
- `x-kobo-device`: Device information (Model: "Kobo Aura")
**Request Body:**
- `ReadingSync` (array): Multiple progress items
- Each item contains: ContentId, PercentRead, EntitlementId, etc.
- `BookmarkSync` (array): Empty for progress-only batch
**Response:**
- `total` (integer): Total items in request
- `synced` (integer): Successfully synced
- `failed` (integer): Failed items
- `results` (array): Per-item results
- `ContentId` (string): Book UUID
- `status` (string): `synced`, `failed`, `skipped`
- `error` (string, optional): Error message if failed
**Status Codes:**
- 200: Batch sync completed
- 207: Multi-status (some succeeded, some failed)
- 401: Unauthorized
- 413: Payload too large (>1MB)
**Batch Sync Advantages:**
- Efficient sync of entire library
- Reduces HTTP overhead
- Faster for devices with many books
- Atomic operation (all or nothing by default)
**Kobo Batch Sync Behavior:**
- Triggered when device connects after being offline
- Occurs during manual "Sync now" operation
- Limited to 100 books per request
- Progress updates shown on device
- Failed items retried individually
**Performance:**
- Typical batch: 10-50 books in 1-3 seconds
- Large batch: 50-100 books in 3-8 seconds
- Timeout: 30 seconds
- Rate limit: 10 batches per minute per device
**Use Cases:**
- Initial device sync after registration
- Catch-up sync after extended offline period
- Library-wide progress update
- Pre-sync before device firmware update
@@ -2,83 +2,17 @@ info:
name: Sync Progress with Bookmarks name: Sync Progress with Bookmarks
type: http type: http
seq: 2 seq: 2
http: http:
method: POST method: POST
url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/markup' url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/markup'
auth: inherit auth: inherit
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
- key: x-kobo-device - key: x-kobo-device
value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Clara"}' value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Clara"}'
body: body:
type: json type: json
json: data: "{\n \"ReadingSync\": [\n {\n \"ContentId\": \"book-uuid\",\n \"PercentRead\": 42.3,\n \"EntitlementId\": \"entitlement-id\",\n \"RemainingTimeMinutes\": 138,\n \"FirstReadTime\": \"2026-01-25T10:00:00Z\",\n \"LastModified\": \"2026-01-30T20:00:00Z\"\n }\n ],\n \"BookmarkSync\": [\n {\n \"ContentId\": \"book-uuid\",\n \"BookmarkText\": \"highlighted text passage\",\n \"BookmarkType\": \"annotation\",\n \"BookmarkTitle\": \"Chapter 3\"\n }\n ]\n}"
ReadingSync: docs: "## Sync Progress with Bookmarks\n\nSynchronizes reading progress and highlights/annotations from Kobo device.\n\n**Method:** POST\n\n**Endpoint:** /api/sync/kobo/markup\n\n**Authentication:** Bearer token\n\n**Headers:**\n- `Authorization`: Bearer {{kobo_device_token}}\n- `x-kobo-device`: Device information JSON\n\n**Request Body:**\n- `ReadingSync` (array): Progress items (see Sync Reading Progress)\n- `BookmarkSync` (array): Highlights and annotations\n - `ContentId` (string): Book UUID\n - `BookmarkText` (string): Highlighted text or note content\n - `BookmarkType` (string): Type of bookmark\n - `annotation`: Highlighted text\n - `note`: Personal note\n - `bookmark`: Location bookmark\n - `BookmarkTitle` (string): Reference (e.g., chapter name)\n - `ChapterID` (string, optional): Chapter identifier\n - `DateCreated` (string, optional): Creation timestamp\n\n**Response:**\n- `progress_synced` (integer): Progress items synced\n- `bookmarks_synced` (integer): Bookmark\
- ContentId: book-uuid \ items synced\n- `conflicts_resolved` (integer): Number of conflicts auto-resolved\n- `timestamp` (string): Sync timestamp\n- `details` (object): Sync breakdown by type\n\n**Status Codes:**\n- 200: Successful sync\n- 401: Unauthorized\n- 400: Invalid data\n\n**Kobo Highlight Features:**\n- 5 highlight colors (yellow, green, blue, pink, orange)\n- Chapter-based organization\n- Linked to reading progress\n- Appears in Kobo \"Notebook\" view\n- Can be exported from device\n- Syncs across all user devices\n\n**Annotation Sync:**\n- Highlight text preserved exactly\n- Color mapped to system colors\n- Chapter reference maintained\n- Location data converted to standard format\n- Notes attached to highlights synced separately"
PercentRead: 42.3
EntitlementId: entitlement-id
RemainingTimeMinutes: 138
FirstReadTime: '2026-01-25T10:00:00Z'
LastModified: '2026-01-30T20:00:00Z'
BookmarkSync:
- ContentId: book-uuid
BookmarkText: highlighted text passage
BookmarkType: annotation
BookmarkTitle: Chapter 3
docs: |-
## Sync Progress with Bookmarks
Synchronizes reading progress and highlights/annotations from Kobo device.
**Method:** POST
**Endpoint:** /api/sync/kobo/markup
**Authentication:** Bearer token
**Headers:**
- `Authorization`: Bearer {{kobo_device_token}}
- `x-kobo-device`: Device information JSON
**Request Body:**
- `ReadingSync` (array): Progress items (see Sync Reading Progress)
- `BookmarkSync` (array): Highlights and annotations
- `ContentId` (string): Book UUID
- `BookmarkText` (string): Highlighted text or note content
- `BookmarkType` (string): Type of bookmark
- `annotation`: Highlighted text
- `note`: Personal note
- `bookmark`: Location bookmark
- `BookmarkTitle` (string): Reference (e.g., chapter name)
- `ChapterID` (string, optional): Chapter identifier
- `DateCreated` (string, optional): Creation timestamp
**Response:**
- `progress_synced` (integer): Progress items synced
- `bookmarks_synced` (integer): Bookmark items synced
- `conflicts_resolved` (integer): Number of conflicts auto-resolved
- `timestamp` (string): Sync timestamp
- `details` (object): Sync breakdown by type
**Status Codes:**
- 200: Successful sync
- 401: Unauthorized
- 400: Invalid data
**Kobo Highlight Features:**
- 5 highlight colors (yellow, green, blue, pink, orange)
- Chapter-based organization
- Linked to reading progress
- Appears in Kobo "Notebook" view
- Can be exported from device
- Syncs across all user devices
**Annotation Sync:**
- Highlight text preserved exactly
- Color mapped to system colors
- Chapter reference maintained
- Location data converted to standard format
- Notes attached to highlights synced separately
@@ -2,83 +2,18 @@ info:
name: Sync Reading Progress name: Sync Reading Progress
type: http type: http
seq: 1 seq: 1
http: http:
method: POST method: POST
url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/markup' url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/markup'
auth: inherit auth: inherit
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
- key: x-kobo-device - key: x-kobo-device
value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Clara"}' value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Clara"}'
body: body:
type: json type: json
json: data: "{\n \"ReadingSync\": [\n {\n \"ContentId\": \"book-uuid-here\",\n \"PercentRead\": 45.6,\n \"EntitlementId\": \"entitlement-id-here\",\n \"RemainingTimeMinutes\": 120,\n \"FirstReadTime\": \"2026-01-25T10:00:00Z\",\n \"LastModified\": \"2026-01-30T20:00:00Z\"\n }\n ],\n \"BookmarkSync\": []\n}"
ReadingSync: docs: "## Sync Kobo Reading Progress\n\nSynchronizes reading progress from a Kobo device to the server using Bearer token authentication.\n\n**Method:** POST\n\n**Endpoint:** /api/sync/kobo/markup\n\n**Authentication:** Bearer token (in Authorization header)\n\n**Headers:**\n- `Authorization`: Bearer {{kobo_device_token}}\n- `x-kobo-device`: JSON-encoded device info\n - `DeviceId`: Kobo device identifier\n - `Model`: Device model (e.g., \"Kobo Clara\", \"Kobo Libra\", \"Kobo Aura\")\n\n**Request Body:**\n- `ReadingSync` (array): Reading progress items\n - `ContentId` (string): Book/Content UUID\n - `PercentRead` (number): Reading progress 0-100\n - `EntitlementId` (string): Kobo entitlement ID\n - `RemainingTimeMinutes` (integer): Estimated reading time remaining\n - `FirstReadTime` (string): ISO 8601 timestamp when first opened\n - `LastModified` (string): ISO 8601 timestamp of last progress update\n- `BookmarkSync` (array): Empty array for progress-only sync\n\n**Response:**\n\
- ContentId: book-uuid-here - `synced` (integer): Number of items synced\n- `failed` (integer): Number of items that failed to sync\n- `timestamp` (string): Server timestamp of sync\n- `books` (array): Synced book data\n - `ContentId` (string): Book UUID\n - `status` (string): `synced`, `failed`, `skipped`\n - `server_percent` (number): Server-side progress (for conflict detection)\n\n**Status Codes:**\n- 200: Sync successful\n- 401: Invalid device token\n- 403: Device not approved\n- 400: Invalid request format\n\n**Authentication Methods:**\nThis endpoint uses **Bearer token authentication** (token in Authorization header).\nAlternative: Use `/sync/kobo/{token}/markup` with token in URL path.\n\n**Kobo Sync Features:**\n- Native Kobo sync protocol\n- Supports Kobo Clara, Libra, Aura, Forma, Sage, Elipsa\n- Progress percentage tracking\n- Reading time estimation\n- Cross-device synchronization\n- Automatic conflict resolution (most recent wins)\n\n**Sync Frequency:**\n- Kobo devices auto-sync every 15-30 minutes\
PercentRead: 45.6 \ when connected to WiFi\n- Manual sync available from device settings\n- Sync triggers on: device wake, book close, WiFi connection"
EntitlementId: entitlement-id-here
RemainingTimeMinutes: 120
FirstReadTime: '2026-01-25T10:00:00Z'
LastModified: '2026-01-30T20:00:00Z'
BookmarkSync: []
docs: |-
## Sync Kobo Reading Progress
Synchronizes reading progress from a Kobo device to the server using Bearer token authentication.
**Method:** POST
**Endpoint:** /api/sync/kobo/markup
**Authentication:** Bearer token (in Authorization header)
**Headers:**
- `Authorization`: Bearer {{kobo_device_token}}
- `x-kobo-device`: JSON-encoded device info
- `DeviceId`: Kobo device identifier
- `Model`: Device model (e.g., "Kobo Clara", "Kobo Libra", "Kobo Aura")
**Request Body:**
- `ReadingSync` (array): Reading progress items
- `ContentId` (string): Book/Content UUID
- `PercentRead` (number): Reading progress 0-100
- `EntitlementId` (string): Kobo entitlement ID
- `RemainingTimeMinutes` (integer): Estimated reading time remaining
- `FirstReadTime` (string): ISO 8601 timestamp when first opened
- `LastModified` (string): ISO 8601 timestamp of last progress update
- `BookmarkSync` (array): Empty array for progress-only sync
**Response:**
- `synced` (integer): Number of items synced
- `failed` (integer): Number of items that failed to sync
- `timestamp` (string): Server timestamp of sync
- `books` (array): Synced book data
- `ContentId` (string): Book UUID
- `status` (string): `synced`, `failed`, `skipped`
- `server_percent` (number): Server-side progress (for conflict detection)
**Status Codes:**
- 200: Sync successful
- 401: Invalid device token
- 403: Device not approved
- 400: Invalid request format
**Authentication Methods:**
This endpoint uses **Bearer token authentication** (token in Authorization header).
Alternative: Use `/sync/kobo/{token}/markup` with token in URL path.
**Kobo Sync Features:**
- Native Kobo sync protocol
- Supports Kobo Clara, Libra, Aura, Forma, Sage, Elipsa
- Progress percentage tracking
- Reading time estimation
- Cross-device synchronization
- Automatic conflict resolution (most recent wins)
**Sync Frequency:**
- Kobo devices auto-sync every 15-30 minutes when connected to WiFi
- Manual sync available from device settings
- Sync triggers on: device wake, book close, WiFi connection
@@ -2,91 +2,18 @@ info:
name: Sync Single Bookmark name: Sync Single Bookmark
type: http type: http
seq: 5 seq: 5
http: http:
method: POST method: POST
url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/bookmark' url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/bookmark'
auth: inherit auth: inherit
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
- key: x-kobo-device - key: x-kobo-device
value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Clara"}' value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Clara"}'
body: body:
type: json type: json
json: data: "{\n \"ContentId\": \"book-uuid\",\n \"BookmarkText\": \"Bookmarked passage\",\n \"BookmarkType\": \"annotation\",\n \"BookmarkTitle\": \"Chapter 3\"\n}"
ContentId: book-uuid docs: "## Sync Single Bookmark\n\nSynchronizes an individual bookmark/highlight from Kobo device.\n\n**Method:** POST\n\n**Endpoint:** /api/sync/kobo/bookmark\n\n**Authentication:** Bearer token\n\n**Headers:**\n- `Authorization`: Bearer {{kobo_device_token}}\n- `x-kobo-device`: Device information JSON\n\n**Request Body:**\n- `ContentId` (string): Book UUID\n- `BookmarkText` (string): Highlighted text or bookmark description\n- `BookmarkType` (string): Type of bookmark\n - `annotation`: Highlighted text passage\n - `note`: Personal note\n - `bookmark`: Location marker\n- `BookmarkTitle` (string): Reference title (e.g., chapter name)\n- `ChapterID` (string, optional): Chapter identifier\n- `DateCreated` (string, optional): ISO 8601 timestamp\n- `highlight_color` (string, optional): Color name (yellow, green, blue, pink, orange)\n\n**Response:**\n- `id` (string): Server bookmark ID\n- `ContentId` (string): Associated book UUID\n- `status` (string): `created`, `updated`, `skipped` (duplicate)\n\
BookmarkText: Bookmarked passage - `timestamp` (string): Server timestamp\n- `url` (string): API URL to retrieve bookmark\n\n**Status Codes:**\n- 201: Bookmark created\n- 200: Bookmark updated (duplicate found)\n- 409: Duplicate bookmark (unchanged)\n- 401: Unauthorized\n- 400: Invalid bookmark data\n\n**Single Bookmark Sync vs Batch:**\n- **Single bookmark endpoint:** Real-time, immediate sync\n- **Batch markup endpoint:** Deferred, periodic sync\n- Use single when user explicitly creates highlight\n- Use batch for periodic background sync\n\n**Kobo Trigger:**\n- User highlights text → immediate single sync\n- User adds note → immediate single sync\n- Device goes online → batch sync of all changes\n\n**Duplicate Detection:**\n- Same ContentId + similar BookmarkText + same location\n- Updates existing if text modified\n- Skips if identical bookmark exists\n- Preserves creation date of original\n\n**Use Cases:**\n- Real-time highlight sync\n- Instant note backup\n- Immediate annotation sharing\n- Quick single annotation\
BookmarkType: annotation \ update\n- Testing annotation sync\n\n**Notes:**\n- Much faster than full markup sync\n- Lower bandwidth usage\n- Ideal for intermittent connectivity\n- Can be called multiple times safely"
BookmarkTitle: Chapter 3
docs: |-
## Sync Single Bookmark
Synchronizes an individual bookmark/highlight from Kobo device.
**Method:** POST
**Endpoint:** /api/sync/kobo/bookmark
**Authentication:** Bearer token
**Headers:**
- `Authorization`: Bearer {{kobo_device_token}}
- `x-kobo-device`: Device information JSON
**Request Body:**
- `ContentId` (string): Book UUID
- `BookmarkText` (string): Highlighted text or bookmark description
- `BookmarkType` (string): Type of bookmark
- `annotation`: Highlighted text passage
- `note`: Personal note
- `bookmark`: Location marker
- `BookmarkTitle` (string): Reference title (e.g., chapter name)
- `ChapterID` (string, optional): Chapter identifier
- `DateCreated` (string, optional): ISO 8601 timestamp
- `highlight_color` (string, optional): Color name (yellow, green, blue, pink, orange)
**Response:**
- `id` (string): Server bookmark ID
- `ContentId` (string): Associated book UUID
- `status` (string): `created`, `updated`, `skipped` (duplicate)
- `timestamp` (string): Server timestamp
- `url` (string): API URL to retrieve bookmark
**Status Codes:**
- 201: Bookmark created
- 200: Bookmark updated (duplicate found)
- 409: Duplicate bookmark (unchanged)
- 401: Unauthorized
- 400: Invalid bookmark data
**Single Bookmark Sync vs Batch:**
- **Single bookmark endpoint:** Real-time, immediate sync
- **Batch markup endpoint:** Deferred, periodic sync
- Use single when user explicitly creates highlight
- Use batch for periodic background sync
**Kobo Trigger:**
- User highlights text → immediate single sync
- User adds note → immediate single sync
- Device goes online → batch sync of all changes
**Duplicate Detection:**
- Same ContentId + similar BookmarkText + same location
- Updates existing if text modified
- Skips if identical bookmark exists
- Preserves creation date of original
**Use Cases:**
- Real-time highlight sync
- Instant note backup
- Immediate annotation sharing
- Quick single annotation update
- Testing annotation sync
**Notes:**
- Much faster than full markup sync
- Lower bandwidth usage
- Ideal for intermittent connectivity
- Can be called multiple times safely
@@ -2,105 +2,17 @@ info:
name: Sync with Annotations name: Sync with Annotations
type: http type: http
seq: 4 seq: 4
http: http:
method: POST method: POST
url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/markup' url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/markup'
auth: inherit auth: inherit
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
- key: x-kobo-device - key: x-kobo-device
value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Libra"}' value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Libra"}'
body: body:
type: json type: json
json: data: "{\n \"ReadingSync\": [\n {\n \"ContentId\": \"book-uuid\",\n \"PercentRead\": 55.0,\n \"EntitlementId\": \"entitlement-id\",\n \"RemainingTimeMinutes\": 120,\n \"FirstReadTime\": \"2026-01-25T10:00:00Z\",\n \"LastModified\": \"2026-01-30T20:00:00Z\"\n }\n ],\n \"BookmarkSync\": [\n {\n \"ContentId\": \"book-uuid\",\n \"BookmarkText\": \"Important quote\",\n \"BookmarkType\": \"annotation\",\n \"BookmarkTitle\": \"Chapter 4 - The Truth\"\n },\n {\n \"ContentId\": \"book-uuid\",\n \"BookmarkText\": \"Another quote\",\n \"BookmarkType\": \"annotation\",\n \"BookmarkTitle\": \"Chapter 5\"\n },\n {\n \"ContentId\": \"book-uuid\",\n \"BookmarkText\": \"Note to myself\",\n \"BookmarkType\": \"note\",\n \"BookmarkTitle\": \"Personal note\"\n }\n ]\n}"
ReadingSync: docs: "## Sync with Multiple Annotations\n\nSynchronizes reading progress with multiple highlights and notes.\n\n**Method:** POST\n\n**Endpoint:** /api/sync/kobo/markup\n\n**Authentication:** Bearer token\n\n**Headers:**\n- `Authorization`: Bearer {{kobo_device_token}}\n- `x-kobo-device`: Device info (Model: \"Kobo Libra\")\n\n**Request Body:**\n- `ReadingSync` (array): Single book progress\n- `BookmarkSync` (array): Multiple annotations\n - Can include highlights (annotation type)\n - Can include notes (note type)\n - Each has: ContentId, BookmarkText, BookmarkType, BookmarkTitle\n\n**Response:**\n- `progress_synced` (boolean): Progress update status\n- `annotations_synced` (integer): Number of annotations synced\n- `highlights_count` (integer): Highlights synced\n- `notes_count` (integer): Notes synced\n- `conflicts` (array): Any annotation conflicts resolved\n- `timestamp` (string): Sync completion time\n\n**Status Codes:**\n- 200: Successful sync\n- 401: Unauthorized\n- 400: Invalid\
- ContentId: book-uuid \ annotation data\n\n**Kobo Annotation Types:**\n- **Highlights:** Selected text passages\n - 5 preset colors available\n - Can have chapter titles\n - Exportable to PDF/Mobile\n- **Notes:** Personal annotations\n - Free-form text\n - Can be attached to highlights\n - Separate from highlights\n- **Bookmarks:** Location markers\n - Chapter positions\n - Quick navigation\n\n**Sync Behavior:**\n- Duplicates detected by content matching\n- Most recent edit wins conflicts\n- Annotations linked to book content\n- Chapter references preserved\n- Order maintained from device\n\n**Kobo Notebook Export:**\n- All annotations appear in Kobo \"Notebook\"\n- Can be exported to PDF\n- Can be exported to Mobile (text)\n- Organized by book\n- Shows highlight context\n\n**Use Cases:**\n- Study and research\n- Book club discussion prep\n- Content review\n- Sharing insights\n- Personal learning archive"
PercentRead: 55.0
EntitlementId: entitlement-id
RemainingTimeMinutes: 120
FirstReadTime: '2026-01-25T10:00:00Z'
LastModified: '2026-01-30T20:00:00Z'
BookmarkSync:
- ContentId: book-uuid
BookmarkText: Important quote
BookmarkType: annotation
BookmarkTitle: Chapter 4 - The Truth
- ContentId: book-uuid
BookmarkText: Another quote
BookmarkType: annotation
BookmarkTitle: Chapter 5
- ContentId: book-uuid
BookmarkText: Note to myself
BookmarkType: note
BookmarkTitle: Personal note
docs: |-
## Sync with Multiple Annotations
Synchronizes reading progress with multiple highlights and notes.
**Method:** POST
**Endpoint:** /api/sync/kobo/markup
**Authentication:** Bearer token
**Headers:**
- `Authorization`: Bearer {{kobo_device_token}}
- `x-kobo-device`: Device info (Model: "Kobo Libra")
**Request Body:**
- `ReadingSync` (array): Single book progress
- `BookmarkSync` (array): Multiple annotations
- Can include highlights (annotation type)
- Can include notes (note type)
- Each has: ContentId, BookmarkText, BookmarkType, BookmarkTitle
**Response:**
- `progress_synced` (boolean): Progress update status
- `annotations_synced` (integer): Number of annotations synced
- `highlights_count` (integer): Highlights synced
- `notes_count` (integer): Notes synced
- `conflicts` (array): Any annotation conflicts resolved
- `timestamp` (string): Sync completion time
**Status Codes:**
- 200: Successful sync
- 401: Unauthorized
- 400: Invalid annotation data
**Kobo Annotation Types:**
- **Highlights:** Selected text passages
- 5 preset colors available
- Can have chapter titles
- Exportable to PDF/Mobile
- **Notes:** Personal annotations
- Free-form text
- Can be attached to highlights
- Separate from highlights
- **Bookmarks:** Location markers
- Chapter positions
- Quick navigation
**Sync Behavior:**
- Duplicates detected by content matching
- Most recent edit wins conflicts
- Annotations linked to book content
- Chapter references preserved
- Order maintained from device
**Kobo Notebook Export:**
- All annotations appear in Kobo "Notebook"
- Can be exported to PDF
- Can be exported to Mobile (text)
- Organized by book
- Shows highlight context
**Use Cases:**
- Study and research
- Book club discussion prep
- Content review
- Sharing insights
- Personal learning archive
+2 -32
View File
@@ -8,38 +8,8 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"BookmarkSync\": [\n {\n \"BookmarkId\": \"bookmark_2\"\ data: "{\n \"BookmarkSync\": [\n {\n \"BookmarkId\": \"bookmark_2\",\n \"ContentId\": \"kobo_xyz789\",\n \"BookmarkText\": \"Important note\",\n \"BookmarkType\": \"bookmark\",\n \"DateCreated\": \"2026-01-31T12:00:00Z\""
,\n \"ContentId\": \"kobo_xyz789\",\n \"BookmarkText\": \"Important\
\ note\",\n \"BookmarkType\": \"bookmark\",\n \"DateCreated\"\
: \"2026-01-31T12:00:00Z\""
headers: headers:
- key: Authorization - key: Authorization
value: Bearer {{device_token value: Bearer {{device_token
docs: "## Kobo Bookmark Sync\n\n Synchronizes bookmarks from a Kobo device to the Bookhoard server.\n\n **Method:** POST\n\n **Endpoint:** /api/v1/kobo/bookmark\n\n **Authentication:** Bearer token (device token)\n\n **Request Body:**\n - `BookmarkSync` (array): Array of bookmark objects\n - `BookmarkId` (string): Unique bookmark ID\n - `ContentId` (string): Book/content ID\n - `BookmarkText` (string): Bookmark text or note\n - `BookmarkType` (string): Type (bookmark, highlight, note)\n - `DateCreated` (string): ISO 8601 timestamp\n\n **Response:**\n - `Status` (string): Sync status (Success, Partial)\n - `BookmarksSynced` (number): Number of bookmarks synced\n\n **Status Codes:**\n - 200: Success\n - 401: Unauthorized\n - 500: Internal server error"
docs: |-
## Kobo Bookmark Sync
Synchronizes bookmarks from a Kobo device to the Bookhoard server.
**Method:** POST
**Endpoint:** /api/v1/kobo/bookmark
**Authentication:** Bearer token (device token)
**Request Body:**
- `BookmarkSync` (array): Array of bookmark objects
- `BookmarkId` (string): Unique bookmark ID
- `ContentId` (string): Book/content ID
- `BookmarkText` (string): Bookmark text or note
- `BookmarkType` (string): Type (bookmark, highlight, note)
- `DateCreated` (string): ISO 8601 timestamp
**Response:**
- `Status` (string): Sync status (Success, Partial)
- `BookmarksSynced` (number): Number of bookmarks synced
**Status Codes:**
- 200: Success
- 401: Unauthorized
- 500: Internal server error
+2 -30
View File
@@ -8,36 +8,8 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"unlinked_book_id\": \"{{unlinked_book_id" data: "{\n \"unlinked_book_id\": \"{{unlinked_book_id"
headers: headers:
- key: Authorization - key: Authorization
value: Bearer {{user_token value: Bearer {{user_token
docs: "## Link Unlinked Book - Manual Resolution\n\n Manually links an unlinked book to a media item in the library.\n\n **Method:** POST\n\n **Endpoint:** /api/sync/link-book\n\n **Authentication:** Bearer token\n\n **Request Body:**\n - `unlinked_book_id` (string): Unlinked book UUID\n - `media_item_id` (string): Media item UUID to link to\n - `confidence_score` (number): Match confidence (0-1, 1.0 for manual)\n\n **Response:**\n - `status` (string): Link status (linked)\n - `unlinked_book_id` (string): Unlinked book UUID\n - `media_item_id` (string): Media item UUID\n - `message` (string): Success message\n\n **Status Codes:**\n - 200: Success - book linked\n - 400: Invalid request\n - 401: Unauthorized\n - 404: Book or media item not found\n - 500: Internal server error"
docs: |-
## Link Unlinked Book - Manual Resolution
Manually links an unlinked book to a media item in the library.
**Method:** POST
**Endpoint:** /api/sync/link-book
**Authentication:** Bearer token
**Request Body:**
- `unlinked_book_id` (string): Unlinked book UUID
- `media_item_id` (string): Media item UUID to link to
- `confidence_score` (number): Match confidence (0-1, 1.0 for manual)
**Response:**
- `status` (string): Link status (linked)
- `unlinked_book_id` (string): Unlinked book UUID
- `media_item_id` (string): Media item UUID
- `message` (string): Success message
**Status Codes:**
- 200: Success - book linked
- 400: Invalid request
- 401: Unauthorized
- 404: Book or media item not found
- 500: Internal server error
@@ -8,18 +8,8 @@ http:
auth: none auth: none
body: body:
type: json type: json
jsonBody: "{\n \"ContentId\": \"book-uuid\",\n \"BookmarkText\": \"Highlighted\ data: "{\n \"ContentId\": \"book-uuid\",\n \"BookmarkText\": \"Highlighted text\",\n \"BookmarkType\": \"annotation\",\n \"BookmarkTitle\": \"Chapter 3\""
\ text\",\n \"BookmarkType\": \"annotation\",\n \"BookmarkTitle\": \"\
Chapter 3\""
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
docs: "## Sync Bookmark - URL Path Token\n\n Synchronizes bookmarks and annotations from Kobo device using token in URL path.\n\n **Method:** POST\n\n **Endpoint:** /sync/kobo/{kobo_device_token"
docs: |-
## Sync Bookmark - URL Path Token
Synchronizes bookmarks and annotations from Kobo device using token in URL path.
**Method:** POST
**Endpoint:** /sync/kobo/{kobo_device_token
@@ -8,19 +8,8 @@ http:
auth: none auth: none
body: body:
type: json type: json
jsonBody: "{\n \"ReadingSync\": [\n {\n \"ContentId\": \"book-uuid\"\ data: "{\n \"ReadingSync\": [\n {\n \"ContentId\": \"book-uuid\",\n \"PercentRead\": 45.6,\n \"EntitlementId\": \"entitlement-id\",\n \"RemainingTimeMinutes\": 120,\n \"FirstReadTime\": \"2026-01-25T10:00:00Z\",\n \"LastModified\": \"2026-01-30T20:00:00Z\""
,\n \"PercentRead\": 45.6,\n \"EntitlementId\": \"entitlement-id\"\
,\n \"RemainingTimeMinutes\": 120,\n \"FirstReadTime\": \"2026-01-25T10:00:00Z\"\
,\n \"LastModified\": \"2026-01-30T20:00:00Z\""
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
docs: "## Sync Reading Progress - URL Path Token\n\n Synchronizes reading progress from Kobo device using token in URL path.\n\n **Method:** POST\n\n **Endpoint:** /sync/kobo/{kobo_device_token"
docs: |-
## Sync Reading Progress - URL Path Token
Synchronizes reading progress from Kobo device using token in URL path.
**Method:** POST
**Endpoint:** /sync/kobo/{kobo_device_token
@@ -3,18 +3,8 @@ docs: "## Checkpoint Sync\n\nIncremental sync using checkpoint-based change trac
\ Repeat from step 3\n\n**Sync Optimization:**\n- Only books with progress changes\n- Skips unmodified books\n- Delta transfer\n- Compression for large payloads\n- Batch processing\n\n**Failure Handling:**\n- Checkpoint ID preserved on failure\n- Retry with same checkpoint\n- Full sync if checkpoint expired\n- Checkpoint validity: 24 hours\n- Auto-fallback to full sync\n\n**Performance:**\n- Small changes (1-10 books): 100-300ms\n- Medium changes (10-50 books): 300ms-1s\n- Large changes (50-100 books): 1-3s\n- Typical: 5-10x faster than full sync\n\n**When to Use:**\n- Default sync mode for most users\n- Periodic background sync\n- Large library management\n- Mobile/network-constrained environments\n- Battery-powered devices\n\n**Configuration:**\n- Checkpoint expiration: 24 hours\n- Max history: 100 checkpoints\n- Auto-cleanup of old checkpoints\n- Configurable sync interval" \ Repeat from step 3\n\n**Sync Optimization:**\n- Only books with progress changes\n- Skips unmodified books\n- Delta transfer\n- Compression for large payloads\n- Batch processing\n\n**Failure Handling:**\n- Checkpoint ID preserved on failure\n- Retry with same checkpoint\n- Full sync if checkpoint expired\n- Checkpoint validity: 24 hours\n- Auto-fallback to full sync\n\n**Performance:**\n- Small changes (1-10 books): 100-300ms\n- Medium changes (10-50 books): 300ms-1s\n- Large changes (50-100 books): 1-3s\n- Typical: 5-10x faster than full sync\n\n**When to Use:**\n- Default sync mode for most users\n- Periodic background sync\n- Large library management\n- Mobile/network-constrained environments\n- Battery-powered devices\n\n**Configuration:**\n- Checkpoint expiration: 24 hours\n- Max history: 100 checkpoints\n- Auto-cleanup of old checkpoints\n- Configurable sync interval"
http: http:
body: body:
json:
books:
- chapter: 3
percentage: 0.45
uuid: '{{media_item_id}}'
- chapter: 8
percentage: 0.75
uuid: '{{media_item_id}}'
checkpoint_id: checkpoint-uuid
since_timestamp: '2026-01-30T19:00:00Z'
sync_mode: checkpoint
type: json type: json
data: "{\n \"books\": [\n {\n \"chapter\": 3,\n \"percentage\": 0.45,\n \"uuid\": \"{{media_item_id}}\"\n },\n {\n \"chapter\": 8,\n \"percentage\": 0.75,\n \"uuid\": \"{{media_item_id}}\"\n }\n ],\n \"checkpoint_id\": \"checkpoint-uuid\",\n \"since_timestamp\": \"2026-01-30T19:00:00Z\",\n \"sync_mode\": \"checkpoint\"\n}"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
@@ -3,14 +3,8 @@ docs: "## Immediate Sync - Page Turn\n\nReal-time progress sync triggered immedi
- Research and study\n- Testing sync functionality\n- When power source available\n\n**When NOT to Use:**\n- Battery conservation needed\n- Unstable network\n- Extended reading sessions\n- Background sync preferred" - Research and study\n- Testing sync functionality\n- When power source available\n\n**When NOT to Use:**\n- Battery conservation needed\n- Unstable network\n- Extended reading sessions\n- Background sync preferred"
http: http:
body: body:
json:
books:
- chapter: 3
percentage: 0.45678
timestamp: '2026-01-30T20:00:00Z'
uuid: '{{media_item_id}}'
sync_mode: immediate
type: json type: json
data: "{\n \"books\": [\n {\n \"chapter\": 3,\n \"percentage\": 0.45678,\n \"timestamp\": \"2026-01-30T20:00:00Z\",\n \"uuid\": \"{{media_item_id}}\"\n }\n ],\n \"sync_mode\": \"immediate\"\n}"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
@@ -1,8 +1,8 @@
docs: "## KOReader Sync Annotations - Per-Book SHA-256\n\n Synchronizes annotations (highlights) from KOReader with per-annotation SHA-256 hashes for multi-book sync.\n\n **Method:** POST\n\n **Endpoint:** /api/v1/koreader/sync/bookmarks\n\n **Authentication:** Bearer token (KOReader device token)\n\n **Request Body:**\n - `book_uuid` (string): Primary book UUID\n - `highlights` (array): Array of highlight objects\n - `text` (string): Highlighted text\n - `pos0`, `pos1` (string): EPUB CFI positions\n - `color` (string): Highlight color (hex)\n - `page` (number): Page number\n - `book_sha256` (string): SHA-256 hash for this specific book\n\n **Response:**\n - `highlights_synced` (number): Number of highlights synced\n\n **Status Codes:**\n - 200: Success\n - 401: Unauthorized\n - 500: Internal server error\n\n **Note:** Each highlight can include its own book_sha256, allowing annotations from multiple books in a single request." docs: "## KOReader Sync Annotations - Per-Book SHA-256\n\n Synchronizes annotations (highlights) from KOReader with per-annotation SHA-256 hashes for multi-book sync.\n\n **Method:** POST\n\n **Endpoint:** /api/v1/koreader/sync/bookmarks\n\n **Authentication:** Bearer token (KOReader device token)\n\n **Request Body:**\n - `book_uuid` (string): Primary book UUID\n - `highlights` (array): Array of highlight objects\n - `text` (string): Highlighted text\n - `pos0`, `pos1` (string): EPUB CFI positions\n - `color` (string): Highlight color (hex)\n - `page` (number): Page number\n - `book_sha256` (string): SHA-256 hash for this specific book\n\n **Response:**\n - `highlights_synced` (number): Number of highlights synced\n\n **Status Codes:**\n - 200: Success\n - 401: Unauthorized\n - 500: Internal server error\n\n **Note:** Each highlight can include its own book_sha256, allowing annotations from multiple books in a single request."
http: http:
body: body:
jsonBody: "{\n \"book_uuid\": \"{{book_uuid"
type: json type: json
data: "{\n \"book_sha256\": \"{{book_sha256}}\",\n \"bookmarks\": [\n {\n \"chapter\": 3,\n \"datetime\": \"2026-01-30T19:55:00Z\",\n \"page\": 45,\n \"pos0\": \"p1\",\n \"text\": \"Per-book annotation\",\n \"type\": \"bookmark\"\n }\n ]\n}"
headers: [] headers: []
method: POST method: POST
url: '{{base_url}}/api/sync/koreader/bookmarks' url: '{{base_url}}/api/sync/koreader/bookmarks'
@@ -1,8 +1,8 @@
docs: "## KOReader Sync Bookmarks - SHA-256\n\n Synchronizes bookmarks, notes, and highlights from KOReader using SHA-256 hash for book identification.\n\n **Method:** POST\n\n **Endpoint:** /api/v1/koreader/sync/bookmarks\n\n **Authentication:** Bearer token (KOReader device token)\n\n **Request Body:**\n - `book_sha256` (string): SHA-256 hash of book file\n - `bookmarks` (array): Array of bookmarks\n - `notes` (array): Array of notes\n - `highlights` (array): Array of highlights\n\n **Response:**\n - `sync_status` (string): Sync status\n - `total_synced` (number): Total items synced\n\n **Status Codes:**\n - 200: Success\n - 401: Unauthorized\n - 500: Internal server error" docs: "## KOReader Sync Bookmarks - SHA-256\n\n Synchronizes bookmarks, notes, and highlights from KOReader using SHA-256 hash for book identification.\n\n **Method:** POST\n\n **Endpoint:** /api/v1/koreader/sync/bookmarks\n\n **Authentication:** Bearer token (KOReader device token)\n\n **Request Body:**\n - `book_sha256` (string): SHA-256 hash of book file\n - `bookmarks` (array): Array of bookmarks\n - `notes` (array): Array of notes\n - `highlights` (array): Array of highlights\n\n **Response:**\n - `sync_status` (string): Sync status\n - `total_synced` (number): Total items synced\n\n **Status Codes:**\n - 200: Success\n - 401: Unauthorized\n - 500: Internal server error"
http: http:
body: body:
jsonBody: "{\n \"book_sha256\": \"{{book_sha256"
type: json type: json
data: "{\n \"book_sha256\": \"{{book_sha256}}\",\n \"bookmarks\": [\n {\n \"chapter\": 3,\n \"datetime\": \"2026-01-30T19:55:00Z\",\n \"page\": 45,\n \"pos0\": \"p1\",\n \"text\": \"SHA-256 bookmark\",\n \"type\": \"bookmark\"\n }\n ]\n}"
headers: [] headers: []
method: POST method: POST
url: '{{base_url}}/api/sync/koreader/bookmarks' url: '{{base_url}}/api/sync/koreader/bookmarks'
@@ -3,20 +3,8 @@ docs: "## Sync KOReader Bookmarks\n\nSynchronizes bookmarks separately from prog
\ bookmark\" menu\n- Shown in \"Bookmarks\" panel\n- Quick access via \"Go to bookmark\"\n- Exportable to JSON\n- Can be edited/deleted\n\n**Use Cases:**\n- Quick navigation aids\n- Chapter markers\n- Important passages\n- Reading progress points\n- Study session markers" \ bookmark\" menu\n- Shown in \"Bookmarks\" panel\n- Quick access via \"Go to bookmark\"\n- Exportable to JSON\n- Can be edited/deleted\n\n**Use Cases:**\n- Quick navigation aids\n- Chapter markers\n- Important passages\n- Reading progress points\n- Study session markers"
http: http:
body: body:
json:
books:
- bookmarks:
- chapter: 3
datetime: '2026-01-30T19:55:00Z'
notes: Marked this chapter as important
page: 45
pos0: epubcfi(/6/4/2:15)
pos1: epubcfi(/6/4/2:20)
text: Important passage
type: bookmark
uuid: '{{media_item_id}}'
library_id: optional-library-uuid
type: json type: json
data: "{\n \"book_uuid\": \"{{media_item_id}}\",\n \"bookmarks\": [\n {\n \"chapter\": 3,\n \"datetime\": \"2026-01-30T19:55:00Z\",\n \"page\": 45,\n \"pos0\": \"epubcfi(/6/4/2:15)\",\n \"pos1\": \"epubcfi(/6/4/2:20)\",\n \"text\": \"Important passage\",\n \"type\": \"bookmark\"\n }\n ]\n}"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
@@ -4,10 +4,7 @@ docs: "## Sync KOReader Highlights\n\nSynchronizes text highlights separately fr
http: http:
body: body:
type: json type: json
jsonBody: "{\n \"book_uuid\": \"786b9c39-e8c3-47ab-a48a-c7f6cd769930\",\n \"highlights\": [\n {\n \"datetime\": \"2026-01-30T19:50:00Z\",\n \"text\": \"bruno sweep highlight\",\n \"chapter\": 1,\n \"page\": 1,\n \"pos0\": \"1\",\n \"pos1\": \"9\",\n \"color\": \"#ffff00\"\n }\n ]\n}" data: "{\n \"book_uuid\": \"786b9c39-e8c3-47ab-a48a-c7f6cd769930\",\n \"highlights\": [\n {\n \"datetime\": \"2026-01-30T19:50:00Z\",\n \"text\": \"bruno sweep highlight\",\n \"chapter\": 1,\n \"page\": 1,\n \"pos0\": \"1\",\n \"pos1\": \"9\",\n \"color\": \"#ffff00\"\n }\n ]\n}"
headers:
- key: Content-Type
value: application/json
method: POST method: POST
url: '{{base_url}}/api/sync/koreader/bookmarks' url: '{{base_url}}/api/sync/koreader/bookmarks'
auth: auth:
@@ -3,8 +3,8 @@ docs: "## Sync KOReader Notes\n\nSynchronizes user notes separately from highlig
\ via \"Add note\" option\n- Edited in note editor\n- Shown in \"Notes\" panel\n- Can be organized by chapter\n- Export functionality available\n- Search support\n\n**Advanced Features:**\n- **Markdown**: Bold, italic, lists (some versions)\n- **Tags**: User-defined tags (via plugins)\n- **Links**: Internal/external links\n- **Formatting**: Rich text in newer versions\n\n**Use Cases:**\n- Study notes\n- Personal reflections\n- Academic annotations\n- Research insights\n- Book club discussion prep\n- Knowledge management" \ via \"Add note\" option\n- Edited in note editor\n- Shown in \"Notes\" panel\n- Can be organized by chapter\n- Export functionality available\n- Search support\n\n**Advanced Features:**\n- **Markdown**: Bold, italic, lists (some versions)\n- **Tags**: User-defined tags (via plugins)\n- **Links**: Internal/external links\n- **Formatting**: Rich text in newer versions\n\n**Use Cases:**\n- Study notes\n- Personal reflections\n- Academic annotations\n- Research insights\n- Book club discussion prep\n- Knowledge management"
http: http:
body: body:
jsonBody: "{\n \"book_uuid\": \"{{media_item_id}}\",\n \"notes\": [\n {\n \"datetime\": \"2026-01-30T19:52:00Z\",\n \"notes\": \"A personal note about this chapter\",\n \"chapter\": 4,\n \"page\": 57,\n \"pos0\": \"3\"\n }\n ]\n}"
type: json type: json
data: "{\n \"book_uuid\": \"{{media_item_id}}\",\n \"notes\": [\n {\n \"datetime\": \"2026-01-30T19:52:00Z\",\n \"notes\": \"A personal note about this chapter\",\n \"chapter\": 4,\n \"page\": 57,\n \"pos0\": \"3\"\n }\n ]\n}"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
@@ -1,8 +1,8 @@
docs: "## KOReader Sync Progress - SHA-256 Only\n\n Synchronizes reading progress using only SHA-256 hash for book identification (when UUID is not available).\n\n **Method:** POST\n\n **Endpoint:** /api/v1/koreader/sync/progress\n\n **Authentication:** Bearer token (KOReader device token)\n\n **Request Body:**\n - `sync_mode` (string): Sync mode (immediate, deferred)\n - `books` (array): Array of book progress objects\n - `sha256` (string): SHA-256 hash of book file\n - `file_path` (string): Path to book file\n - `percentage` (number): Progress percentage\n - `page` (number): Current page\n - `total_pages` (number): Total pages\n\n **Response:**\n - `sync_status` (string): Sync status\n - `books_synced` (number): Number of books synced\n\n **Status Codes:**\n - 200: Success\n - 202: Accepted\n - 401: Unauthorized\n - 500: Internal server error\n\n **Note:** Use this when book UUID is not available, falling back to SHA-256 hash for identification." docs: "## KOReader Sync Progress - SHA-256 Only\n\n Synchronizes reading progress using only SHA-256 hash for book identification (when UUID is not available).\n\n **Method:** POST\n\n **Endpoint:** /api/v1/koreader/sync/progress\n\n **Authentication:** Bearer token (KOReader device token)\n\n **Request Body:**\n - `sync_mode` (string): Sync mode (immediate, deferred)\n - `books` (array): Array of book progress objects\n - `sha256` (string): SHA-256 hash of book file\n - `file_path` (string): Path to book file\n - `percentage` (number): Progress percentage\n - `page` (number): Current page\n - `total_pages` (number): Total pages\n\n **Response:**\n - `sync_status` (string): Sync status\n - `books_synced` (number): Number of books synced\n\n **Status Codes:**\n - 200: Success\n - 202: Accepted\n - 401: Unauthorized\n - 500: Internal server error\n\n **Note:** Use this when book UUID is not available, falling back to SHA-256 hash for identification."
http: http:
body: body:
jsonBody: "{\n \"sync_mode\": \"immediate\",\n \"books\": [\n {\n \"sha256\": \"{{book_sha256"
type: json type: json
data: "{\n \"sync_mode\": \"immediate\",\n \"books\": [\n {\n \"sha256\": \"{{book_sha256}}\",\n \"percentage\": 0.5\n }\n ]\n}"
headers: [] headers: []
method: POST method: POST
url: '{{base_url}}/api/sync/koreader/progress' url: '{{base_url}}/api/sync/koreader/progress'
@@ -2,8 +2,8 @@ docs: "## KOReader Sync Progress - SHA-256\n\n Synchronizes reading progress fr
\ (number): Number of books synced\n\n **Status Codes:**\n - 200: Success\n - 202: Accepted - processing\n - 401: Unauthorized\n - 500: Internal server error" \ (number): Number of books synced\n\n **Status Codes:**\n - 200: Success\n - 202: Accepted - processing\n - 401: Unauthorized\n - 500: Internal server error"
http: http:
body: body:
jsonBody: "{\n \"sync_mode\": \"immediate\",\n \"books\": [\n {\n \"uuid\": \"{{book_uuid"
type: json type: json
data: "{\n \"sync_mode\": \"immediate\",\n \"books\": [\n {\n \"sha256\": \"{{book_sha256}}\",\n \"percentage\": 0.5\n }\n ]\n}"
headers: [] headers: []
method: POST method: POST
url: '{{base_url}}/api/sync/koreader/progress' url: '{{base_url}}/api/sync/koreader/progress'
@@ -2,28 +2,8 @@ docs: "## Sync KOReader Progress - Multiple Books\n\nSynchronizes reading progre
\ Batch Behavior:**\n- Triggered on device wake from sleep\n- Occurs during \"Sync now\" operation\n- Runs on WiFi connection\n- Limited to 100 books per request\n- Automatic retry on failed books\n\n**Performance:**\n- Small batch (2-10 books): 200-500ms\n- Medium batch (10-50 books): 500ms-2s\n- Large batch (50-100 books): 2-5s\n- Timeout: 30 seconds\n\n**Use Cases:**\n- Device initialization sync\n- Periodic background sync\n- Post-offline catch-up sync\n- Library-wide progress update\n- Before firmware update" \ Batch Behavior:**\n- Triggered on device wake from sleep\n- Occurs during \"Sync now\" operation\n- Runs on WiFi connection\n- Limited to 100 books per request\n- Automatic retry on failed books\n\n**Performance:**\n- Small batch (2-10 books): 200-500ms\n- Medium batch (10-50 books): 500ms-2s\n- Large batch (50-100 books): 2-5s\n- Timeout: 30 seconds\n\n**Use Cases:**\n- Device initialization sync\n- Periodic background sync\n- Post-offline catch-up sync\n- Library-wide progress update\n- Before firmware update"
http: http:
body: body:
json:
books:
- authors:
- Author One
chapter: 1
epubcfi: epubcfi(/6/4/2:10)
last_read: '2026-01-30T19:00:00Z'
percentage: 0.25
progress: 0.25
title: Book One
uuid: '{{media_item_id}}'
- authors:
- Author Two
chapter: 8
epubcfi: epubcfi(/6/4/2:50)
last_read: '2026-01-30T20:00:00Z'
percentage: 0.75
progress: 0.75
title: Book Two
uuid: '{{media_item_id}}'
library_id: optional-library-uuid
type: json type: json
data: "{\n \"books\": [\n {\n \"authors\": [\n \"Author One\"\n ],\n \"chapter\": 1,\n \"epubcfi\": \"epubcfi(/6/4/2:10)\",\n \"last_read\": \"2026-01-30T19:00:00Z\",\n \"percentage\": 0.25,\n \"progress\": 0.25,\n \"title\": \"Book One\",\n \"uuid\": \"{{media_item_id}}\"\n },\n {\n \"authors\": [\n \"Author Two\"\n ],\n \"chapter\": 8,\n \"epubcfi\": \"epubcfi(/6/4/2:50)\",\n \"last_read\": \"2026-01-30T20:00:00Z\",\n \"percentage\": 0.75,\n \"progress\": 0.75,\n \"title\": \"Book Two\",\n \"uuid\": \"{{media_item_id}}\"\n }\n ],\n \"library_id\": \"optional-library-uuid\"\n}"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
@@ -3,20 +3,8 @@ docs: "## Sync KOReader Progress - Single Book\n\nSynchronizes reading progress
- Real-time page turn sync\n- Progress backup\n- Cross-device continuity\n- Reading time tracking\n- Chapter completion detection" - Real-time page turn sync\n- Progress backup\n- Cross-device continuity\n- Reading time tracking\n- Chapter completion detection"
http: http:
body: body:
json:
books:
- authors:
- Author Name
chapter: 3
character: 15432
epubcfi: epubcfi(/6/4/2:15)
last_read: '2026-01-30T20:00:00Z'
percentage: 0.45
progress: 0.45
title: Book Title
uuid: '{{media_item_id}}'
library_id: optional-library-uuid
type: json type: json
data: "{\n \"books\": [\n {\n \"authors\": [\n \"Author Name\"\n ],\n \"chapter\": 3,\n \"character\": 15432,\n \"epubcfi\": \"epubcfi(/6/4/2:15)\",\n \"last_read\": \"2026-01-30T20:00:00Z\",\n \"percentage\": 0.45,\n \"progress\": 0.45,\n \"title\": \"Book Title\",\n \"uuid\": \"{{media_item_id}}\"\n }\n ],\n \"library_id\": \"optional-library-uuid\"\n}"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
@@ -3,26 +3,8 @@ docs: "## Sync Progress with Bookmarks\n\nSynchronizes reading progress with boo
- Searchable by content\n\n**Use Cases:**\n- Study and research\n- Content review\n- Passage tracking\n- Quick navigation\n- Cross-device bookmark access" - Searchable by content\n\n**Use Cases:**\n- Study and research\n- Content review\n- Passage tracking\n- Quick navigation\n- Cross-device bookmark access"
http: http:
body: body:
json:
books:
- authors:
- Author Name
bookmarks:
- chapter: 3
datetime: '2026-01-30T19:55:00Z'
notes: highlighted text
page: 45
pos0: epubcfi(/6/4/2:15)
pos1: epubcfi(/6/4/2:20)
text: highlighted text excerpt
type: highlight
last_read: '2026-01-30T20:00:00Z'
percentage: 0.45
progress: 0.45
title: Book Title
uuid: '{{media_item_id}}'
library_id: optional-library-uuid
type: json type: json
data: "{\n \"books\": [\n {\n \"authors\": [\n \"Author Name\"\n ],\n \"bookmarks\": [\n {\n \"chapter\": 3,\n \"datetime\": \"2026-01-30T19:55:00Z\",\n \"notes\": \"highlighted text\",\n \"page\": 45,\n \"pos0\": \"epubcfi(/6/4/2:15)\",\n \"pos1\": \"epubcfi(/6/4/2:20)\",\n \"text\": \"highlighted text excerpt\",\n \"type\": \"highlight\"\n }\n ],\n \"last_read\": \"2026-01-30T20:00:00Z\",\n \"percentage\": 0.45,\n \"progress\": 0.45,\n \"title\": \"Book Title\",\n \"uuid\": \"{{media_item_id}}\"\n }\n ],\n \"library_id\": \"optional-library-uuid\"\n}"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
@@ -3,29 +3,8 @@ docs: "## Sync Progress with Highlights and Notes\n\nSynchronizes reading progre
\ passages\n- Blue: Key concepts\n- Red: Critical information\n- Orange: Interesting quotes\n- Custom colors available\n\n**Sync Advantages:**\n- Separation allows granular control\n- Highlights sync without notes\n- Notes sync independently\n- Better conflict resolution\n- Efficient for large annotation sets\n\n**Use Cases:**\n- Academic research\n- Study groups\n- Content analysis\n- Personal knowledge management\n- Sharing insights" \ passages\n- Blue: Key concepts\n- Red: Critical information\n- Orange: Interesting quotes\n- Custom colors available\n\n**Sync Advantages:**\n- Separation allows granular control\n- Highlights sync without notes\n- Notes sync independently\n- Better conflict resolution\n- Efficient for large annotation sets\n\n**Use Cases:**\n- Academic research\n- Study groups\n- Content analysis\n- Personal knowledge management\n- Sharing insights"
http: http:
body: body:
json:
books:
- authors:
- Author Name
highlights:
- chapter: 4
datetime: '2026-01-30T19:50:00Z'
page_end: 79
page_start: 78
pos0: epubcfi(/6/4/2:20)
pos1: epubcfi(/6/4/2:30)
text: Important passage
last_read: '2026-01-30T20:00:00Z'
notes:
- chapter: 4
datetime: '2026-01-30T19:52:00Z'
text: My note about this chapter
percentage: 0.6
progress: 0.6
title: Book Title
uuid: '{{media_item_id}}'
library_id: optional-library-uuid
type: json type: json
data: "{\n \"books\": [\n {\n \"authors\": [\n \"Author Name\"\n ],\n \"highlights\": [\n {\n \"chapter\": 4,\n \"datetime\": \"2026-01-30T19:50:00Z\",\n \"page_end\": 79,\n \"page_start\": 78,\n \"pos0\": \"epubcfi(/6/4/2:20)\",\n \"pos1\": \"epubcfi(/6/4/2:30)\",\n \"text\": \"Important passage\"\n }\n ],\n \"last_read\": \"2026-01-30T20:00:00Z\",\n \"notes\": [\n {\n \"chapter\": 4,\n \"datetime\": \"2026-01-30T19:52:00Z\",\n \"text\": \"My note about this chapter\"\n }\n ],\n \"percentage\": 0.6,\n \"progress\": 0.6,\n \"title\": \"Book Title\",\n \"uuid\": \"{{media_item_id}}\"\n }\n ],\n \"library_id\": \"optional-library-uuid\"\n}"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
@@ -2,8 +2,8 @@ docs: "## KOReader Sync Progress\n\n Synchronizes reading progress from a KORea
\ model identifier\n\n **Response:**\n - `sync_status` (string): Sync status (accepted, processing)\n - `books_synced` (number): Number of books synced\n\n **Status Codes:**\n - 202: Accepted - sync queued\n - 401: Unauthorized\n - 500: Internal server error" \ model identifier\n\n **Response:**\n - `sync_status` (string): Sync status (accepted, processing)\n - `books_synced` (number): Number of books synced\n\n **Status Codes:**\n - 202: Accepted - sync queued\n - 401: Unauthorized\n - 500: Internal server error"
http: http:
body: body:
jsonBody: "{\n \"library_id\": null,\n \"books\": [\n {\n \"uuid\": \"{{book_uuid"
type: json type: json
data: "{\n \"sync_mode\": \"immediate\",\n \"books\": [\n {\n \"uuid\": \"{{media_item_id}}\",\n \"percentage\": 0.45,\n \"chapter\": 3\n }\n ]\n}"
headers: [] headers: []
method: POST method: POST
url: '{{base_url}}/api/sync/koreader/progress' url: '{{base_url}}/api/sync/koreader/progress'
@@ -8,16 +8,8 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"media_item_ids\": [\n \"{{bookUUID1" data: "{\n \"media_item_ids\": [\n \"{{bookUUID1"
headers: headers:
- key: Authorization - key: Authorization
value: Bearer {{userToken value: Bearer {{userToken
docs: "## Add Books to Kobo Shelf\n\n Add one or more books to a Kobo device shelf. Manages which books should be synced to a specific Kobo device. Supports multiple shelves for organization.\n\n **Method:** POST\n\n **Endpoint:** /api/devices/{deviceID"
docs: |-
## Add Books to Kobo Shelf
Add one or more books to a Kobo device shelf. Manages which books should be synced to a specific Kobo device. Supports multiple shelves for organization.
**Method:** POST
**Endpoint:** /api/devices/{deviceID
@@ -2,51 +2,15 @@ info:
name: Check Pending Registration name: Check Pending Registration
type: http type: http
seq: 4 seq: 4
http: http:
method: POST method: POST
url: '{{base_url}}/api/devices/register/status' url: '{{base_url}}/api/devices/register/status'
auth: none auth: none
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
body: body:
type: json type: json
json: data: "{\n \"registration_id\": \"registration-uuid-here\"\n}"
registration_id: registration-uuid-here docs: "## Check Registration Status\n\nChecks the current status of a device registration request.\n\n**Method:** POST\n\n**Endpoint:** /api/devices/register/status\n\n**Authentication:** None\n\n**Request Body:**\n- `registration_id` (string): UUID received from registration request\n\n**Response:**\n- `registration_id` (string): The registration UUID\n- `status` (string): Current status\n - `pending`: Awaiting user approval\n - `approved`: Registration approved, device ready\n - `rejected`: Registration rejected by user\n - `expired`: Registration expired (not approved in time)\n- `device_name` (string): Name of the device\n- `device_type` (string): Type of device\n- `created_at` (string): Registration timestamp\n- `updated_at` (string): Last status update timestamp\n\n**Status Codes:**\n- 200: Status retrieved successfully\n- 404: Registration ID not found\n- 400: Invalid registration ID format\n\n**Polling Recommendations:**\n- Poll every 5-10 seconds while status is `pending`\n\
- Stop polling when status changes to `approved`, `rejected`, or `expired`\n- Use exponential backoff for mobile devices to save battery"
docs: |-
## Check Registration Status
Checks the current status of a device registration request.
**Method:** POST
**Endpoint:** /api/devices/register/status
**Authentication:** None
**Request Body:**
- `registration_id` (string): UUID received from registration request
**Response:**
- `registration_id` (string): The registration UUID
- `status` (string): Current status
- `pending`: Awaiting user approval
- `approved`: Registration approved, device ready
- `rejected`: Registration rejected by user
- `expired`: Registration expired (not approved in time)
- `device_name` (string): Name of the device
- `device_type` (string): Type of device
- `created_at` (string): Registration timestamp
- `updated_at` (string): Last status update timestamp
**Status Codes:**
- 200: Status retrieved successfully
- 404: Registration ID not found
- 400: Invalid registration ID format
**Polling Recommendations:**
- Poll every 5-10 seconds while status is `pending`
- Stop polling when status changes to `approved`, `rejected`, or `expired`
- Use exponential backoff for mobile devices to save battery
@@ -8,30 +8,5 @@ http:
auth: none auth: none
body: body:
type: json type: json
jsonBody: "{\n \"registration_id\": \"{{registrationId}}" data: "{\n \"registration_id\": \"{{registrationId}}"
docs: "## Check Registration Status\n\n Checks the current status of a device registration request.\n\n **Method:** POST\n\n **Endpoint:** /api/devices/register/status\n\n **Authentication:** None\n\n **Request Body:**\n - `registration_id` (string, required): Registration request ID\n\n **Response:**\n - `status` (string): Registration status (pending, approved, rejected, expired)\n - `device_name` (string): Device name\n - `device_type` (string): Device type\n - `created_at` (string): Request timestamp\n - `expires_at` (string): Expiration timestamp\n\n **Status Codes:**\n - 200: Success\n - 404: Registration ID not found\n - 410: Registration expired"
docs: |-
## Check Registration Status
Checks the current status of a device registration request.
**Method:** POST
**Endpoint:** /api/devices/register/status
**Authentication:** None
**Request Body:**
- `registration_id` (string, required): Registration request ID
**Response:**
- `status` (string): Registration status (pending, approved, rejected, expired)
- `device_name` (string): Device name
- `device_type` (string): Device type
- `created_at` (string): Request timestamp
- `expires_at` (string): Expiration timestamp
**Status Codes:**
- 200: Success
- 404: Registration ID not found
- 410: Registration expired
@@ -8,48 +8,12 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"file_path\": \"/mnt/sd/books/my-book.kepub.epub\",\n \"\ data: "{\n \"file_path\": \"/mnt/sd/books/my-book.kepub.epub\",\n \"media_item_id\": \"{{media_item_id"
media_item_id\": \"{{media_item_id"
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
runtime: runtime:
scripts: scripts:
- type: tests - type: tests
code: "test_create_device_file_alias_success(status, headers, body) {\n if\ code: "test_create_device_file_alias_success(status, headers, body) {\n if (status !== 201 && status !== 200) {\n throw new Error(\"Expected status 201 or 200, got \" + status);"
\ (status !== 201 && status !== 200) {\n throw new Error(\"Expected status\ docs: "## Create Device File Alias\n\n Creates a new file alias for a device. File aliases map device-specific file paths to media items.\n\n **Method:** POST\n\n **Endpoint:** /api/devices/:id/file-aliases\n\n **Authentication:** Required (Bearer token)\n\n **Path Parameters:**\n - `id` (string, required): Device UUID\n\n **Request Body:**\n - `file_path` (string, required): Device-specific file path\n - `media_item_id` (string, required): Media item UUID to link to\n\n **Response:** Created file alias object\n - `id` (string): Alias UUID\n - `device_id` (string): Device UUID\n - `file_path` (string): Device-specific file path\n - `media_item_id` (string): Associated media item UUID\n - `created_at` (string): Creation timestamp\n - `updated_at` (string): Last update timestamp\n\n **Status Codes:**\n - 201: Created\n - 200: Success\n - 400: Invalid request body\n - 401: Unauthorized\n - 404: Device not found\n - 500: Internal server error"
\ 201 or 200, got \" + status);"
docs: |-
## Create Device File Alias
Creates a new file alias for a device. File aliases map device-specific file paths to media items.
**Method:** POST
**Endpoint:** /api/devices/:id/file-aliases
**Authentication:** Required (Bearer token)
**Path Parameters:**
- `id` (string, required): Device UUID
**Request Body:**
- `file_path` (string, required): Device-specific file path
- `media_item_id` (string, required): Media item UUID to link to
**Response:** Created file alias object
- `id` (string): Alias UUID
- `device_id` (string): Device UUID
- `file_path` (string): Device-specific file path
- `media_item_id` (string): Associated media item UUID
- `created_at` (string): Creation timestamp
- `updated_at` (string): Last update timestamp
**Status Codes:**
- 201: Created
- 200: Success
- 400: Invalid request body
- 401: Unauthorized
- 404: Device not found
- 500: Internal server error
+31 -11
View File
@@ -2,57 +2,77 @@ info:
name: Disable Device Sync name: Disable Device Sync
type: http type: http
seq: 8 seq: 8
http: http:
method: PUT method: PUT
url: '{{base_url}}/api/devices/{{device_id}}' url: '{{base_url}}/api/devices/{{device_id}}'
auth: inherit auth: inherit
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
body: body:
type: json type: json
json: data: "{\n \"device_name\": \"My Kobo Clara\",\n \"sync_enabled\": false,\n \"auto_sync\": false,\n \"sync_frequency_minutes\": 30\n}"
device_name: My Kobo Clara docs: '## Disable Device Sync
sync_enabled: false
auto_sync: false
sync_frequency_minutes: 30
docs: |-
## Disable Device Sync
Disables synchronization for a specific device. Disables synchronization for a specific device.
**Method:** PUT **Method:** PUT
**Endpoint:** /api/devices/:device_id **Endpoint:** /api/devices/:device_id
**Authentication:** Required (Bearer token) **Authentication:** Required (Bearer token)
**Path Parameters:** **Path Parameters:**
- `device_id` (string): UUID of the device - `device_id` (string): UUID of the device
**Request Body:** **Request Body:**
- `device_name` (string, optional): Device name - `device_name` (string, optional): Device name
- `sync_enabled` (boolean): Must be `false` - `sync_enabled` (boolean): Must be `false`
- `auto_sync` (boolean): Should be `false` - `auto_sync` (boolean): Should be `false`
- `sync_frequency_minutes` (integer, optional): Any value (sync disabled) - `sync_frequency_minutes` (integer, optional): Any value (sync disabled)
**Response:** **Response:**
- `id` (string): Device UUID - `id` (string): Device UUID
- `device_name` (string): Device name - `device_name` (string): Device name
- `sync_enabled` (boolean): `false` - `sync_enabled` (boolean): `false`
- `auto_sync` (boolean): `false` - `auto_sync` (boolean): `false`
- `message` (string): Confirmation message - `message` (string): Confirmation message
**Status Codes:** **Status Codes:**
- 200: Sync disabled successfully - 200: Sync disabled successfully
- 401: Unauthorized - 401: Unauthorized
- 403: Forbidden - 403: Forbidden
- 404: Device not found - 404: Device not found
**Use Cases:** **Use Cases:**
- Temporarily disable sync for troubleshooting - Temporarily disable sync for troubleshooting
- Stop sync for a lost or stolen device - Stop sync for a lost or stolen device
- Disable sync before selling or giving away device - Disable sync before selling or giving away device
- Prevent data usage on limited connections
- Prevent data usage on limited connections'
@@ -8,32 +8,5 @@ http:
auth: none auth: none
body: body:
type: json type: json
jsonBody: "{\n \"device_name\": \"My Kindle Paperwhite\",\n \"device_type\"\ data: "{\n \"device_name\": \"My Kindle Paperwhite\",\n \"device_type\": \"koreader\",\n \"device_identifier\": \"kindle-pw5-hardware-id-12345\""
: \"koreader\",\n \"device_identifier\": \"kindle-pw5-hardware-id-12345\"" docs: "## Initiate Device Registration\n\n Initiates a new device registration request that requires admin approval.\n\n **Method:** POST\n\n **Endpoint:** /api/devices/register\n\n **Authentication:** None (open endpoint)\n\n **Request Body:**\n - `device_name` (string, required): Human-readable device name (max 100 chars)\n - `device_type` (string, required): Device type (kobo, koreader, kindle, etc.)\n - `device_identifier` (string, required): Unique hardware ID (max 200 chars)\n\n **Response:**\n - `registration_id` (string): Unique registration request ID\n - `status` (string): Initial status (pending)\n - `expires_at` (string): Expiration timestamp (usually 24 hours)\n - `message` (string): Informational message\n\n **Status Codes:**\n - 201: Registration initiated\n - 400: Invalid request\n - 409: Device already registered"
docs: |-
## Initiate Device Registration
Initiates a new device registration request that requires admin approval.
**Method:** POST
**Endpoint:** /api/devices/register
**Authentication:** None (open endpoint)
**Request Body:**
- `device_name` (string, required): Human-readable device name (max 100 chars)
- `device_type` (string, required): Device type (kobo, koreader, kindle, etc.)
- `device_identifier` (string, required): Unique hardware ID (max 200 chars)
**Response:**
- `registration_id` (string): Unique registration request ID
- `status` (string): Initial status (pending)
- `expires_at` (string): Expiration timestamp (usually 24 hours)
- `message` (string): Informational message
**Status Codes:**
- 201: Registration initiated
- 400: Invalid request
- 409: Device already registered
+24 -10
View File
@@ -2,48 +2,62 @@ info:
name: Register Device name: Register Device
type: http type: http
seq: 1 seq: 1
http: http:
method: POST method: POST
url: '{{base_url}}/api/devices/register' url: '{{base_url}}/api/devices/register'
auth: none auth: none
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
body: body:
type: json type: json
json: data: "{\n \"device_name\": \"My Kobo Clara\",\n \"device_type\": \"kobo\",\n \"device_identifier\": \"N1234567890123\"\n}"
device_name: My Kobo Clara docs: '## Register Device
device_type: kobo
device_identifier: N1234567890123
docs: |-
## Register Device
Initiates device registration by sending device information to the server. Initiates device registration by sending device information to the server.
**Method:** POST **Method:** POST
**Endpoint:** /api/devices/register **Endpoint:** /api/devices/register
**Authentication:** None (public endpoint) **Authentication:** None (public endpoint)
**Request Body:** **Request Body:**
- `device_name` (string): Human-readable name for the device - `device_name` (string): Human-readable name for the device
- `device_type` (string): Type of device - `kobo`, `koreader`, or `web` - `device_type` (string): Type of device - `kobo`, `koreader`, or `web`
- `device_identifier` (string): Unique device identifier (serial number or other ID) - `device_identifier` (string): Unique device identifier (serial number or other ID)
**Response:** **Response:**
- `registration_id` (string): UUID for tracking registration status - `registration_id` (string): UUID for tracking registration status
- `status` (string): Registration status - `pending`, `approved`, or `rejected` - `status` (string): Registration status - `pending`, `approved`, or `rejected`
- `message` (string): Status message - `message` (string): Status message
**Status Codes:** **Status Codes:**
- 201: Registration initiated successfully - 201: Registration initiated successfully
- 400: Invalid request data - 400: Invalid request data
- 409: Device already registered - 409: Device already registered
**Notes:** **Notes:**
- Device registration requires user approval before activation - Device registration requires user approval before activation
- Registration ID should be stored for status checking - Registration ID should be stored for status checking
- Device identifier must be unique per device
- Device identifier must be unique per device'
@@ -2,47 +2,60 @@ info:
name: Register KOReader Device name: Register KOReader Device
type: http type: http
seq: 2 seq: 2
http: http:
method: POST method: POST
url: '{{base_url}}/api/devices/register' url: '{{base_url}}/api/devices/register'
auth: none auth: none
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
body: body:
type: json type: json
json: data: "{\n \"device_name\": \"My Kindle Paperwhite\",\n \"device_type\": \"koreader\",\n \"device_identifier\": \"G090GP123456789\"\n}"
device_name: My Kindle Paperwhite docs: '## Register KOReader Device
device_type: koreader
device_identifier: G090GP123456789
docs: |-
## Register KOReader Device
Registers a KOReader device (typically Kindle devices running KOReader software). Registers a KOReader device (typically Kindle devices running KOReader software).
**Method:** POST **Method:** POST
**Endpoint:** /api/devices/register **Endpoint:** /api/devices/register
**Authentication:** None (public endpoint) **Authentication:** None (public endpoint)
**Request Body:** **Request Body:**
- `device_name` (string): Human-readable name for the device - `device_name` (string): Human-readable name for the device
- `device_type` (string): Must be `koreader` - `device_type` (string): Must be `koreader`
- `device_identifier` (string): Kindle serial number or unique device ID - `device_identifier` (string): Kindle serial number or unique device ID
**Response:** **Response:**
- `registration_id` (string): UUID for tracking registration status - `registration_id` (string): UUID for tracking registration status
- `status` (string): Registration status - `pending` - `status` (string): Registration status - `pending`
- `message` (string): Confirmation message - `message` (string): Confirmation message
**Status Codes:** **Status Codes:**
- 201: Registration initiated - 201: Registration initiated
- 400: Invalid device type or data - 400: Invalid device type or data
**KOReader Notes:** **KOReader Notes:**
- KOReader runs on Kindle, Kobo, PocketBook, and Android devices - KOReader runs on Kindle, Kobo, PocketBook, and Android devices
- Device identifier is typically the device serial number - Device identifier is typically the device serial number
- Supports bookmarks, progress, highlights, and annotations sync
- Supports bookmarks, progress, highlights, and annotations sync'
+24 -10
View File
@@ -2,48 +2,62 @@ info:
name: Register Web Device name: Register Web Device
type: http type: http
seq: 3 seq: 3
http: http:
method: POST method: POST
url: '{{base_url}}/api/devices/register' url: '{{base_url}}/api/devices/register'
auth: none auth: none
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
body: body:
type: json type: json
json: data: "{\n \"device_name\": \"Chrome Browser\",\n \"device_type\": \"web\",\n \"device_identifier\": \"web-client-abc123\"\n}"
device_name: Chrome Browser docs: '## Register Web Device
device_type: web
device_identifier: web-client-abc123
docs: |-
## Register Web Device
Registers a web browser client for reading progress sync. Registers a web browser client for reading progress sync.
**Method:** POST **Method:** POST
**Endpoint:** /api/devices/register **Endpoint:** /api/devices/register
**Authentication:** None (public endpoint) **Authentication:** None (public endpoint)
**Request Body:** **Request Body:**
- `device_name` (string): Browser identification (e.g., "Chrome Browser") - `device_name` (string): Browser identification (e.g., "Chrome Browser")
- `device_type` (string): Must be `web` - `device_type` (string): Must be `web`
- `device_identifier` (string): Unique browser/client identifier - `device_identifier` (string): Unique browser/client identifier
**Response:** **Response:**
- `registration_id` (string): UUID for tracking - `registration_id` (string): UUID for tracking
- `status` (string): `pending` or `auto-approved` - `status` (string): `pending` or `auto-approved`
- `message` (string): Status message - `message` (string): Status message
**Status Codes:** **Status Codes:**
- 201: Registration successful - 201: Registration successful
- 400: Invalid request - 400: Invalid request
**Web Device Notes:** **Web Device Notes:**
- Web devices use localStorage for device identification - Web devices use localStorage for device identification
- May be auto-approved without admin intervention - May be auto-approved without admin intervention
- Used for browser-based reading progress tracking - Used for browser-based reading progress tracking
- Supports manual progress updates, highlights, and notes
- Supports manual progress updates, highlights, and notes'
@@ -2,60 +2,83 @@ info:
name: Update Device Settings name: Update Device Settings
type: http type: http
seq: 7 seq: 7
http: http:
method: PUT method: PUT
url: '{{base_url}}/api/devices/{{device_id}}' url: '{{base_url}}/api/devices/{{device_id}}'
auth: inherit auth: inherit
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
body: body:
type: json type: json
json: data: "{\n \"device_name\": \"Updated Device Name\",\n \"sync_enabled\": true,\n \"auto_sync\": true,\n \"sync_frequency_minutes\": 10\n}"
device_name: Updated Device Name docs: '## Update Device Settings
sync_enabled: true
auto_sync: true
sync_frequency_minutes: 10
docs: |-
## Update Device Settings
Updates device configuration and sync settings. Updates device configuration and sync settings.
**Method:** PUT **Method:** PUT
**Endpoint:** /api/devices/:device_id **Endpoint:** /api/devices/:device_id
**Authentication:** Required (Bearer token) **Authentication:** Required (Bearer token)
**Path Parameters:** **Path Parameters:**
- `device_id` (string): UUID of the device - `device_id` (string): UUID of the device
**Request Body:** **Request Body:**
- `device_name` (string, optional): New device name - `device_name` (string, optional): New device name
- `sync_enabled` (boolean, optional): Enable/disable sync - `sync_enabled` (boolean, optional): Enable/disable sync
- `auto_sync` (boolean, optional): Enable automatic sync - `auto_sync` (boolean, optional): Enable automatic sync
- `sync_frequency_minutes` (integer, optional): Sync interval (5-1440 minutes) - `sync_frequency_minutes` (integer, optional): Sync interval (5-1440 minutes)
**Response:** **Response:**
- `id` (string): Device UUID - `id` (string): Device UUID
- `device_name` (string): Updated device name - `device_name` (string): Updated device name
- `sync_enabled` (boolean): Updated sync status - `sync_enabled` (boolean): Updated sync status
- `auto_sync` (boolean): Updated auto-sync setting - `auto_sync` (boolean): Updated auto-sync setting
- `sync_frequency_minutes` (integer): Updated sync interval - `sync_frequency_minutes` (integer): Updated sync interval
- `updated_at` (string): Update timestamp - `updated_at` (string): Update timestamp
**Status Codes:** **Status Codes:**
- 200: Settings updated successfully - 200: Settings updated successfully
- 400: Invalid sync frequency or data - 400: Invalid sync frequency or data
- 401: Unauthorized - 401: Unauthorized
- 403: Forbidden (device belongs to different user) - 403: Forbidden (device belongs to different user)
- 404: Device not found - 404: Device not found
**Sync Frequency Guidelines:** **Sync Frequency Guidelines:**
- Mobile devices: 10-30 minutes - Mobile devices: 10-30 minutes
- E-readers on WiFi: 15-60 minutes - E-readers on WiFi: 15-60 minutes
- Web clients: 5-15 minutes - Web clients: 5-15 minutes
- Minimum: 5 minutes - Minimum: 5 minutes
- Maximum: 1440 minutes (24 hours)
- Maximum: 1440 minutes (24 hours)'
+2 -32
View File
@@ -8,35 +8,5 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"device_name\": \"My Updated Kindle\",\n \"sync_enabled\"\ data: "{\n \"device_name\": \"My Updated Kindle\",\n \"sync_enabled\": true,\n \"auto_sync\": true,\n \"sync_frequency_minutes\": 10"
: true,\n \"auto_sync\": true,\n \"sync_frequency_minutes\": 10" docs: "## Update Device\n\n Updates device settings and synchronization preferences.\n\n **Method:** PUT\n\n **Endpoint:** /api/devices/{device_id}\n\n **Authentication:** Required (Bearer token, admin or device owner)\n\n **Path Parameters:**\n - `device_id` (string): Device ID\n\n **Request Body:**\n - `device_name` (string, optional): New device name (max 100 chars)\n - `sync_enabled` (boolean, optional): Enable/disable synchronization\n - `auto_sync` (boolean, optional): Enable automatic synchronization\n - `sync_frequency_minutes` (integer, optional): Sync frequency in minutes (5-1440)\n\n **Response:**\n - Updated device object\n\n **Status Codes:**\n - 200: Success\n - 400: Invalid request\n - 401: Unauthorized\n - 403: Forbidden (not device owner)\n - 404: Device not found"
docs: |-
## Update Device
Updates device settings and synchronization preferences.
**Method:** PUT
**Endpoint:** /api/devices/{device_id}
**Authentication:** Required (Bearer token, admin or device owner)
**Path Parameters:**
- `device_id` (string): Device ID
**Request Body:**
- `device_name` (string, optional): New device name (max 100 chars)
- `sync_enabled` (boolean, optional): Enable/disable synchronization
- `auto_sync` (boolean, optional): Enable automatic synchronization
- `sync_frequency_minutes` (integer, optional): Sync frequency in minutes (5-1440)
**Response:**
- Updated device object
**Status Codes:**
- 200: Success
- 400: Invalid request
- 401: Unauthorized
- 403: Forbidden (not device owner)
- 404: Device not found
@@ -2,65 +2,92 @@ info:
name: Update Sync Frequency name: Update Sync Frequency
type: http type: http
seq: 9 seq: 9
http: http:
method: PUT method: PUT
url: '{{base_url}}/api/devices/{{device_id}}' url: '{{base_url}}/api/devices/{{device_id}}'
auth: inherit auth: inherit
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
body: body:
type: json type: json
json: data: "{\n \"device_name\": \"My Kobo Clara\",\n \"sync_enabled\": true,\n \"auto_sync\": true,\n \"sync_frequency_minutes\": 15\n}"
device_name: My Kobo Clara docs: '## Update Sync Frequency
sync_enabled: true
auto_sync: true
sync_frequency_minutes: 15
docs: |-
## Update Sync Frequency
Changes how often the device automatically syncs with the server. Changes how often the device automatically syncs with the server.
**Method:** PUT **Method:** PUT
**Endpoint:** /api/devices/:device_id **Endpoint:** /api/devices/:device_id
**Authentication:** Required (Bearer token) **Authentication:** Required (Bearer token)
**Path Parameters:** **Path Parameters:**
- `device_id` (string): UUID of the device - `device_id` (string): UUID of the device
**Request Body:** **Request Body:**
- `device_name` (string, optional): Device name - `device_name` (string, optional): Device name
- `sync_enabled` (boolean): Must be `true` for auto-sync - `sync_enabled` (boolean): Must be `true` for auto-sync
- `auto_sync` (boolean): Must be `true` - `auto_sync` (boolean): Must be `true`
- `sync_frequency_minutes` (integer): New sync interval (5-1440) - `sync_frequency_minutes` (integer): New sync interval (5-1440)
**Response:** **Response:**
- `id` (string): Device UUID - `id` (string): Device UUID
- `sync_enabled` (boolean): `true` - `sync_enabled` (boolean): `true`
- `auto_sync` (boolean): `true` - `auto_sync` (boolean): `true`
- `sync_frequency_minutes` (integer): Updated interval - `sync_frequency_minutes` (integer): Updated interval
- `next_sync_at` (string): Estimated next sync time - `next_sync_at` (string): Estimated next sync time
- `updated_at` (string): Update timestamp - `updated_at` (string): Update timestamp
**Status Codes:** **Status Codes:**
- 200: Frequency updated successfully - 200: Frequency updated successfully
- 400: Invalid frequency value - 400: Invalid frequency value
- 401: Unauthorized - 401: Unauthorized
- 403: Forbidden - 403: Forbidden
- 404: Device not found - 404: Device not found
**Recommended Frequencies:** **Recommended Frequencies:**
- **Active reading:** 5-10 minutes - **Active reading:** 5-10 minutes
- **Normal usage:** 15-30 minutes - **Normal usage:** 15-30 minutes
- **Occasional reading:** 30-60 minutes - **Occasional reading:** 30-60 minutes
- **Battery saving:** 60+ minutes - **Battery saving:** 60+ minutes
- **WiFi only (3G):** 30-60 minutes - **WiFi only (3G):** 30-60 minutes
**Notes:** **Notes:**
- More frequent sync = more battery usage - More frequent sync = more battery usage
- Sync only occurs when device is online - Sync only occurs when device is online
- Manual sync can be triggered anytime regardless of frequency
- Manual sync can be triggered anytime regardless of frequency'
+4
View File
@@ -40,3 +40,7 @@ variables:
name: comic_library_id name: comic_library_id
- secret: true - secret: true
name: manga_library_id name: manga_library_id
- secret: true
name: book_sha256
- secret: true
name: book_uuid
+4 -104
View File
@@ -8,107 +8,7 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"name\": \"My Sci-Fi Books\",\n \"resource_type\":\ data: "{\n \"name\": \"My Sci-Fi Books\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\",\n \"sort\": \"title ASC\",\n \"author_filter\": \"\"\n }\n}"
\ \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\"\ docs: "## Create Saved Filter\n\nCreates a new saved filter for the authenticated user.\n\n**Method:** POST\n\n**Endpoint:** /api/saved-filters\n\n**Authentication:** Required (Bearer token)\n\n**Request Body:**\n```json\n{\n \"name\": \"My Sci-Fi Books\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\",\n \"sort\": \"title ASC\",\n \"author_filter\": \"\",\n \"series_filter\": \"\"\n }\n}\n```\n\n**Required Fields:**\n- `name` (string, required): Filter name\n - Must be unique per user + resource type combination\n - Maximum 100 characters\n - Cannot be empty\n- `resource_type` (string, required): Type of resource to filter\n - Examples: \"media-items\", \"collections\", \"devices\"\n - Must match valid resource types\n- `filters` (object, required): Key-value pairs of filter criteria\n - Flexible structure - any valid filter fields\n - Serialized as JSONB in database\n\n**Common Filter Fields for media-items:**\n- `search`:\
,\n \"sort\": \"title ASC\",\n \"author_filter\": \"\"\n }\n\ \ General search term\n- `author_filter`: Filter by author name\n- `genre_filter`: Filter by genre\n- `series_filter`: Filter by series\n- `language_filter`: Filter by language\n- `year_min`: Minimum copyright year\n- `year_max`: Maximum copyright year\n- `has_cover`: Boolean for cover image\n- `sort`: Sort order (e.g., \"title ASC\", \"created_at DESC\")\n\n**Response:**\n```json\n{\n \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"name\": \"My Sci-Fi Books\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\",\n \"sort\": \"title ASC\"\n },\n \"created_at\": \"2024-03-20T12:00:00Z\",\n \"updated_at\": \"2024-03-20T12:00:00Z\"\n}\n```\n\n**Status Codes:**\n- 201: Created - Filter successfully created\n- 400: Bad Request - Invalid request body or missing required fields\n- 401: Unauthorized - Invalid or missing authentication token\n- 409: Conflict - Filter with this name already exists for this resource type\n\n**Error Response\
}" \ Examples:**\n\nDuplicate name error:\n```json\n{\n \"error\": \"filter with name 'My Sci-Fi Books' already exists for this resource type\"\n}\n```\n\n**Example Usage:**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"My Sci-Fi Books\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\",\n \"sort\": \"title ASC\"\n }\n }' \\\n \"{{base_url}}/api/saved-filters\"\n```\n\n**Validation Rules:**\n- Name must be unique per user + resource type\n- Name cannot be empty or whitespace only\n- Resource type must be valid\n- Filters object must be provided (can be empty object {})"
docs: |-
## Create Saved Filter
Creates a new saved filter for the authenticated user.
**Method:** POST
**Endpoint:** /api/saved-filters
**Authentication:** Required (Bearer token)
**Request Body:**
```json
{
"name": "My Sci-Fi Books",
"resource_type": "media-items",
"filters": {
"genre_filter": "Science Fiction",
"sort": "title ASC",
"author_filter": "",
"series_filter": ""
}
}
```
**Required Fields:**
- `name` (string, required): Filter name
- Must be unique per user + resource type combination
- Maximum 100 characters
- Cannot be empty
- `resource_type` (string, required): Type of resource to filter
- Examples: "media-items", "collections", "devices"
- Must match valid resource types
- `filters` (object, required): Key-value pairs of filter criteria
- Flexible structure - any valid filter fields
- Serialized as JSONB in database
**Common Filter Fields for media-items:**
- `search`: General search term
- `author_filter`: Filter by author name
- `genre_filter`: Filter by genre
- `series_filter`: Filter by series
- `language_filter`: Filter by language
- `year_min`: Minimum copyright year
- `year_max`: Maximum copyright year
- `has_cover`: Boolean for cover image
- `sort`: Sort order (e.g., "title ASC", "created_at DESC")
**Response:**
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "My Sci-Fi Books",
"resource_type": "media-items",
"filters": {
"genre_filter": "Science Fiction",
"sort": "title ASC"
},
"created_at": "2024-03-20T12:00:00Z",
"updated_at": "2024-03-20T12:00:00Z"
}
```
**Status Codes:**
- 201: Created - Filter successfully created
- 400: Bad Request - Invalid request body or missing required fields
- 401: Unauthorized - Invalid or missing authentication token
- 409: Conflict - Filter with this name already exists for this resource type
**Error Response Examples:**
Duplicate name error:
```json
{
"error": "filter with name 'My Sci-Fi Books' already exists for this resource type"
}
```
**Example Usage:**
```bash
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "My Sci-Fi Books",
"resource_type": "media-items",
"filters": {
"genre_filter": "Science Fiction",
"sort": "title ASC"
}
}' \
"{{base_url}}/api/saved-filters"
```
**Validation Rules:**
- Name must be unique per user + resource type
- Name cannot be empty or whitespace only
- Resource type must be valid
- Filters object must be provided (can be empty object {})
+4 -107
View File
@@ -8,110 +8,7 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"name\": \"Updated Sci-Fi Books\",\n \"resource_type\"\ data: "{\n \"name\": \"Updated Sci-Fi Books\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction Fantasy\",\n \"sort\": \"author ASC\",\n \"year_min\": \"2000\"\n }\n }"
: \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\ docs: "## Update Saved Filter\n\nUpdates an existing saved filter's name and/or filter criteria.\n\n**Method:** PUT\n\n**Endpoint:** /api/saved-filters/{filter_id}\n\n**Authentication:** Required (Bearer token)\n\n**Path Parameters:**\n- `filter_id` (string, required): UUID of the filter to update\n - Must be a valid UUID\n - Must belong to the authenticated user\n\n**Request Body:**\n```json\n{\n \"name\": \"Updated Sci-Fi Books\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction Fantasy\",\n \"sort\": \"author ASC\",\n \"year_min\": \"2000\",\n \"year_max\": \"2024\"\n }\n}\n```\n\n**Required Fields:**\n- `name` (string, required): New filter name\n - Must be unique per user + resource type (excluding current filter)\n - Maximum 100 characters\n- `resource_type` (string, required): Resource type (must match existing)\n - Cannot change resource type after creation\n- `filters` (object, required): Updated filter criteria\n\
\ Fantasy\",\n \"sort\": \"author ASC\",\n \"year_min\": \"2000\"\ \ - Replaces existing filters entirely\n\n**Response:**\n```json\n{\n \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"name\": \"Updated Sci-Fi Books\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction Fantasy\",\n \"sort\": \"author ASC\",\n \"year_min\": \"2000\",\n \"year_max\": \"2024\"\n },\n \"created_at\": \"2024-03-20T12:00:00Z\",\n \"updated_at\": \"2024-03-20T15:30:00Z\"\n}\n```\n\n**Status Codes:**\n- 200: Success - Filter updated successfully\n- 400: Bad Request - Invalid request body or filter ID\n- 401: Unauthorized - Invalid or missing authentication token\n- 404: Not Found - Filter doesn't exist or doesn't belong to user\n- 409: Conflict - New name conflicts with existing filter\n\n**Error Response Examples:**\n\nFilter not found:\n```json\n{\n \"error\": \"filter not found or access denied\"\n}\n```\n\nDuplicate name:\n```json\n{\n \"error\": \"filter with name 'Updated Sci-Fi Books' already exists\
\n }\n }" \ for this resource type\"\n}\n```\n\n**Example Usage:**\n```bash\ncurl -X PUT \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Updated Sci-Fi Books\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction Fantasy\",\n \"sort\": \"author ASC\",\n \"year_min\": \"2000\"\n }\n }' \\\n \"{{base_url}}/api/saved-filters/{{filter_id}}\"\n```\n\n**Important Notes:**\n- You can only update filters that belong to you\n- The `updated_at` timestamp is automatically updated\n- Cannot change the resource_type (it's immutable)\n- New name must not conflict with other filters you own for the same resource type\n- All filter fields are replaced entirely (partial updates not supported)"
docs: |-
## Update Saved Filter
Updates an existing saved filter's name and/or filter criteria.
**Method:** PUT
**Endpoint:** /api/saved-filters/{filter_id}
**Authentication:** Required (Bearer token)
**Path Parameters:**
- `filter_id` (string, required): UUID of the filter to update
- Must be a valid UUID
- Must belong to the authenticated user
**Request Body:**
```json
{
"name": "Updated Sci-Fi Books",
"resource_type": "media-items",
"filters": {
"genre_filter": "Science Fiction Fantasy",
"sort": "author ASC",
"year_min": "2000",
"year_max": "2024"
}
}
```
**Required Fields:**
- `name` (string, required): New filter name
- Must be unique per user + resource type (excluding current filter)
- Maximum 100 characters
- `resource_type` (string, required): Resource type (must match existing)
- Cannot change resource type after creation
- `filters` (object, required): Updated filter criteria
- Replaces existing filters entirely
**Response:**
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Updated Sci-Fi Books",
"resource_type": "media-items",
"filters": {
"genre_filter": "Science Fiction Fantasy",
"sort": "author ASC",
"year_min": "2000",
"year_max": "2024"
},
"created_at": "2024-03-20T12:00:00Z",
"updated_at": "2024-03-20T15:30:00Z"
}
```
**Status Codes:**
- 200: Success - Filter updated successfully
- 400: Bad Request - Invalid request body or filter ID
- 401: Unauthorized - Invalid or missing authentication token
- 404: Not Found - Filter doesn't exist or doesn't belong to user
- 409: Conflict - New name conflicts with existing filter
**Error Response Examples:**
Filter not found:
```json
{
"error": "filter not found or access denied"
}
```
Duplicate name:
```json
{
"error": "filter with name 'Updated Sci-Fi Books' already exists for this resource type"
}
```
**Example Usage:**
```bash
curl -X PUT \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Updated Sci-Fi Books",
"resource_type": "media-items",
"filters": {
"genre_filter": "Science Fiction Fantasy",
"sort": "author ASC",
"year_min": "2000"
}
}' \
"{{base_url}}/api/saved-filters/{{filter_id}}"
```
**Important Notes:**
- You can only update filters that belong to you
- The `updated_at` timestamp is automatically updated
- Cannot change the resource_type (it's immutable)
- New name must not conflict with other filters you own for the same resource type
- All filter fields are replaced entirely (partial updates not supported)
@@ -8,238 +8,11 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"name\": \"Complete Workflow Test\",\n \"resource_type\"\ data: "{\n \"name\": \"Complete Workflow Test\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\",\n \"sort\": \"title ASC\"\n }\n }"
: \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\"\ docs: "## Complete CRUD Workflow\n\nEnd-to-end test demonstrating the complete lifecycle of a saved filter: Create → Read → Update → Delete.\n\n**Workflow Steps:**\n\n**Step 1: CREATE a new filter**\n```bash\nCREATE_RESPONSE=$(curl -s -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Complete Workflow Test\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\",\n \"sort\": \"title ASC\"\n }\n }' \\\n \"{{base_url}}/api/saved-filters\")\n\necho \"Create Response:\"\necho $CREATE_RESPONSE | jq '.'\n\n# Extract filter ID for subsequent requests\nFILTER_ID=$(echo $CREATE_RESPONSE | jq -r '.id')\necho \"Created filter ID: $FILTER_ID\"\n\n# Expected Status: 201 Created\n# Expected Response:\n# {\n# \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n# \"name\": \"Complete Workflow Test\",\n# \"resource_type\": \"media-items\",\n# \"filters\": {\n#\
,\n \"sort\": \"title ASC\"\n }\n }" \ \"genre_filter\": \"Science Fiction\",\n# \"sort\": \"title ASC\"\n# },\n# \"created_at\": \"2024-03-20T12:00:00Z\",\n# \"updated_at\": \"2024-03-20T12:00:00Z\"\n# }\n```\n\n**Step 2: READ the created filter (list all)**\n```bash\n# List all filters for media-items\nLIST_RESPONSE=$(curl -s \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n \"{{base_url}}/api/saved-filters?resource_type=media-items\")\n\necho \"List Response:\"\necho $LIST_RESPONSE | jq '.'\n\n# Verify our filter is in the list\nFILTER_EXISTS=$(echo $LIST_RESPONSE | jq --arg id \"$FILTER_ID\" \\\n '.[] | select(.id == $id) | .id')\n\nif [ \"$FILTER_EXISTS\" == \"$FILTER_ID\" ]; then\n echo \"✓ Filter found in list\"\nelse\n echo \"✗ Filter NOT found in list\"\nfi\n\n# Expected Status: 200 OK\n# Expected Response: Array containing our newly created filter\n```\n\n**Step 3: UPDATE the filter**\n```bash\n# Update filter name and criteria\nUPDATE_RESPONSE=$(curl -s -X PUT \\\n -H \"Authorization: Bearer\
\ YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Complete Workflow Test - Updated\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction Fantasy\",\n \"sort\": \"author ASC\",\n \"year_min\": \"2000\"\n }\n }' \\\n \"{{base_url}}/api/saved-filters/$FILTER_ID\")\n\necho \"Update Response:\"\necho $UPDATE_RESPONSE | jq '.'\n\n# Verify updated_at timestamp changed\nUPDATED_AT=$(echo $UPDATE_RESPONSE | jq -r '.updated_at')\necho \"Filter updated at: $UPDATED_AT\"\n\n# Expected Status: 200 OK\n# Expected Response:\n# {\n# \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n# \"name\": \"Complete Workflow Test - Updated\",\n# \"resource_type\": \"media-items\",\n# \"filters\": {\n# \"genre_filter\": \"Science Fiction Fantasy\",\n# \"sort\": \"author ASC\",\n# \"year_min\": \"2000\"\n# },\n# \"created_at\": \"2024-03-20T12:00:00Z\",\n# \"updated_at\": \"2024-03-20T12:05:00Z\"\
docs: |- \ # NEW TIMESTAMP\n# }\n```\n\n**Step 4: DELETE the filter**\n```bash\n# Delete the filter\nDELETE_RESPONSE=$(curl -s -X DELETE \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n \"{{base_url}}/api/saved-filters/$FILTER_ID\")\n\necho \"Delete Response:\"\necho $DELETE_RESPONSE\n\n# Expected Status: 204 No Content\n# Expected Response: (empty body)\nif [ -z \"$DELETE_RESPONSE\" ]; then\n echo \"✓ Filter deleted successfully (empty response)\"\nfi\n```\n\n**Step 5: VERIFY deletion**\n```bash\n# Try to get the deleted filter\nGET_RESPONSE=$(curl -s \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n \"{{base_url}}/api/saved-filters?resource_type=media-items\")\n\necho \"List After Delete:\"\necho $GET_RESPONSE | jq '.'\n\n# Verify filter is NOT in the list\nFILTER_EXISTS=$(echo $GET_RESPONSE | jq --arg id \"$FILTER_ID\" \\\n '.[] | select(.id == $id) | .id')\n\nif [ -z \"$FILTER_EXISTS\" ]; then\n echo \"✓ Filter successfully deleted (not found in list)\"\nelse\n echo \"✗ Filter still\
## Complete CRUD Workflow \ exists (deletion failed)\"\nfi\n\n# Expected: Filter no longer appears in list\n```\n\n**Shell Script Complete Example:**\n```bash\n#!/bin/bash\n\nset -e # Exit on error\n\nBASE_URL=\"{{base_url}}\"\nTOKEN=\"YOUR_TOKEN\"\n\necho \"=== Complete CRUD Workflow Test ===\"\necho \"\"\n\n# 1. CREATE\necho \"Step 1: Creating filter...\"\nFILTER_ID=$(curl -s -X POST \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Workflow Test\",\n \"resource_type\": \"media-items\",\n \"filters\": {\"genre_filter\": \"Science Fiction\"}\n }' \\\n \"$BASE_URL/api/saved-filters\" | jq -r '.id')\n\necho \"✓ Created filter: $FILTER_ID\"\necho \"\"\n\n# 2. READ\necho \"Step 2: Reading filters...\"\ncurl -s -H \"Authorization: Bearer $TOKEN\" \\\n \"$BASE_URL/api/saved-filters?resource_type=media-items\" | \\\n jq --arg id \"$FILTER_ID\" '.[] | select(.id == $id)'\n\necho \"✓ Filter retrieved\"\necho \"\"\n\n# 3. UPDATE\necho \"Step 3:\
\ Updating filter...\"\ncurl -s -X PUT \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Workflow Test - Updated\",\n \"resource_type\": \"media-items\",\n \"filters\": {\"genre_filter\": \"Fantasy\"}\n }' \\\n \"$BASE_URL/api/saved-filters/$FILTER_ID\" | jq '.'\n\necho \"✓ Filter updated\"\necho \"\"\n\n# 4. DELETE\necho \"Step 4: Deleting filter...\"\ncurl -s -X DELETE \\\n -H \"Authorization: Bearer $TOKEN\" \\\n \"$BASE_URL/api/saved-filters/$FILTER_ID\" -w \"\\nStatus: %{http_code}\\n\"\n\necho \"✓ Filter deleted\"\necho \"\"\n\n# 5. VERIFY\necho \"Step 5: Verifying deletion...\"\nCOUNT=$(curl -s -H \"Authorization: Bearer $TOKEN\" \\\n \"$BASE_URL/api/saved-filters?resource_type=media-items\" | \\\n jq --arg id \"$FILTER_ID\" '[.[] | select(.id == $id)] | length')\n\nif [ \"$COUNT\" -eq 0 ]; then\n echo \"✓ Verification successful - filter deleted\"\nelse\n echo \"✗ Verification failed - filter still exists\"\
End-to-end test demonstrating the complete lifecycle of a saved filter: Create → Read → Update → Delete. \n exit 1\nfi\n\necho \"\"\necho \"=== All CRUD operations completed successfully ===\"\n```\n\n**Success Criteria:**\n- ✅ Create returns 201 with generated ID\n- ✅ Read returns filter in list\n- ✅ Update modifies filter and updates timestamp\n- ✅ Delete returns 204 No Content\n- ✅ Filter no longer appears in list after deletion"
**Workflow Steps:**
**Step 1: CREATE a new filter**
```bash
CREATE_RESPONSE=$(curl -s -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Complete Workflow Test",
"resource_type": "media-items",
"filters": {
"genre_filter": "Science Fiction",
"sort": "title ASC"
}
}' \
"{{base_url}}/api/saved-filters")
echo "Create Response:"
echo $CREATE_RESPONSE | jq '.'
# Extract filter ID for subsequent requests
FILTER_ID=$(echo $CREATE_RESPONSE | jq -r '.id')
echo "Created filter ID: $FILTER_ID"
# Expected Status: 201 Created
# Expected Response:
# {
# "id": "550e8400-e29b-41d4-a716-446655440000",
# "name": "Complete Workflow Test",
# "resource_type": "media-items",
# "filters": {
# "genre_filter": "Science Fiction",
# "sort": "title ASC"
# },
# "created_at": "2024-03-20T12:00:00Z",
# "updated_at": "2024-03-20T12:00:00Z"
# }
```
**Step 2: READ the created filter (list all)**
```bash
# List all filters for media-items
LIST_RESPONSE=$(curl -s \
-H "Authorization: Bearer YOUR_TOKEN" \
"{{base_url}}/api/saved-filters?resource_type=media-items")
echo "List Response:"
echo $LIST_RESPONSE | jq '.'
# Verify our filter is in the list
FILTER_EXISTS=$(echo $LIST_RESPONSE | jq --arg id "$FILTER_ID" \
'.[] | select(.id == $id) | .id')
if [ "$FILTER_EXISTS" == "$FILTER_ID" ]; then
echo "✓ Filter found in list"
else
echo "✗ Filter NOT found in list"
fi
# Expected Status: 200 OK
# Expected Response: Array containing our newly created filter
```
**Step 3: UPDATE the filter**
```bash
# Update filter name and criteria
UPDATE_RESPONSE=$(curl -s -X PUT \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Complete Workflow Test - Updated",
"resource_type": "media-items",
"filters": {
"genre_filter": "Science Fiction Fantasy",
"sort": "author ASC",
"year_min": "2000"
}
}' \
"{{base_url}}/api/saved-filters/$FILTER_ID")
echo "Update Response:"
echo $UPDATE_RESPONSE | jq '.'
# Verify updated_at timestamp changed
UPDATED_AT=$(echo $UPDATE_RESPONSE | jq -r '.updated_at')
echo "Filter updated at: $UPDATED_AT"
# Expected Status: 200 OK
# Expected Response:
# {
# "id": "550e8400-e29b-41d4-a716-446655440000",
# "name": "Complete Workflow Test - Updated",
# "resource_type": "media-items",
# "filters": {
# "genre_filter": "Science Fiction Fantasy",
# "sort": "author ASC",
# "year_min": "2000"
# },
# "created_at": "2024-03-20T12:00:00Z",
# "updated_at": "2024-03-20T12:05:00Z" # NEW TIMESTAMP
# }
```
**Step 4: DELETE the filter**
```bash
# Delete the filter
DELETE_RESPONSE=$(curl -s -X DELETE \
-H "Authorization: Bearer YOUR_TOKEN" \
"{{base_url}}/api/saved-filters/$FILTER_ID")
echo "Delete Response:"
echo $DELETE_RESPONSE
# Expected Status: 204 No Content
# Expected Response: (empty body)
if [ -z "$DELETE_RESPONSE" ]; then
echo "✓ Filter deleted successfully (empty response)"
fi
```
**Step 5: VERIFY deletion**
```bash
# Try to get the deleted filter
GET_RESPONSE=$(curl -s \
-H "Authorization: Bearer YOUR_TOKEN" \
"{{base_url}}/api/saved-filters?resource_type=media-items")
echo "List After Delete:"
echo $GET_RESPONSE | jq '.'
# Verify filter is NOT in the list
FILTER_EXISTS=$(echo $GET_RESPONSE | jq --arg id "$FILTER_ID" \
'.[] | select(.id == $id) | .id')
if [ -z "$FILTER_EXISTS" ]; then
echo "✓ Filter successfully deleted (not found in list)"
else
echo "✗ Filter still exists (deletion failed)"
fi
# Expected: Filter no longer appears in list
```
**Shell Script Complete Example:**
```bash
#!/bin/bash
set -e # Exit on error
BASE_URL="{{base_url}}"
TOKEN="YOUR_TOKEN"
echo "=== Complete CRUD Workflow Test ==="
echo ""
# 1. CREATE
echo "Step 1: Creating filter..."
FILTER_ID=$(curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Workflow Test",
"resource_type": "media-items",
"filters": {"genre_filter": "Science Fiction"}
}' \
"$BASE_URL/api/saved-filters" | jq -r '.id')
echo "✓ Created filter: $FILTER_ID"
echo ""
# 2. READ
echo "Step 2: Reading filters..."
curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/api/saved-filters?resource_type=media-items" | \
jq --arg id "$FILTER_ID" '.[] | select(.id == $id)'
echo "✓ Filter retrieved"
echo ""
# 3. UPDATE
echo "Step 3: Updating filter..."
curl -s -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Workflow Test - Updated",
"resource_type": "media-items",
"filters": {"genre_filter": "Fantasy"}
}' \
"$BASE_URL/api/saved-filters/$FILTER_ID" | jq '.'
echo "✓ Filter updated"
echo ""
# 4. DELETE
echo "Step 4: Deleting filter..."
curl -s -X DELETE \
-H "Authorization: Bearer $TOKEN" \
"$BASE_URL/api/saved-filters/$FILTER_ID" -w "\nStatus: %{http_code}\n"
echo "✓ Filter deleted"
echo ""
# 5. VERIFY
echo "Step 5: Verifying deletion..."
COUNT=$(curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/api/saved-filters?resource_type=media-items" | \
jq --arg id "$FILTER_ID" '[.[] | select(.id == $id)] | length')
if [ "$COUNT" -eq 0 ]; then
echo "✓ Verification successful - filter deleted"
else
echo "✗ Verification failed - filter still exists"
exit 1
fi
echo ""
echo "=== All CRUD operations completed successfully ==="
```
**Success Criteria:**
- ✅ Create returns 201 with generated ID
- ✅ Read returns filter in list
- ✅ Update modifies filter and updates timestamp
- ✅ Delete returns 204 No Content
- ✅ Filter no longer appears in list after deletion
@@ -8,69 +8,6 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"name\": \"Duplicate Test\",\n \"resource_type\":\ data: "{\n \"name\": \"Duplicate Test\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"search\": \"test\",\n \"genre_filter\": \"Science Fiction\"\n }\n }"
\ \"media-items\",\n \"filters\": {\n \"search\": \"test\",\n \"genre_filter\"\ docs: "## Duplicate Name Validation\n\nTests that the API correctly prevents creating duplicate filter names for the same user + resource type combination.\n\n**Scenario:**\n1. Create first filter with name \"Duplicate Test\"\n2. Attempt to create second filter with same name\n3. Expect 409 Conflict error\n\n**Expected Behavior:**\n- First request: 201 Created\n- Second request: 409 Conflict with error message\n\n**Error Response:**\n```json\n{\n \"error\": \"filter with name 'Duplicate Test' already exists for this resource type\"\n}\n```\n\n**Test Steps:**\n```bash\n# First request - should succeed\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Duplicate Test\",\n \"resource_type\": \"media-items\",\n \"filters\": {\"search\": \"test\"}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Response: 201 Created\n\n# Second request - should fail with 409\ncurl -X POST \\\n -H \"Authorization: Bearer\
: \"Science Fiction\"\n }\n }" \ YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Duplicate Test\",\n \"resource_type\": \"media-items\",\n \"filters\": {\"search\": \"test\"}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Response: 409 Conflict\n# {\n# \"error\": \"filter with name 'Duplicate Test' already exists for this resource type\"\n# }\n```\n\n**Important Notes:**\n- Duplicate check is per user + resource type\n- Same filter name can be used for different resource types\n- Different users can have filters with the same name\n- Validation happens in service layer before database insertion\n\n**Related Tests:**\n- Update filter with name that conflicts with another filter (should also return 409)\n- Create filter with different name for different resource type (should succeed)"
docs: |-
## Duplicate Name Validation
Tests that the API correctly prevents creating duplicate filter names for the same user + resource type combination.
**Scenario:**
1. Create first filter with name "Duplicate Test"
2. Attempt to create second filter with same name
3. Expect 409 Conflict error
**Expected Behavior:**
- First request: 201 Created
- Second request: 409 Conflict with error message
**Error Response:**
```json
{
"error": "filter with name 'Duplicate Test' already exists for this resource type"
}
```
**Test Steps:**
```bash
# First request - should succeed
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Duplicate Test",
"resource_type": "media-items",
"filters": {"search": "test"}
}' \
"{{base_url}}/api/saved-filters"
# Response: 201 Created
# Second request - should fail with 409
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Duplicate Test",
"resource_type": "media-items",
"filters": {"search": "test"}
}' \
"{{base_url}}/api/saved-filters"
# Response: 409 Conflict
# {
# "error": "filter with name 'Duplicate Test' already exists for this resource type"
# }
```
**Important Notes:**
- Duplicate check is per user + resource type
- Same filter name can be used for different resource types
- Different users can have filters with the same name
- Validation happens in service layer before database insertion
**Related Tests:**
- Update filter with name that conflicts with another filter (should also return 409)
- Create filter with different name for different resource type (should succeed)
@@ -8,254 +8,10 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"name\": \"\",\n \"resource_type\": \"media-items\"\ data: "{\n \"name\": \"\",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }"
,\n \"filters\": {}\n }" docs: "## Filter Validation - Edge Cases\n\nTests various validation edge cases including empty names, invalid resource types, malformed JSON, etc.\n\n**Test Cases:**\n\n**Test 1: Empty Filter Name**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"\",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request\n# {\n# \"error\": \"name and resource_type are required\"\n# }\n```\n\n**Test 2: Missing Name Field**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request\n# Missing required field validation\n```\n\n**Test 3: Missing Resource Type**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\"\
\ \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Test Filter\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request\n# {\n# \"error\": \"name and resource_type are required\"\n# }\n```\n\n**Test 4: Invalid Resource Type**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Test Filter\",\n \"resource_type\": \"invalid-type\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request or success\n# Note: Current validation allows any string (extensible design)\n# Future: May validate against allowed resource types\n```\n\n**Test 5: Empty Filters Object**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Empty Filter\",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }' \\\n \
docs: |- \ \"{{base_url}}/api/saved-filters\"\n\n# Expected: 201 Created\n# Empty filters object is valid (matches no criteria)\n```\n\n**Test 6: Very Long Filter Name**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"This is a very long filter name that exceeds one hundred characters and should be rejected by the validation logic in the handler\",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request or 201 Created\n# Note: Plan specifies max 100 chars, but validation may not be enforced\n```\n\n**Test 7: Special Characters in Name**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Filter with <script>alert(\\\"xss\\\")</script>\",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters\"\
## Filter Validation - Edge Cases \n\n# Expected: 201 Created (input sanitized)\n# Note: Names are stored as-is, no HTML escaping needed\n```\n\n**Test 8: Unicode Characters in Name**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"\U0001F680 Sci-Fi Books \U0001F4DA\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\",\n \"search\": \"科幻\"\n }\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 201 Created\n# Unicode (emoji, CJK characters) should be supported\n```\n\n**Test 9: Malformed JSON**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Test\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n\"missing closing brace\": \"broken\"\n}' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request\n# {\n# \"error\": \"invalid request body\"\
\n# }\n```\n\n**Test 10: Invalid Filter ID Format**\n```bash\ncurl -X PUT \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Updated\",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters/not-a-uuid\"\n\n# Expected: 400 Bad Request\n# {\n# \"error\": \"invalid filter ID\"\n# }\n```\n\n**Test 11: Missing Filters Field**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Test Filter\",\n \"resource_type\": \"media-items\"\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request\n# Missing required field\n```\n\n**Test 12: Whitespace-Only Name**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \" \",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n\
Tests various validation edge cases including empty names, invalid resource types, malformed JSON, etc. \ }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request\n# Name validation should reject whitespace-only names\n```\n\n**Validation Rules Summary:**\n\n**Required Fields:**\n- `name`: Non-empty string (max 100 chars per plan)\n- `resource_type`: Non-empty string\n- `filters`: Object (can be empty {})\n\n**Optional Fields:**\n- None (all 3 fields are required)\n\n**Field Validation:**\n- Name uniqueness checked per user + resource type\n- Filter ID must be valid UUID for PUT/DELETE\n- Filters must be valid JSON object\n\n**Common Validation Errors:**\n```json\n{\n \"error\": \"name and resource_type are required\"\n}\n```\n\n```json\n{\n \"error\": \"invalid request body\"\n}\n```\n\n```json\n{\n \"error\": \"invalid filter ID\"\n}\n```\n\n```json\n{\n \"error\": \"filter with name 'X' already exists for this resource type\"\n}\n```"
**Test Cases:**
**Test 1: Empty Filter Name**
```bash
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "",
"resource_type": "media-items",
"filters": {}
}' \
"{{base_url}}/api/saved-filters"
# Expected: 400 Bad Request
# {
# "error": "name and resource_type are required"
# }
```
**Test 2: Missing Name Field**
```bash
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"resource_type": "media-items",
"filters": {}
}' \
"{{base_url}}/api/saved-filters"
# Expected: 400 Bad Request
# Missing required field validation
```
**Test 3: Missing Resource Type**
```bash
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Test Filter",
"filters": {}
}' \
"{{base_url}}/api/saved-filters"
# Expected: 400 Bad Request
# {
# "error": "name and resource_type are required"
# }
```
**Test 4: Invalid Resource Type**
```bash
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Test Filter",
"resource_type": "invalid-type",
"filters": {}
}' \
"{{base_url}}/api/saved-filters"
# Expected: 400 Bad Request or success
# Note: Current validation allows any string (extensible design)
# Future: May validate against allowed resource types
```
**Test 5: Empty Filters Object**
```bash
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Empty Filter",
"resource_type": "media-items",
"filters": {}
}' \
"{{base_url}}/api/saved-filters"
# Expected: 201 Created
# Empty filters object is valid (matches no criteria)
```
**Test 6: Very Long Filter Name**
```bash
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "This is a very long filter name that exceeds one hundred characters and should be rejected by the validation logic in the handler",
"resource_type": "media-items",
"filters": {}
}' \
"{{base_url}}/api/saved-filters"
# Expected: 400 Bad Request or 201 Created
# Note: Plan specifies max 100 chars, but validation may not be enforced
```
**Test 7: Special Characters in Name**
```bash
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Filter with <script>alert(\"xss\")</script>",
"resource_type": "media-items",
"filters": {}
}' \
"{{base_url}}/api/saved-filters"
# Expected: 201 Created (input sanitized)
# Note: Names are stored as-is, no HTML escaping needed
```
**Test 8: Unicode Characters in Name**
```bash
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "🚀 Sci-Fi Books 📚",
"resource_type": "media-items",
"filters": {
"genre_filter": "Science Fiction",
"search": "科幻"
}
}' \
"{{base_url}}/api/saved-filters"
# Expected: 201 Created
# Unicode (emoji, CJK characters) should be supported
```
**Test 9: Malformed JSON**
```bash
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Test",
"resource_type": "media-items",
"filters": {
"missing closing brace": "broken"
}' \
"{{base_url}}/api/saved-filters"
# Expected: 400 Bad Request
# {
# "error": "invalid request body"
# }
```
**Test 10: Invalid Filter ID Format**
```bash
curl -X PUT \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Updated",
"resource_type": "media-items",
"filters": {}
}' \
"{{base_url}}/api/saved-filters/not-a-uuid"
# Expected: 400 Bad Request
# {
# "error": "invalid filter ID"
# }
```
**Test 11: Missing Filters Field**
```bash
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Test Filter",
"resource_type": "media-items"
}' \
"{{base_url}}/api/saved-filters"
# Expected: 400 Bad Request
# Missing required field
```
**Test 12: Whitespace-Only Name**
```bash
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": " ",
"resource_type": "media-items",
"filters": {}
}' \
"{{base_url}}/api/saved-filters"
# Expected: 400 Bad Request
# Name validation should reject whitespace-only names
```
**Validation Rules Summary:**
**Required Fields:**
- `name`: Non-empty string (max 100 chars per plan)
- `resource_type`: Non-empty string
- `filters`: Object (can be empty {})
**Optional Fields:**
- None (all 3 fields are required)
**Field Validation:**
- Name uniqueness checked per user + resource type
- Filter ID must be valid UUID for PUT/DELETE
- Filters must be valid JSON object
**Common Validation Errors:**
```json
{
"error": "name and resource_type are required"
}
```
```json
{
"error": "invalid request body"
}
```
```json
{
"error": "invalid filter ID"
}
```
```json
{
"error": "filter with name 'X' already exists for this resource type"
}
```
+2 -22
View File
@@ -8,25 +8,5 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"library_id\": \"{{library_id" data: "{\n \"library_id\": \"{{library_id"
docs: "## Start Watch Mode\n \n Starts real-time file system monitoring for a specific library. New media items will be detected and processed almost instantly.\n \n **Method:** POST\n \n **Endpoint:** /api/scanner/watch/start\n \n **Authentication:** Required (Bearer token, Admin only)\n \n **Request Body:**\n - `library_id` (string, required): UUID of the library to watch\n - Example: `\"550e8400-e29b-41d4-a716-446655440000\"`\n \n **Response:** HTTP 200 (OK)\n ```json\n {\n \"message\": \"watch mode started for library\",\n \"library_id\": \"550e8400-e29b-41d4-a716-446655440000\""
docs: |-
## Start Watch Mode
Starts real-time file system monitoring for a specific library. New media items will be detected and processed almost instantly.
**Method:** POST
**Endpoint:** /api/scanner/watch/start
**Authentication:** Required (Bearer token, Admin only)
**Request Body:**
- `library_id` (string, required): UUID of the library to watch
- Example: `"550e8400-e29b-41d4-a716-446655440000"`
**Response:** HTTP 200 (OK)
```json
{
"message": "watch mode started for library",
"library_id": "550e8400-e29b-41d4-a716-446655440000"
+2 -22
View File
@@ -8,25 +8,5 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"library_id\": \"{{library_id" data: "{\n \"library_id\": \"{{library_id"
docs: "## Stop Watch Mode\n \n Stops real-time file system monitoring for a specific library.\n \n **Method:** POST\n \n **Endpoint:** /api/scanner/watch/stop\n \n **Authentication:** Required (Bearer token, Admin only)\n \n **Request Body:**\n - `library_id` (string, required): UUID of the library to stop watching\n - Example: `\"550e8400-e29b-41d4-a716-446655440000\"`\n \n **Response:** HTTP 200 (OK)\n ```json\n {\n \"message\": \"watch mode stopped for library\",\n \"library_id\": \"550e8400-e29b-41d4-a716-446655440000\""
docs: |-
## Stop Watch Mode
Stops real-time file system monitoring for a specific library.
**Method:** POST
**Endpoint:** /api/scanner/watch/stop
**Authentication:** Required (Bearer token, Admin only)
**Request Body:**
- `library_id` (string, required): UUID of the library to stop watching
- Example: `"550e8400-e29b-41d4-a716-446655440000"`
**Response:** HTTP 200 (OK)
```json
{
"message": "watch mode stopped for library",
"library_id": "550e8400-e29b-41d4-a716-446655440000"
+2 -29
View File
@@ -8,35 +8,8 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"base_url\": \"https://bookhoard.example.com\",\n \"opds_base_url\"\ data: "{\n \"base_url\": \"https://bookhoard.example.com\",\n \"opds_base_url\": \"https://bookhoard.example.com/opds\",\n \"api_base_url\": \"https://bookhoard.example.com/api\""
: \"https://bookhoard.example.com/opds\",\n \"api_base_url\": \"https://bookhoard.example.com/api\""
headers: headers:
- key: Authorization - key: Authorization
value: Bearer {{admin_token value: Bearer {{admin_token
docs: "## Update System Configuration\n\n Updates system-wide configuration settings for the Bookhoard instance.\n\n **Method:** PUT\n\n **Endpoint:** /api/system/config\n\n **Authentication:** Bearer token (admin only)\n\n **Request Body:**\n - `base_url` (string): Base URL for the instance\n - `opds_base_url` (string): OPDS endpoint base URL\n - `api_base_url` (string): API endpoint base URL\n\n **Response:**\n - `status` (string): Update status\n - `config` (object): Updated configuration\n\n **Status Codes:**\n - 200: Success\n - 400: Invalid configuration\n - 401: Unauthorized\n - 403: Forbidden (admin only)\n - 500: Internal server error"
docs: |-
## Update System Configuration
Updates system-wide configuration settings for the Bookhoard instance.
**Method:** PUT
**Endpoint:** /api/system/config
**Authentication:** Bearer token (admin only)
**Request Body:**
- `base_url` (string): Base URL for the instance
- `opds_base_url` (string): OPDS endpoint base URL
- `api_base_url` (string): API endpoint base URL
**Response:**
- `status` (string): Update status
- `config` (object): Updated configuration
**Status Codes:**
- 200: Success
- 400: Invalid configuration
- 401: Unauthorized
- 403: Forbidden (admin only)
- 500: Internal server error
+5 -15
View File
@@ -4,22 +4,12 @@ info:
seq: 2 seq: 2
http: http:
method: PUT method: PUT
url: "{{base_url}}/api/libraries/scan-settings" url: '{{base_url}}/api/libraries/scan-settings'
auth: inherit auth: inherit
body: body:
type: json type: json
json: data: "{\n \"scan_poll_interval_seconds\": 30,\n \"auto_scan_enabled\": true\n}"
scan_poll_interval_seconds: 30
auto_scan_enabled: true
headers: headers:
- key: Content-Type - key: Content-Type
value: application/json value: application/json
docs: "## Update System Scan Settings\n\n Updates system-wide scan settings that apply to all libraries.\n\n **Authentication**: Admin token required\n **Request**: Scan frequency (15-1440 minutes) and enabled status\n **Response**: Success message"
docs: |-
## Update System Scan Settings
Updates system-wide scan settings that apply to all libraries.
**Authentication**: Admin token required
**Request**: Scan frequency (15-1440 minutes) and enabled status
**Response**: Success message
+2 -26
View File
@@ -8,29 +8,5 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: |- data: "{\n \"new_password\": \"NewSecurePassword123!\",\n \"confirm_password\": \"NewSecurePassword123!\"\n}"
{ docs: "## Admin Reset User Password\n \n Admin-only endpoint to reset another user's password.\n \n **Method:** PUT\n \n **Endpoint:** /api/auth/password/:id\n \n **Authentication:** Required (Admin Bearer token)\n \n **Request Body:**\n - `new_password` (string, required): New password\n - `confirm_password` (string, required): Must match new_password\n \n **Status Codes:**\n - 200: Success\n - 400: Passwords do not match or invalid format\n - 403: Not an admin\n - 404: User not found"
"new_password": "NewSecurePassword123!",
"confirm_password": "NewSecurePassword123!"
}
docs: |-
## Admin Reset User Password
Admin-only endpoint to reset another user's password.
**Method:** PUT
**Endpoint:** /api/auth/password/:id
**Authentication:** Required (Admin Bearer token)
**Request Body:**
- `new_password` (string, required): New password
- `confirm_password` (string, required): Must match new_password
**Status Codes:**
- 200: Success
- 400: Passwords do not match or invalid format
- 403: Not an admin
- 404: User not found
+2 -32
View File
@@ -8,35 +8,5 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: |- data: "{\n \"username\": \"newusername\",\n \"email\": \"newemail@example.com\",\n \"role\": \"admin\"\n}"
{ docs: "## Admin Update User\n \n Admin-only endpoint to update another user's profile.\n \n **Method:** PUT\n \n **Endpoint:** /api/auth/profile/:id\n \n **Authentication:** Required (Admin Bearer token)\n \n **Request Body:**\n - `username` (string, optional): New username\n - `email` (string, optional): New email\n - `first_name` (string, optional): First name\n - `last_name` (string, optional): Last name\n - `theme` (string, optional): Theme preference\n - `role` (string, optional): Role (\"user\" or \"admin\") - admin only\n \n **Status Codes:**\n - 200: Success\n - 400: Invalid role or cannot demote last admin\n - 403: Not an admin\n - 404: User not found\n - 409: Username or email already taken"
"username": "newusername",
"email": "newemail@example.com",
"role": "admin"
}
docs: |-
## Admin Update User
Admin-only endpoint to update another user's profile.
**Method:** PUT
**Endpoint:** /api/auth/profile/:id
**Authentication:** Required (Admin Bearer token)
**Request Body:**
- `username` (string, optional): New username
- `email` (string, optional): New email
- `first_name` (string, optional): First name
- `last_name` (string, optional): Last name
- `theme` (string, optional): Theme preference
- `role` (string, optional): Role ("user" or "admin") - admin only
**Status Codes:**
- 200: Success
- 400: Invalid role or cannot demote last admin
- 403: Not an admin
- 404: User not found
- 409: Username or email already taken
+2 -27
View File
@@ -8,30 +8,5 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: "{\n \"current_password\": \"password123\",\n \"new_password\"\ data: "{\n \"current_password\": \"password123\",\n \"new_password\": \"newpassword123\",\n \"confirm_password\": \"newpassword123\""
: \"newpassword123\",\n \"confirm_password\": \"newpassword123\"" docs: "## Update Password\n \n Updates the authenticated user's password.\n \n **Method:** PUT\n \n **Endpoint:** /api/auth/password\n \n **Authentication:** Required\n \n **Request Body:**\n - `current_password` (string, required): Current password for verification\n - `new_password` (string, required): New password (minimum 6 characters)\n - `confirm_password` (string, required): Confirmation of new password\n \n **Response:**\n - `message` (string): Success message\n \n **Status Codes:**\n - 200: Success\n - 400: Password validation failed\n - 401: Current password incorrect\n - 401: Unauthorized"
docs: |-
## Update Password
Updates the authenticated user's password.
**Method:** PUT
**Endpoint:** /api/auth/password
**Authentication:** Required
**Request Body:**
- `current_password` (string, required): Current password for verification
- `new_password` (string, required): New password (minimum 6 characters)
- `confirm_password` (string, required): Confirmation of new password
**Response:**
- `message` (string): Success message
**Status Codes:**
- 200: Success
- 400: Password validation failed
- 401: Current password incorrect
- 401: Unauthorized
+2 -35
View File
@@ -8,38 +8,5 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: |- data: "{\n \"username\": \"updateduser\",\n \"email\": \"updated@tests.bookhoard.internal\",\n \"first_name\": \"Updated\",\n \"last_name\": \"User\",\n \"theme\": \"dracula\"\n}"
{ docs: "## Update User Profile\n \n Updates the authenticated user's profile information.\n \n **Method:** PUT\n \n **Endpoint:** /api/auth/profile\n \n **Authentication:** Required (Bearer token)\n \n **Request Body:**\n - `username` (string, optional): New username (must be unique)\n - `email` (string, optional): New email address (must be unique)\n - `first_name` (string, optional): First name\n - `last_name` (string, optional): Last name\n - `theme` (string, optional): Theme preference\n \n **Response:**\n - `message` (string): Success message\n \n **Status Codes:**\n - 200: Success\n - 400: Invalid request\n - 401: Unauthorized\n - 409: Username or email already taken"
"username": "updateduser",
"email": "updated@tests.bookhoard.internal",
"first_name": "Updated",
"last_name": "User",
"theme": "dracula"
}
docs: |-
## Update User Profile
Updates the authenticated user's profile information.
**Method:** PUT
**Endpoint:** /api/auth/profile
**Authentication:** Required (Bearer token)
**Request Body:**
- `username` (string, optional): New username (must be unique)
- `email` (string, optional): New email address (must be unique)
- `first_name` (string, optional): First name
- `last_name` (string, optional): Last name
- `theme` (string, optional): Theme preference
**Response:**
- `message` (string): Success message
**Status Codes:**
- 200: Success
- 400: Invalid request
- 401: Unauthorized
- 409: Username or email already taken
+2 -26
View File
@@ -8,29 +8,5 @@ http:
auth: inherit auth: inherit
body: body:
type: json type: json
jsonBody: |- data: "{\n \"theme\": \"dracula\"\n}"
{ docs: "## Update Theme\n \n Fast theme switching for the application UI.\n \n **Method:** PUT\n \n **Endpoint:** /api/auth/theme\n \n **Authentication:** Required (Bearer token)\n \n **Request Body:**\n - `theme` (string, required): Theme name\n \n **Response:**\n - `message` (string): Success message\n \n **Status Codes:**\n - 200: Success\n - 400: Invalid theme\n - 401: Unauthorized"
"theme": "dracula"
}
docs: |-
## Update Theme
Fast theme switching for the application UI.
**Method:** PUT
**Endpoint:** /api/auth/theme
**Authentication:** Required (Bearer token)
**Request Body:**
- `theme` (string, required): Theme name
**Response:**
- `message` (string): Success message
**Status Codes:**
- 200: Success
- 400: Invalid theme
- 401: Unauthorized