diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts
index 2873bb0c3e..2bb0e7c4d1 100644
--- a/e2e/src/api/specs/asset.e2e-spec.ts
+++ b/e2e/src/api/specs/asset.e2e-spec.ts
@@ -7,13 +7,12 @@ import {
 } from '@immich/sdk';
 import { exiftool } from 'exiftool-vendored';
 import { DateTime } from 'luxon';
-import { createHash } from 'node:crypto';
 import { readFile, writeFile } from 'node:fs/promises';
 import { basename, join } from 'node:path';
 import { Socket } from 'socket.io-client';
 import { createUserDto, uuidDto } from 'src/fixtures';
 import { errorDto } from 'src/responses';
-import { apiUtils, app, dbUtils, tempDir, testAssetDir, wsUtils } from 'src/utils';
+import { apiUtils, app, dbUtils, fileUtils, tempDir, testAssetDir, wsUtils } from 'src/utils';
 import request from 'supertest';
 import { afterAll, beforeAll, describe, expect, it } from 'vitest';
 
@@ -21,8 +20,6 @@ const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
 
 const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
 
-const sha1 = (bytes: Buffer) => createHash('sha1').update(bytes).digest('base64');
-
 const readTags = async (bytes: Buffer, filename: string) => {
   const filepath = join(tempDir, filename);
   await writeFile(filepath, bytes);
@@ -739,8 +736,8 @@ describe('/asset', () => {
       const asset = await apiUtils.getAssetInfo(admin.accessToken, assetLocation.id);
 
       const original = await readFile(locationAssetFilepath);
-      const originalChecksum = sha1(original);
-      const downloadChecksum = sha1(body);
+      const originalChecksum = fileUtils.sha1(original);
+      const downloadChecksum = fileUtils.sha1(body);
 
       expect(originalChecksum).toBe(downloadChecksum);
       expect(downloadChecksum).toBe(asset.checksum);
diff --git a/e2e/src/api/specs/download.e2e-spec.ts b/e2e/src/api/specs/download.e2e-spec.ts
index cf4aae3e0a..af328934b4 100644
--- a/e2e/src/api/specs/download.e2e-spec.ts
+++ b/e2e/src/api/specs/download.e2e-spec.ts
@@ -1,18 +1,23 @@
 import { AssetFileUploadResponseDto, LoginResponseDto } from '@immich/sdk';
+import { readFile, writeFile } from 'node:fs/promises';
 import { errorDto } from 'src/responses';
-import { apiUtils, app, dbUtils } from 'src/utils';
+import { apiUtils, app, dbUtils, fileUtils, tempDir } from 'src/utils';
 import request from 'supertest';
 import { beforeAll, describe, expect, it } from 'vitest';
 
 describe('/download', () => {
   let admin: LoginResponseDto;
   let asset1: AssetFileUploadResponseDto;
+  let asset2: AssetFileUploadResponseDto;
 
   beforeAll(async () => {
     apiUtils.setup();
     await dbUtils.reset();
     admin = await apiUtils.adminSetup();
-    asset1 = await apiUtils.createAsset(admin.accessToken);
+    [asset1, asset2] = await Promise.all([
+      apiUtils.createAsset(admin.accessToken),
+      apiUtils.createAsset(admin.accessToken),
+    ]);
   });
 
   describe('POST /download/info', () => {
@@ -40,6 +45,39 @@ describe('/download', () => {
     });
   });
 
+  describe('POST /download/archive', () => {
+    it('should require authentication', async () => {
+      const { status, body } = await request(app)
+        .post(`/download/archive`)
+        .send({ assetIds: [asset1.id, asset2.id] });
+
+      expect(status).toBe(401);
+      expect(body).toEqual(errorDto.unauthorized);
+    });
+
+    it('should download an archive', async () => {
+      const { status, body } = await request(app)
+        .post('/download/archive')
+        .set('Authorization', `Bearer ${admin.accessToken}`)
+        .send({ assetIds: [asset1.id, asset2.id] });
+
+      expect(status).toBe(200);
+      expect(body instanceof Buffer).toBe(true);
+
+      await writeFile(`${tempDir}/archive.zip`, body);
+      await fileUtils.unzip(`${tempDir}/archive.zip`, `${tempDir}/archive`);
+      const files = [
+        { filename: 'example.png', id: asset1.id },
+        { filename: 'example+1.png', id: asset2.id },
+      ];
+      for (const { id, filename } of files) {
+        const bytes = await readFile(`${tempDir}/archive/${filename}`);
+        const asset = await apiUtils.getAssetInfo(admin.accessToken, id);
+        expect(fileUtils.sha1(bytes)).toBe(asset.checksum);
+      }
+    });
+  });
+
   describe('POST /download/asset/:id', () => {
     it('should require authentication', async () => {
       const { status, body } = await request(app).post(`/download/asset/${asset1.id}`);
diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts
index 34f25e396d..9be730c7e1 100644
--- a/e2e/src/utils.ts
+++ b/e2e/src/utils.ts
@@ -25,6 +25,7 @@ import {
 } from '@immich/sdk';
 import { BrowserContext } from '@playwright/test';
 import { exec, spawn } from 'node:child_process';
+import { createHash } from 'node:crypto';
 import { access } from 'node:fs/promises';
 import { tmpdir } from 'node:os';
 import path from 'node:path';
@@ -76,6 +77,10 @@ export const fileUtils = {
   reset: async () => {
     await execPromise(`docker exec -i "${serverContainerName}" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`);
   },
+  unzip: async (input: string, output: string) => {
+    await execPromise(`unzip -o -d "${output}" "${input}"`);
+  },
+  sha1: (bytes: Buffer) => createHash('sha1').update(bytes).digest('base64'),
 };
 
 export const dbUtils = {
@@ -150,7 +155,7 @@ export interface CliResponse {
 export const immichCli = async (args: string[]) => {
   let _resolve: (value: CliResponse) => void;
   const deferred = new Promise<CliResponse>((resolve) => (_resolve = resolve));
-  const _args = ['node_modules/.bin/immich', '-d', '/tmp/immich/', ...args];
+  const _args = ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args];
   const child = spawn('node', _args, {
     stdio: 'pipe',
   });