diff --git a/cli/src/utils.ts b/cli/src/utils.ts
index e63dc5ea4a..3b239bacc4 100644
--- a/cli/src/utils.ts
+++ b/cli/src/utils.ts
@@ -1,4 +1,4 @@
-import { defaults, getMyUserInfo, isHttpError } from '@immich/sdk';
+import { getMyUserInfo, init, isHttpError } from '@immich/sdk';
 import { glob } from 'fast-glob';
 import { createHash } from 'node:crypto';
 import { createReadStream } from 'node:fs';
@@ -46,8 +46,7 @@ export const connect = async (url: string, key: string) => {
     // noop
   }
 
-  defaults.baseUrl = url;
-  defaults.headers = { 'x-api-key': key };
+  init({ baseUrl: url, apiKey: key });
 
   const [error] = await withError(getMyUserInfo());
   if (isHttpError(error)) {
diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts
index 12dbac2f0a..9f001910fe 100644
--- a/e2e/src/utils.ts
+++ b/e2e/src/utils.ts
@@ -16,7 +16,6 @@ import {
   createPerson,
   createSharedLink,
   createUser,
-  defaults,
   deleteAssets,
   getAllAssets,
   getAllJobsStatus,
@@ -24,6 +23,7 @@ import {
   getConfigDefaults,
   login,
   searchMetadata,
+  setBaseUrl,
   signUpAdmin,
   updateAdminOnboarding,
   updateAlbumUser,
@@ -255,8 +255,8 @@ export const utils = {
     });
   },
 
-  setApiEndpoint: () => {
-    defaults.baseUrl = app;
+  initSdk: () => {
+    setBaseUrl(app);
   },
 
   adminSetup: async (options?: AdminSetupOptions) => {
@@ -462,7 +462,7 @@ export const utils = {
   },
 };
 
-utils.setApiEndpoint();
+utils.initSdk();
 
 if (!existsSync(`${testAssetDir}/albums`)) {
   throw new Error(
diff --git a/e2e/src/web/specs/auth.e2e-spec.ts b/e2e/src/web/specs/auth.e2e-spec.ts
index 73d62f1b10..ebafbf1f67 100644
--- a/e2e/src/web/specs/auth.e2e-spec.ts
+++ b/e2e/src/web/specs/auth.e2e-spec.ts
@@ -3,7 +3,7 @@ import { utils } from 'src/utils';
 
 test.describe('Registration', () => {
   test.beforeAll(() => {
-    utils.setApiEndpoint();
+    utils.initSdk();
   });
 
   test.beforeEach(async () => {
diff --git a/e2e/src/web/specs/shared-link.e2e-spec.ts b/e2e/src/web/specs/shared-link.e2e-spec.ts
index 3540ed72e2..8687306615 100644
--- a/e2e/src/web/specs/shared-link.e2e-spec.ts
+++ b/e2e/src/web/specs/shared-link.e2e-spec.ts
@@ -17,7 +17,7 @@ test.describe('Shared Links', () => {
   let sharedLinkPassword: SharedLinkResponseDto;
 
   test.beforeAll(async () => {
-    utils.setApiEndpoint();
+    utils.initSdk();
     await utils.resetDatabase();
     admin = await utils.adminSetup();
     asset = await utils.createAsset(admin.accessToken);
diff --git a/open-api/typescript-sdk/README.md b/open-api/typescript-sdk/README.md
index 53a10eefc5..9d74360b20 100644
--- a/open-api/typescript-sdk/README.md
+++ b/open-api/typescript-sdk/README.md
@@ -13,12 +13,11 @@ npm i --save @immich/sdk
 For a more detailed example, check out the [`@immich/cli`](https://github.com/immich-app/immich/tree/main/cli).
 
 ```typescript
-import { defaults, getAllAlbums, getAllAssets, getMyUserInfo } from "@immich/sdk";
+import { getAllAlbums, getAllAssets, getMyUserInfo, init } from "@immich/sdk";
 
 const API_KEY = "<API_KEY>"; // process.env.IMMICH_API_KEY
 
-defaults.baseUrl = "https://demo.immich.app/api";
-defaults.headers = { "x-api-key": API_KEY };
+init({ baseUrl: "https://demo.immich.app/api", apiKey: API_KEY });
 
 const user = await getMyUserInfo();
 const assets = await getAllAssets({ take: 1000 });
diff --git a/open-api/typescript-sdk/src/index.ts b/open-api/typescript-sdk/src/index.ts
index d81c7282ac..0a715c564b 100644
--- a/open-api/typescript-sdk/src/index.ts
+++ b/open-api/typescript-sdk/src/index.ts
@@ -1,2 +1,25 @@
+import { defaults } from './fetch-client.js';
+
 export * from './fetch-client.js';
 export * from './fetch-errors.js';
+
+export interface InitOptions {
+  baseUrl: string;
+  apiKey: string;
+}
+
+export const init = ({ baseUrl, apiKey }: InitOptions) => {
+  setBaseUrl(baseUrl);
+  setApiKey(apiKey);
+};
+
+export const getBaseUrl = () => defaults.baseUrl;
+
+export const setBaseUrl = (baseUrl: string) => {
+  defaults.baseUrl = baseUrl;
+};
+
+export const setApiKey = (apiKey: string) => {
+  defaults.headers = defaults.headers || {};
+  defaults.headers['x-api-key'] = apiKey;
+};
diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts
index fb61842b48..87cef15737 100644
--- a/web/src/lib/utils.ts
+++ b/web/src/lib/utils.ts
@@ -5,8 +5,8 @@ import {
   AssetJobName,
   JobName,
   ThumbnailFormat,
-  defaults,
   finishOAuth,
+  getBaseUrl,
   linkOAuthAccount,
   startOAuth,
   unlinkOAuthAccount,
@@ -155,7 +155,7 @@ const createUrl = (path: string, parameters?: Record<string, unknown>) => {
   const url = new URL(path, 'https://example.com');
   url.search = searchParameters.toString();
 
-  return defaults.baseUrl + url.pathname + url.search + url.hash;
+  return getBaseUrl() + url.pathname + url.search + url.hash;
 };
 
 export const getAssetFileUrl = (...[assetId, isWeb, isThumb]: [string, boolean, boolean]) => {
diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts
index bda0bb6ffe..7a8bff68b2 100644
--- a/web/src/lib/utils/asset-utils.ts
+++ b/web/src/lib/utils/asset-utils.ts
@@ -10,7 +10,7 @@ import { createAlbum } from '$lib/utils/album-utils';
 import { encodeHTMLSpecialChars } from '$lib/utils/string-utils';
 import {
   addAssetsToAlbum as addAssets,
-  defaults,
+  getBaseUrl,
   getDownloadInfo,
   updateAssets,
   type AlbumResponseDto,
@@ -121,7 +121,7 @@ export const downloadArchive = async (fileName: string, options: DownloadInfoDto
       // TODO use sdk once it supports progress events
       const { data } = await downloadRequest({
         method: 'POST',
-        url: defaults.baseUrl + '/download/archive' + (key ? `?key=${key}` : ''),
+        url: getBaseUrl() + '/download/archive' + (key ? `?key=${key}` : ''),
         data: { assetIds: archive.assetIds },
         signal: abort.signal,
         onDownloadProgress: (event) => downloadManager.update(downloadKey, event.loaded),
@@ -177,7 +177,7 @@ export const downloadFile = async (asset: AssetResponseDto) => {
       // TODO use sdk once it supports progress events
       const { data } = await downloadRequest({
         method: 'POST',
-        url: defaults.baseUrl + `/download/asset/${id}` + (key ? `?key=${key}` : ''),
+        url: getBaseUrl() + `/download/asset/${id}` + (key ? `?key=${key}` : ''),
         signal: abort.signal,
         onDownloadProgress: (event) => downloadManager.update(downloadKey, event.loaded, event.total),
       });
diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts
index ce7b18c2de..9472364d09 100644
--- a/web/src/lib/utils/file-uploader.ts
+++ b/web/src/lib/utils/file-uploader.ts
@@ -6,7 +6,7 @@ import { ExecutorQueue } from '$lib/utils/executor-queue';
 import {
   Action,
   checkBulkUpload,
-  defaults,
+  getBaseUrl,
   getSupportedMediaTypes,
   type AssetFileUploadResponseDto,
 } from '@immich/sdk';
@@ -119,7 +119,7 @@ async function fileUploader(asset: File, albumId: string | undefined = undefined
     if (!responseData) {
       uploadAssetsStore.updateAsset(deviceAssetId, { message: 'Uploading...' });
       const response = await uploadRequest<AssetFileUploadResponseDto>({
-        url: defaults.baseUrl + '/asset/upload' + (key ? `?key=${key}` : ''),
+        url: getBaseUrl() + '/asset/upload' + (key ? `?key=${key}` : ''),
         data: formData,
         onUploadProgress: (event) => uploadAssetsStore.updateProgress(deviceAssetId, event.loaded, event.total),
       });