> ## Documentation Index
> Fetch the complete documentation index at: https://komga-sdk.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Read Lists

> Create ordered reading lists of individual books

Read Lists organize individual books in a defined sequence. They are useful for:

* Crossover events spanning multiple series
* Recommended reading orders for complex universes
* Personal “to-read” queues
* Curated book-club selections

For series-level grouping, use [Collections](/guides/collections). Generated operations return `{ data, error, request, response }`; pass flat parameters first and `{ client }` second. Creating, updating, and deleting a read list requires the **ADMIN** role.

## List read lists

```typescript theme={null}
import { getReadLists } from 'komga-sdk';

const result = await getReadLists(
  {
    page: 0,
    size: 20,
    search: 'Crisis',
    library_id: ['library-1'],
  },
  { client },
);

if (result.data) {
  console.log(`Found ${result.data.totalElements ?? 0} read lists`);
  for (const readList of result.data.content ?? []) {
    console.log(`${readList.name} - ${readList.bookIds.length} books`);
  }
}
```

`search`, `library_id`, `page`, `size`, and `unpaged` remain flat query parameters. Use `unpaged: true` when calculating a complete queue.

## Get a read list by ID

A read-list response contains the ordered `bookIds` array:

```typescript theme={null}
import { getReadListById } from 'komga-sdk';

const result = await getReadListById({ id: 'readlist-123' }, { client });
if (result.data) {
  console.log(`${result.data.name}: ${result.data.bookIds.length} books`);
  console.log(`Required order: ${result.data.ordered}`);
}
```

## Create a read list

The generated body parameter is `readListCreationDto`. Keep the IDs in the intended order when `ordered` is true:

```typescript theme={null}
import { createReadList } from 'komga-sdk';

const result = await createReadList(
  {
    readListCreationDto: {
      name: 'Crisis on Infinite Earths Reading Order',
      summary: 'Complete crossover event reading order',
      ordered: true,
      bookIds: [
        'book-001', // Issue #1
        'book-002', // Issue #2
        'book-003', // Superman tie-in
        'book-004', // Issue #3
      ],
    },
  },
  { client },
);

if (result.data) console.log(`Created read list: ${result.data.name}`);
```

> [!TIP]
>
> Use `ordered: true` for a sequence where next/previous navigation matters. Use `ordered: false` for a thematic grouping where the server does not need to preserve a reading order.

## Update a read list

`readListUpdateDto` is partial, but `bookIds` is a complete replacement when supplied. Fetch first when inserting one book into an existing order:

```typescript theme={null}
import { updateReadListById } from 'komga-sdk';

await updateReadListById(
  {
    id: 'readlist-123',
    readListUpdateDto: {
      name: 'Revised Reading Order',
      summary: 'Includes the latest tie-ins',
      ordered: true,
      bookIds: ['book-001', 'book-002', 'book-005', 'book-006'],
    },
  },
  { client },
);
```

## Delete a read list

Deleting a read list does not delete its books:

```typescript theme={null}
import { deleteReadListById } from 'komga-sdk';

await deleteReadListById({ id: 'readlist-123' }, { client });
```

## Get books in a read list

The path parameter is named `id`. The returned page preserves the read-list order; `unpaged: true` is useful when progress or export code needs every book:

```typescript theme={null}
import { getBooksByReadListId } from 'komga-sdk';

const result = await getBooksByReadListId(
  { id: 'readlist-123', page: 0, size: 50 },
  { client },
);

for (const book of result.data?.content ?? []) {
  console.log(`${book.metadata.title} (${book.metadata.number})`);
}
```

## Find read lists containing a book

This operation returns an array rather than a paginated response:

```typescript theme={null}
import { getReadListsByBookId } from 'komga-sdk';

const result = await getReadListsByBookId({ bookId: 'book-123' }, { client });
for (const readList of result.data ?? []) {
  console.log(`${readList.name} (${readList.bookIds.length} books)`);
}
```

## Import a ComicRack list

`matchComicRackList` accepts the uploaded `.cbl` file directly. The result contains one request entry per requested book and its candidate matches; inspect these before creating a list:

```typescript theme={null}
import { matchComicRackList } from 'komga-sdk';

const file = new File([cblBlob], 'reading-order.cbl', { type: 'text/xml' });
const result = await matchComicRackList({ file }, { client });

if (result.data) {
  for (const request of result.data.requests) {
    const bestMatch = request.matches[0];
    console.log(`${request.request.series.join(', ')} #${request.request.number}: ${bestMatch?.books.length ?? 0} matches`);
  }
}
```

A missing or ambiguous match is not a successful book selection. Resolve those entries in the UI or by a follow-up search before sending `bookIds` to `createReadList`.

## Thumbnails

Thumbnail reads return binary data. Uploads accept a flat `file` and optional `selected` query value; deleting an upload needs its `thumbnailId`.

```typescript theme={null}
import {
  addUserUploadedReadListThumbnail,
  deleteUserUploadedReadListThumbnail,
  getReadListThumbnail,
} from 'komga-sdk';

const thumbnail = await getReadListThumbnail({ id: 'readlist-123' }, { client });
if (thumbnail.data) console.log(URL.createObjectURL(thumbnail.data));

const file = new File([imageBlob], 'thumbnail.jpg', { type: 'image/jpeg' });
await addUserUploadedReadListThumbnail(
  { id: 'readlist-123', file, selected: true },
  { client },
);

await deleteUserUploadedReadListThumbnail(
  { id: 'readlist-123', thumbnailId: 'thumbnail-456' },
  { client },
);
```

## Navigate within a read list

At a boundary the server may return no book. Check `error` first; only treat an undefined `data` value as “end” or “beginning” when the request itself succeeded:

```typescript theme={null}
import {
  getBookSiblingNextInReadList,
  getBookSiblingPreviousInReadList,
} from 'komga-sdk';

const next = await getBookSiblingNextInReadList(
  { id: 'readlist-123', bookId: 'book-001' },
  { client },
);
if (next.error) console.error('Next-book request failed', next.response?.status, next.error);
else console.log(next.data ? `Next: ${next.data.metadata.title}` : 'End of read list');

const previous = await getBookSiblingPreviousInReadList(
  { id: 'readlist-123', bookId: 'book-002' },
  { client },
);
if (previous.error) console.error('Previous-book request failed', previous.response?.status, previous.error);
else console.log(previous.data ? `Previous: ${previous.data.metadata.title}` : 'Beginning of read list');
```

## Workflows

### Build a crossover reading order

```typescript theme={null}
import { createReadList } from 'komga-sdk';

async function createCrossoverReadList(eventName: string, bookIds: string[]) {
  return createReadList(
    {
      readListCreationDto: {
        name: `${eventName} Reading Order`,
        summary: `Complete reading order for ${eventName}`,
        ordered: true,
        bookIds,
      },
    },
    { client },
  );
}

await createCrossoverReadList('Infinite Crisis', [
  'book-crisis-01',
  'book-crisis-02',
  'book-superman-tie-in-01',
  'book-crisis-03',
  'book-batman-tie-in-01',
]);
```

### Create a “Next Up” queue

The current list operation requires `bookSearch`; use a read-status condition rather than the removed top-level `readStatus` body field:

```typescript theme={null}
import { createReadList, getBooks } from 'komga-sdk';

async function createNextUpReadList() {
  const booksResult = await getBooks(
    {
      page: 0,
      size: 20,
      sort: ['metadata.releaseDate,desc'],
      bookSearch: {
        condition: { readStatus: { operator: 'is', value: 'UNREAD' } },
      },
    },
    { client },
  );

  const bookIds = (booksResult.data?.content ?? []).map((book) => book.id);
  if (bookIds.length === 0) return;

  return createReadList(
    {
      readListCreationDto: {
        name: 'My Next Reads',
        summary: 'Recently released unread books',
        ordered: false,
        bookIds,
      },
    },
    { client },
  );
}
```

### Calculate progress through a read list

Read each book's `readProgress.completed` flag. An absent progress object is unread; do not use the old `read` property:

```typescript theme={null}
import { getBooksByReadListId } from 'komga-sdk';

async function getReadingProgress(readListId: string) {
  const result = await getBooksByReadListId(
    { id: readListId, unpaged: true },
    { client },
  );
  const books = result.data?.content ?? [];
  const read = books.filter((book) => book.readProgress?.completed).length;
  return {
    total: books.length,
    read,
    percentage: books.length === 0 ? 0 : Math.round((read / books.length) * 100),
  };
}
```

## Next steps

* Group series with [Collections](/guides/collections).
* Track completion with [Read Progress](/guides/read-progress).
* Manage books in the [Books guide](/guides/books).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.