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

# Downloads

> Download books, series, and read lists as binary files

Download operations require the **FILE\_DOWNLOAD** role. Their generated response type is `StreamingResponseBody` (`unknown`) because Komga streams files. Pass `parseAs: 'blob'` in the options object when you need a browser `Blob`, then narrow `result.data` before using it.

## Download a book

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

const result = await downloadBookFile(
  { bookId: 'book-123' },
  { client, parseAs: 'blob' },
);
if (result.error) {
  console.error(result.response?.status, result.error);
} else if (result.data instanceof Blob) {
  const url = URL.createObjectURL(result.data);
  const link = document.createElement('a');
  link.href = url;
  link.download = 'book.cbz';
  link.click();
  URL.revokeObjectURL(url);
}
```

The wildcard route is exposed as `downloadBookFile1` and has the same flat signature. Use it only when your reverse proxy requires that route:

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

const result = await downloadBookFile1(
  { bookId: 'book-123' },
  { client, parseAs: 'blob' },
);
if (!result.error && result.data instanceof Blob) {
  console.log(`Received ${result.data.size} bytes`);
}
```

### Read the server filename

The generated result includes the native `Response` so you can read `Content-Disposition`:

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

const result = await downloadBookFile(
  { bookId: 'book-123' },
  { client, parseAs: 'blob' },
);
if (result.error || !(result.data instanceof Blob) || !result.response) {
  throw new Error('Book download failed');
}

const disposition = result.response.headers.get('Content-Disposition') ?? '';
const utf8 = disposition.match(/filename\*=UTF-8''([^;]+)/i);
const regular = disposition.match(/filename="?([^";]+)"?/i);
const filename = utf8
  ? decodeURIComponent(utf8[1])
  : regular?.[1] ?? 'download';

const url = URL.createObjectURL(result.data);
console.log(`Download ${filename} from ${url}`);
```

## Download archives

Download an entire series as a ZIP. The parameter is `seriesId`:

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

const result = await downloadSeriesAsZip(
  { seriesId: 'series-123' },
  { client, parseAs: 'blob' },
);
if (!result.error && result.data instanceof Blob) {
  console.log(`Series archive: ${result.data.size} bytes`);
}
```

Download a read list as a ZIP. The generated operation names the path parameter `id`:

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

const result = await downloadReadListAsZip(
  { id: 'readlist-123' },
  { client, parseAs: 'blob' },
);
if (!result.error && result.data instanceof Blob) {
  const bytes = await result.data.arrayBuffer();
  console.log(`Read list archive: ${bytes.byteLength} bytes`);
}
```

Large series and read lists are created on the server and may take time to produce. The SDK does not expose a progress event and does not add retries or caching. `parseAs: 'blob'` buffers the response, so use it for ordinary downloads; if you need incremental progress, provide an application-owned `fetch`/streaming transport and apply the same authentication policy outside the core SDK.

> [!WARNING]
>
> An archive request can be expensive for a large series or read list. Avoid firing duplicate requests from UI rerenders, and surface the server-side wait to users instead of treating a slow response or connection as an immediate failure.

## Save a download in Node.js

Node.js 18+ provides `Blob` and Fetch. Use `parseAs: 'blob'`, convert the result to an `ArrayBuffer`, and write it with the Node filesystem APIs:

```typescript theme={null}
import { writeFile } from 'node:fs/promises';
import { downloadBookFile } from 'komga-sdk';

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

if (result.error || !(result.data instanceof Blob)) {
  throw new Error(`Download failed (${result.response?.status ?? 'network error'})`);
}

await writeFile('book.cbz', Buffer.from(await result.data.arrayBuffer()));
```

For a server-side download service, keep the `KomgaClient` on the server so Basic credentials or API keys never enter a browser bundle. The SDK returns the native `Response` as `result.response`; use it for `Content-Type`, `Content-Length`, and `Content-Disposition` when proxying the file to another client.

## Batch and offline workflows

For several books, list the books first, then download sequentially or with a bounded worker pool. Sequential requests make back-pressure explicit and avoid exhausting the server's file-download role:

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

const listed = await getBooks(
  {
    page: 0,
    size: 20,
    sort: ['metadata.numberSort,asc'],
    bookSearch: {
      condition: {
        seriesId: { operator: 'is', value: 'series-123' },
      },
    },
  },
  { client },
);

const files: Array<{ bookId: string; file: Blob }> = [];
for (const book of listed.data?.content ?? []) {
  const downloaded = await downloadBookFile(
    { bookId: book.id },
    { client, parseAs: 'blob' },
  );
  if (!downloaded.error && downloaded.data instanceof Blob) {
    files.push({ bookId: book.id, file: downloaded.data });
  }
}
```

An offline package should retain the book metadata alongside the binary and should be prepared for a missing or changed book between the list and the download. For a single book, `getBookById` plus `getBookPages` can provide the metadata and page count before downloading; do not infer either from the archive bytes.

## Browser helper

Keep object URL cleanup in one helper:

```typescript theme={null}
function triggerDownload(file: Blob | File, filename: string): void {
  const url = URL.createObjectURL(file);
  const link = document.createElement('a');
  link.href = url;
  link.download = filename;
  document.body.appendChild(link);
  link.click();
  link.remove();
  URL.revokeObjectURL(url);
}
```

Use the helper with any binary result:

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

const result = await downloadBookFile(
  { bookId: 'book-123' },
  { client, parseAs: 'blob' },
);
if (!result.error && result.data instanceof Blob) {
  triggerDownload(result.data, 'my-book.cbz');
}
```

## Error handling

The default result is a typed success/error union. Status `401` means the client did not authenticate, `403` means the account lacks `FILE_DOWNLOAD`, and `404` usually means the requested ID is not available:

```typescript theme={null}
import { downloadBookFile, KomgaApiError, unwrap } from 'komga-sdk';

try {
  const file = unwrap(
    await downloadBookFile({ bookId: 'book-123' }, { client, parseAs: 'blob' }),
  );
  if (!(file instanceof Blob)) throw new Error('Expected a binary download');
  triggerDownload(file, 'book.cbz');
} catch (error) {
  if (error instanceof KomgaApiError) {
    switch (error.status) {
      case 401:
        console.error('Authenticate the client before downloading');
        break;
      case 403:
        console.error('The account needs FILE_DOWNLOAD');
        break;
      case 404:
        console.error('Book not found');
        break;
      default:
        console.error(error.body);
    }
  } else {
    throw error;
  }
}
```

## Next steps

> **[Books](/guides/books)**
>
> Browse and manage books.

> **[Read lists](/guides/read-lists)**
>
> Create reading lists for export.

> **[Authentication](/authentication)**
>
> Configure Basic or API-key authentication.


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