> ## 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 Progress

> Track reading progress for books and series

Use the generated operations to record page-based progress, mark books or series read, and store OPDS/Readium positions. Each operation returns `{ data, error, request, response }`; pass flat parameters first and `{ client }` second.

## Book read progress

### Progress lifecycle

A book with no `readProgress` is **unread**. Setting `page` while `completed` is false makes it **in progress**; setting `completed: true` makes it **read**. Deleting progress removes the server record and returns the book to unread. The `page` value is one-based: page 1 is the first page.

### Mark a book as read

`readProgressUpdateDto` is the generated body parameter. `page` can be omitted when completion is explicit:

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

await markBookReadProgress(
  {
    bookId: 'book-123',
    readProgressUpdateDto: { completed: true },
  },
  { client },
);
```

### Update page progress

`completed` can be omitted; Komga determines it from the page passed and the book's total page count:

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

await markBookReadProgress(
  {
    bookId: 'book-123',
    readProgressUpdateDto: { page: 42, completed: false },
  },
  { client },
);
```

### Mark a book as unread

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

await deleteBookReadProgress({ bookId: 'book-123' }, { client });
```

## Series read progress

These operations update every book in the series. They are useful for an explicit “mark all” action; they do not preserve each book's previous page position:

```typescript theme={null}
import { markSeriesAsRead, markSeriesAsUnread } from 'komga-sdk';

await markSeriesAsRead({ seriesId: 'series-123' }, { client });
await markSeriesAsUnread({ seriesId: 'series-123' }, { client });
```

## OPDS progression

OPDS progression stores an exact Readium position and device metadata. Use it for EPUB readers; use page-based progress for comics and manga. `getBookProgression` can return no position when the user has never opened the book.

### Get a position

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

const result = await getBookProgression({ bookId: 'book-123' }, { client });

if (result.data) {
  console.log(`Position: ${result.data.locator.locations?.position ?? 0}`);
  console.log(`Progress: ${(result.data.locator.locations?.totalProgression ?? 0) * 100}%`);
  console.log(`Device: ${result.data.device.name}`);
}
```

### Save a position

`r2Progression` requires `device`, `locator`, and an ISO timestamp. The `locations` object requires `fragments`, even when it is empty; `position` and `totalProgression` are optional:

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

await updateBookProgression(
  {
    bookId: 'book-123',
    r2Progression: {
      device: { id: 'device-unique-id', name: 'Web Reader' },
      locator: {
        href: '/chapter1.xhtml',
        type: 'application/xhtml+xml',
        locations: {
          fragments: [],
          position: 1250,
          totalProgression: 0.45,
        },
      },
      modified: new Date().toISOString(),
    },
  },
  { client },
);
```

When multiple devices synchronize, keep the server's `modified` timestamp and only overwrite it when the local position is newer according to your application's conflict policy.

## Workflows

### Build a reading tracker

The current `getBooks` operation requires a `bookSearch` object. The lifecycle fields on `BookDto` make it possible to compute all three states without a second request:

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

async function getReadingStats() {
  const result = await getBooks(
    {
      unpaged: true,
      sort: ['metadata.title,asc'],
      bookSearch: {},
    },
    { client },
  );
  const books = result.data?.content ?? [];

  const stats = {
    total: result.data?.totalElements ?? books.length,
    unread: books.filter((book) => !book.readProgress).length,
    inProgress: books.filter((book) => book.readProgress && !book.readProgress.completed).length,
    completed: books.filter((book) => book.readProgress?.completed).length,
  };
  return stats;
}
```

### Resume in-progress books

`BookDto.readProgress` includes the current page, total page count is in `book.media.pagesCount`, `lastModified` tracks progress updates, and `readDate` is the completion timestamp when available:

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

async function getResumeQueue() {
  const result = await getBooks(
    {
      page: 0,
      size: 20,
      sort: ['readProgress.lastModified,desc'],
      bookSearch: {
        condition: { readStatus: { operator: 'is', value: 'IN_PROGRESS' } },
      },
    },
    { client },
  );

  return (result.data?.content ?? []).map((book) => ({
    id: book.id,
    title: book.metadata.title,
    page: book.readProgress?.page ?? 1,
    totalPages: book.media.pagesCount,
    lastRead: book.readProgress?.lastModified,
  }));
}
```

### Mark books in a series as read

Use `getBooks` with a `seriesId` condition so pagination and filtering use the current list operation. `BookDto.number` is numeric, unlike the display string in metadata:

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

async function markBooksAsReadUpTo(seriesId: string, upToNumber: number) {
  const result = await getBooks(
    {
      unpaged: true,
      sort: ['metadata.numberSort,asc'],
      bookSearch: {
        condition: { seriesId: { operator: 'is', value: seriesId } },
      },
    },
    { client },
  );

  for (const book of result.data?.content ?? []) {
    if (book.number <= upToNumber) {
      await markBookReadProgress(
        {
          bookId: book.id,
          readProgressUpdateDto: { completed: true },
        },
        { client },
      );
    }
  }
}
```

### Filter by read status

Read status values are `UNREAD`, `IN_PROGRESS`, and `READ`. Put the condition inside `bookSearch`; the removed top-level `readStatus` body field is not a 1.0 parameter:

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

const unread = await getBooks(
  {
    page: 0,
    size: 20,
    bookSearch: { condition: { readStatus: { operator: 'is', value: 'UNREAD' } } },
  },
  { client },
);

const inProgress = await getBooks(
  {
    page: 0,
    size: 20,
    bookSearch: { condition: { readStatus: { operator: 'is', value: 'IN_PROGRESS' } } },
  },
  { client },
);

const completed = await getBooks(
  {
    page: 0,
    size: 20,
    bookSearch: { condition: { readStatus: { operator: 'is', value: 'READ' } } },
  },
  { client },
);
```

### Sync a position across devices

Compare timestamps before writing a local position so an older mobile event does not overwrite a newer server position:

```typescript theme={null}
import { getBookProgression, updateBookProgression } from 'komga-sdk';

async function syncReadingPosition(bookId: string, position: number, totalProgression: number) {
  const current = await getBookProgression({ bookId }, { client });
  const serverModified = current.data?.modified ? new Date(current.data.modified) : new Date(0);
  const modified = new Date();
  if (modified <= serverModified) return current;

  return updateBookProgression(
    {
      bookId,
      r2Progression: {
        device: { id: 'web-reader', name: 'Web Reader' },
        locator: {
          href: '',
          type: 'application/octet-stream',
          locations: { fragments: [], position, totalProgression },
        },
        modified: modified.toISOString(),
      },
    },
    { client },
  );
}
```

## Handle failures

Generated calls do not throw for an HTTP error by default. Inspect `error` and `response`; a 401 means authentication is missing or invalid, while a 403 means the user lacks the operation's permission:

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

const result = await markBookReadProgress(
  { bookId: 'book-123', readProgressUpdateDto: { completed: true } },
  { client },
);

if (result.error) {
  console.error('Progress update failed', result.response?.status, result.error);
}
```

Use `unwrap` when your application prefers exceptions. It throws `KomgaApiError` for an unsuccessful response; see [Quickstart](/quickstart) for the throwing boundary.

## Next steps

> **[Books](/guides/books)**
>
> Learn more about book operations.

> **[Series](/guides/series)**
>
> Manage series and their metadata.


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