From 445abab14254a154c9c754f37cd76a144a6c839a Mon Sep 17 00:00:00 2001
From: Parkreiner <throwawayclover@gmail.com>
Date: Wed, 1 May 2024 14:44:53 +0000
Subject: [PATCH 1/7] wip: commit progress on code split-up

---
 site/src/api/api.ts | 183 +++++++++++++++++++++++++++++++++-----------
 1 file changed, 139 insertions(+), 44 deletions(-)

diff --git a/site/src/api/api.ts b/site/src/api/api.ts
index 6b4102073b3d1..323a41ce2dd8e 100644
--- a/site/src/api/api.ts
+++ b/site/src/api/api.ts
@@ -19,12 +19,150 @@
  *
  * For example, `utils/delay` must be imported using `../utils/delay` instead.
  */
-import globalAxios, { isAxiosError } from "axios";
+import globalAxios, { type AxiosInstance, isAxiosError } from "axios";
 import type dayjs from "dayjs";
 import userAgentParser from "ua-parser-js";
 import { delay } from "../utils/delay";
 import * as TypesGen from "./typesGenerated";
 
+////////////////////////////////////////////////////////////////////////////////
+// START OF API FILE
+////////////////////////////////////////////////////////////////////////////////
+
+// withDefaultFeatures sets all unspecified features to not_entitled and
+// disabled.
+export const withDefaultFeatures = (
+  fs: Partial<TypesGen.Entitlements["features"]>,
+): TypesGen.Entitlements["features"] => {
+  for (const feature of TypesGen.FeatureNames) {
+    // Skip fields that are already filled.
+    if (fs[feature] !== undefined) {
+      continue;
+    }
+
+    fs[feature] = {
+      enabled: false,
+      entitlement: "not_entitled",
+    };
+  }
+
+  return fs as TypesGen.Entitlements["features"];
+};
+
+const CONTENT_TYPE_JSON = {
+  "Content-Type": "application/json",
+} as const satisfies HeadersInit;
+
+export class Api {
+  constructor(private readonly axios: AxiosInstance) {}
+
+  login = async (
+    email: string,
+    password: string,
+  ): Promise<TypesGen.LoginWithPasswordResponse> => {
+    const payload = JSON.stringify({
+      email,
+      password,
+    });
+
+    const response = await this.axios.post<TypesGen.LoginWithPasswordResponse>(
+      "/api/v2/users/login",
+      payload,
+      { headers: CONTENT_TYPE_JSON },
+    );
+
+    return response.data;
+  };
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// START OF CLIENT FILE
+////////////////////////////////////////////////////////////////////////////////
+
+// This is a hard coded CSRF token/cookie pair for local development. In prod,
+// the GoLang webserver generates a random cookie with a new token for each
+// document request. For local development, we don't use the Go webserver for
+// static files, so this is the 'hack' to make local development work with
+// remote apis. The CSRF cookie for this token is "JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4="
+const csrfToken =
+  "KNKvagCBEHZK7ihe2t7fj6VeJ0UyTDco1yVUJE8N06oNqxLu5Zx1vRxZbgfC0mJJgeGkVjgs08mgPbcWPBkZ1A==";
+
+// Always attach CSRF token to all requests. In puppeteer the document is
+// undefined. In those cases, just do nothing.
+const tokenMetadataElement =
+  typeof document !== "undefined"
+    ? document.head.querySelector('meta[property="csrf-token"]')
+    : null;
+
+interface ClientApi {
+  api: Api;
+  getCsrfToken: () => string;
+  setSessionToken: (token: string) => void;
+  setHost: (host: string | undefined) => void;
+}
+
+export class Client implements ClientApi {
+  private readonly axios: AxiosInstance;
+  readonly api: Api;
+
+  constructor() {
+    this.axios = this.getConfiguredAxiosInstance();
+    this.api = new Api(this.axios);
+  }
+
+  private getConfiguredAxiosInstance(): AxiosInstance {
+    const axios = globalAxios.create();
+
+    // Adds 304 for the default axios validateStatus function
+    // https://github.com/axios/axios#handling-errors Check status here
+    // https://httpstatusdogs.com/
+    axios.defaults.validateStatus = (status) => {
+      return (status >= 200 && status < 300) || status === 304;
+    };
+
+    const metadataIsAvailable =
+      tokenMetadataElement !== null &&
+      tokenMetadataElement.getAttribute("content") !== null;
+
+    if (metadataIsAvailable) {
+      if (process.env.NODE_ENV === "development") {
+        // Development mode uses a hard-coded CSRF token
+        this.axios.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken;
+        axios.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken;
+        tokenMetadataElement.setAttribute("content", csrfToken);
+      } else {
+        axios.defaults.headers.common["X-CSRF-TOKEN"] =
+          tokenMetadataElement.getAttribute("content") ?? "";
+      }
+    } else {
+      // Do not write error logs if we are in a FE unit test.
+      if (process.env.JEST_WORKER_ID === undefined) {
+        console.error("CSRF token not found");
+      }
+    }
+
+    return axios;
+  }
+
+  getCsrfToken = (): string => {
+    return csrfToken;
+  };
+
+  setSessionToken = (token: string): void => {
+    this.axios.defaults.headers.common["Coder-Session-Token"] = token;
+  };
+
+  setHost = (host: string | undefined): void => {
+    this.axios.defaults.baseURL = host;
+  };
+}
+
+export const client = new Client();
+
+////////////////////////////////////////////////////////////////////////////////
+// START OF OLD CODE
+////////////////////////////////////////////////////////////////////////////////
+
 export const axiosInstance = globalAxios.create();
 
 // Adds 304 for the default axios validateStatus function
@@ -47,24 +185,6 @@ export const hardCodedCSRFCookie = (): string => {
   return csrfToken;
 };
 
-// withDefaultFeatures sets all unspecified features to not_entitled and
-// disabled.
-export const withDefaultFeatures = (
-  fs: Partial<TypesGen.Entitlements["features"]>,
-): TypesGen.Entitlements["features"] => {
-  for (const feature of TypesGen.FeatureNames) {
-    // Skip fields that are already filled.
-    if (fs[feature] !== undefined) {
-      continue;
-    }
-    fs[feature] = {
-      enabled: false,
-      entitlement: "not_entitled",
-    };
-  }
-  return fs as TypesGen.Entitlements["features"];
-};
-
 // Always attach CSRF token to all requests. In puppeteer the document is
 // undefined. In those cases, just do nothing.
 const token =
@@ -97,31 +217,6 @@ export const setHost = (host?: string) => {
   axiosInstance.defaults.baseURL = host;
 };
 
-const CONTENT_TYPE_JSON = {
-  "Content-Type": "application/json",
-};
-
-export const provisioners: TypesGen.ProvisionerDaemon[] = [
-  {
-    id: "terraform",
-    name: "Terraform",
-    created_at: "",
-    provisioners: [],
-    tags: {},
-    version: "v2.34.5",
-    api_version: "1.0",
-  },
-  {
-    id: "cdr-basic",
-    name: "Basic",
-    created_at: "",
-    provisioners: [],
-    tags: {},
-    version: "v2.34.5",
-    api_version: "1.0",
-  },
-];
-
 export const login = async (
   email: string,
   password: string,

From 08849639284d7ce1f151c9c70e4532735285f1b1 Mon Sep 17 00:00:00 2001
From: Parkreiner <throwawayclover@gmail.com>
Date: Wed, 1 May 2024 15:49:04 +0000
Subject: [PATCH 2/7] wip: commit more progress

---
 site/src/api/api.ts | 712 ++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 653 insertions(+), 59 deletions(-)

diff --git a/site/src/api/api.ts b/site/src/api/api.ts
index 323a41ce2dd8e..873d0addf4bdd 100644
--- a/site/src/api/api.ts
+++ b/site/src/api/api.ts
@@ -29,6 +29,38 @@ import * as TypesGen from "./typesGenerated";
 // START OF API FILE
 ////////////////////////////////////////////////////////////////////////////////
 
+/**
+ * @returns {EventSource} An EventSource that emits workspace event objects
+ * (ServerSentEvent)
+ */
+export const watchWorkspace = (workspaceId: string): EventSource => {
+  return new EventSource(
+    `${location.protocol}//${location.host}/api/v2/workspaces/${workspaceId}/watch`,
+    { withCredentials: true },
+  );
+};
+
+export const getURLWithSearchParams = (
+  basePath: string,
+  options?: SearchParamOptions,
+): string => {
+  if (!options) {
+    return basePath;
+  }
+
+  const searchParams = new URLSearchParams();
+  const keys = Object.keys(options) as (keyof SearchParamOptions)[];
+  keys.forEach((key) => {
+    const value = options[key];
+    if (value !== undefined && value !== "") {
+      searchParams.append(key, value.toString());
+    }
+  });
+
+  const searchString = searchParams.toString();
+  return searchString ? `${basePath}?${searchString}` : basePath;
+};
+
 // withDefaultFeatures sets all unspecified features to not_entitled and
 // disabled.
 export const withDefaultFeatures = (
@@ -49,10 +81,31 @@ export const withDefaultFeatures = (
   return fs as TypesGen.Entitlements["features"];
 };
 
-const CONTENT_TYPE_JSON = {
+// This is the base header that is used for several requests. This is defined as
+// a readonly value, but only copies of it should be passed into the API calls,
+// because Axios is able to mutate the headers
+const BASE_CONTENT_TYPE_JSON = {
   "Content-Type": "application/json",
 } as const satisfies HeadersInit;
 
+type TemplateOptions = Readonly<{
+  readonly deprecated?: boolean;
+}>;
+
+type SearchParamOptions = TypesGen.Pagination & {
+  q?: string;
+};
+
+type RestartWorkspaceParameters = Readonly<{
+  workspace: TypesGen.Workspace;
+  buildParameters?: TypesGen.WorkspaceBuildParameter[];
+}>;
+
+export type DeleteWorkspaceOptions = Pick<
+  TypesGen.CreateWorkspaceBuildRequest,
+  "log_level" & "orphan"
+>;
+
 export class Api {
   constructor(private readonly axios: AxiosInstance) {}
 
@@ -60,19 +113,609 @@ export class Api {
     email: string,
     password: string,
   ): Promise<TypesGen.LoginWithPasswordResponse> => {
-    const payload = JSON.stringify({
-      email,
-      password,
-    });
-
+    const payload = JSON.stringify({ email, password });
     const response = await this.axios.post<TypesGen.LoginWithPasswordResponse>(
       "/api/v2/users/login",
       payload,
-      { headers: CONTENT_TYPE_JSON },
+      { headers: { ...BASE_CONTENT_TYPE_JSON } },
+    );
+
+    return response.data;
+  };
+
+  convertToOAUTH = async (request: TypesGen.ConvertLoginRequest) => {
+    const response = await this.axios.post<TypesGen.OAuthConversionResponse>(
+      "/api/v2/users/me/convert-login",
+      request,
+    );
+
+    return response.data;
+  };
+
+  logout = async (): Promise<void> => {
+    return this.axios.post("/api/v2/users/logout");
+  };
+
+  getAuthenticatedUser = async () => {
+    const response = await this.axios.get<TypesGen.User>("/api/v2/users/me");
+    return response.data;
+  };
+
+  getUserParameters = async (templateID: string) => {
+    const response = await this.axios.get<TypesGen.UserParameter[]>(
+      `/api/v2/users/me/autofill-parameters?template_id=${templateID}`,
+    );
+
+    return response.data;
+  };
+
+  getAuthMethods = async (): Promise<TypesGen.AuthMethods> => {
+    const response = await this.axios.get<TypesGen.AuthMethods>(
+      "/api/v2/users/authmethods",
+    );
+
+    return response.data;
+  };
+
+  getUserLoginType = async (): Promise<TypesGen.UserLoginType> => {
+    const response = await this.axios.get<TypesGen.UserLoginType>(
+      "/api/v2/users/me/login-type",
+    );
+
+    return response.data;
+  };
+
+  checkAuthorization = async (
+    params: TypesGen.AuthorizationRequest,
+  ): Promise<TypesGen.AuthorizationResponse> => {
+    const response = await this.axios.post<TypesGen.AuthorizationResponse>(
+      `/api/v2/authcheck`,
+      params,
+    );
+
+    return response.data;
+  };
+
+  getApiKey = async (): Promise<TypesGen.GenerateAPIKeyResponse> => {
+    const response = await this.axios.post<TypesGen.GenerateAPIKeyResponse>(
+      "/api/v2/users/me/keys",
+    );
+
+    return response.data;
+  };
+
+  getTokens = async (
+    params: TypesGen.TokensFilter,
+  ): Promise<TypesGen.APIKeyWithOwner[]> => {
+    const response = await this.axios.get<TypesGen.APIKeyWithOwner[]>(
+      `/api/v2/users/me/keys/tokens`,
+      { params },
+    );
+
+    return response.data;
+  };
+
+  deleteToken = async (keyId: string): Promise<void> => {
+    await this.axios.delete("/api/v2/users/me/keys/" + keyId);
+  };
+
+  createToken = async (
+    params: TypesGen.CreateTokenRequest,
+  ): Promise<TypesGen.GenerateAPIKeyResponse> => {
+    const response = await this.axios.post(
+      `/api/v2/users/me/keys/tokens`,
+      params,
+    );
+
+    return response.data;
+  };
+
+  getTokenConfig = async (): Promise<TypesGen.TokenConfig> => {
+    const response = await this.axios.get(
+      "/api/v2/users/me/keys/tokens/tokenconfig",
+    );
+
+    return response.data;
+  };
+
+  getUsers = async (
+    options: TypesGen.UsersRequest,
+    signal?: AbortSignal,
+  ): Promise<TypesGen.GetUsersResponse> => {
+    const url = getURLWithSearchParams("/api/v2/users", options);
+    const response = await this.axios.get<TypesGen.GetUsersResponse>(
+      url.toString(),
+      { signal },
+    );
+
+    return response.data;
+  };
+
+  getOrganization = async (
+    organizationId: string,
+  ): Promise<TypesGen.Organization> => {
+    const response = await this.axios.get<TypesGen.Organization>(
+      `/api/v2/organizations/${organizationId}`,
+    );
+
+    return response.data;
+  };
+
+  getOrganizations = async (): Promise<TypesGen.Organization[]> => {
+    const response = await this.axios.get<TypesGen.Organization[]>(
+      "/api/v2/users/me/organizations",
+    );
+    return response.data;
+  };
+
+  getTemplate = async (templateId: string): Promise<TypesGen.Template> => {
+    const response = await this.axios.get<TypesGen.Template>(
+      `/api/v2/templates/${templateId}`,
+    );
+
+    return response.data;
+  };
+
+  getTemplates = async (
+    organizationId: string,
+    options?: TemplateOptions,
+  ): Promise<TypesGen.Template[]> => {
+    const params: Record<string, string> = {};
+    if (options?.deprecated !== undefined) {
+      // Just want to check if it isn't undefined. If it has
+      // a boolean value, convert it to a string and include
+      // it as a param.
+      params["deprecated"] = String(options.deprecated);
+    }
+
+    const response = await this.axios.get<TypesGen.Template[]>(
+      `/api/v2/organizations/${organizationId}/templates`,
+      { params },
+    );
+
+    return response.data;
+  };
+
+  getTemplateByName = async (
+    organizationId: string,
+    name: string,
+  ): Promise<TypesGen.Template> => {
+    const response = await this.axios.get<TypesGen.Template>(
+      `/api/v2/organizations/${organizationId}/templates/${name}`,
+    );
+
+    return response.data;
+  };
+
+  getTemplateVersion = async (
+    versionId: string,
+  ): Promise<TypesGen.TemplateVersion> => {
+    const response = await this.axios.get<TypesGen.TemplateVersion>(
+      `/api/v2/templateversions/${versionId}`,
+    );
+
+    return response.data;
+  };
+
+  getTemplateVersionResources = async (
+    versionId: string,
+  ): Promise<TypesGen.WorkspaceResource[]> => {
+    const response = await this.axios.get<TypesGen.WorkspaceResource[]>(
+      `/api/v2/templateversions/${versionId}/resources`,
+    );
+
+    return response.data;
+  };
+
+  getTemplateVersionVariables = async (
+    versionId: string,
+  ): Promise<TypesGen.TemplateVersionVariable[]> => {
+    // Defined as separate variable to avoid wonky Prettier formatting because
+    // the type definition is so long
+    type VerArray = TypesGen.TemplateVersionVariable[];
+
+    const response = await axiosInstance.get<VerArray>(
+      `/api/v2/templateversions/${versionId}/variables`,
+    );
+
+    return response.data;
+  };
+
+  getTemplateVersions = async (
+    templateId: string,
+  ): Promise<TypesGen.TemplateVersion[]> => {
+    const response = await this.axios.get<TypesGen.TemplateVersion[]>(
+      `/api/v2/templates/${templateId}/versions`,
+    );
+    return response.data;
+  };
+
+  getTemplateVersionByName = async (
+    organizationId: string,
+    templateName: string,
+    versionName: string,
+  ): Promise<TypesGen.TemplateVersion> => {
+    const response = await this.axios.get<TypesGen.TemplateVersion>(
+      `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}`,
+    );
+
+    return response.data;
+  };
+
+  getPreviousTemplateVersionByName = async (
+    organizationId: string,
+    templateName: string,
+    versionName: string,
+  ) => {
+    try {
+      const response = await this.axios.get<TypesGen.TemplateVersion>(
+        `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}/previous`,
+      );
+
+      return response.data;
+    } catch (error) {
+      // When there is no previous version, like the first version of a
+      // template, the API returns 404 so in this case we can safely return
+      // undefined
+      const is404 =
+        isAxiosError(error) && error.response && error.response.status === 404;
+
+      if (is404) {
+        return undefined;
+      }
+
+      throw error;
+    }
+  };
+
+  createTemplateVersion = async (
+    organizationId: string,
+    data: TypesGen.CreateTemplateVersionRequest,
+  ): Promise<TypesGen.TemplateVersion> => {
+    const response = await this.axios.post<TypesGen.TemplateVersion>(
+      `/api/v2/organizations/${organizationId}/templateversions`,
+      data,
+    );
+
+    return response.data;
+  };
+
+  getTemplateVersionExternalAuth = async (
+    versionId: string,
+  ): Promise<TypesGen.TemplateVersionExternalAuth[]> => {
+    const response = await this.axios.get(
+      `/api/v2/templateversions/${versionId}/external-auth`,
+    );
+
+    return response.data;
+  };
+
+  getTemplateVersionRichParameters = async (
+    versionId: string,
+  ): Promise<TypesGen.TemplateVersionParameter[]> => {
+    const response = await this.axios.get(
+      `/api/v2/templateversions/${versionId}/rich-parameters`,
+    );
+    return response.data;
+  };
+
+  createTemplate = async (
+    organizationId: string,
+    data: TypesGen.CreateTemplateRequest,
+  ): Promise<TypesGen.Template> => {
+    const response = await this.axios.post(
+      `/api/v2/organizations/${organizationId}/templates`,
+      data,
+    );
+
+    return response.data;
+  };
+
+  updateActiveTemplateVersion = async (
+    templateId: string,
+    data: TypesGen.UpdateActiveTemplateVersion,
+  ) => {
+    const response = await this.axios.patch<TypesGen.Response>(
+      `/api/v2/templates/${templateId}/versions`,
+      data,
+    );
+    return response.data;
+  };
+
+  patchTemplateVersion = async (
+    templateVersionId: string,
+    data: TypesGen.PatchTemplateVersionRequest,
+  ) => {
+    const response = await this.axios.patch<TypesGen.TemplateVersion>(
+      `/api/v2/templateversions/${templateVersionId}`,
+      data,
     );
 
     return response.data;
   };
+
+  archiveTemplateVersion = async (templateVersionId: string) => {
+    const response = await this.axios.post<TypesGen.TemplateVersion>(
+      `/api/v2/templateversions/${templateVersionId}/archive`,
+    );
+
+    return response.data;
+  };
+
+  unarchiveTemplateVersion = async (templateVersionId: string) => {
+    const response = await this.axios.post<TypesGen.TemplateVersion>(
+      `/api/v2/templateversions/${templateVersionId}/unarchive`,
+    );
+    return response.data;
+  };
+
+  updateTemplateMeta = async (
+    templateId: string,
+    data: TypesGen.UpdateTemplateMeta,
+  ): Promise<TypesGen.Template | null> => {
+    const response = await this.axios.patch<TypesGen.Template>(
+      `/api/v2/templates/${templateId}`,
+      data,
+    );
+
+    // On 304 response there is no data payload.
+    if (response.status === 304) {
+      return null;
+    }
+
+    return response.data;
+  };
+
+  deleteTemplate = async (templateId: string): Promise<TypesGen.Template> => {
+    const response = await this.axios.delete<TypesGen.Template>(
+      `/api/v2/templates/${templateId}`,
+    );
+
+    return response.data;
+  };
+
+  getWorkspace = async (
+    workspaceId: string,
+    params?: TypesGen.WorkspaceOptions,
+  ): Promise<TypesGen.Workspace> => {
+    const response = await this.axios.get<TypesGen.Workspace>(
+      `/api/v2/workspaces/${workspaceId}`,
+      { params },
+    );
+
+    return response.data;
+  };
+
+  getWorkspaces = async (
+    options: TypesGen.WorkspacesRequest,
+  ): Promise<TypesGen.WorkspacesResponse> => {
+    const url = getURLWithSearchParams("/api/v2/workspaces", options);
+    const response = await this.axios.get<TypesGen.WorkspacesResponse>(url);
+    return response.data;
+  };
+
+  getWorkspaceByOwnerAndName = async (
+    username = "me",
+    workspaceName: string,
+    params?: TypesGen.WorkspaceOptions,
+  ): Promise<TypesGen.Workspace> => {
+    const response = await this.axios.get<TypesGen.Workspace>(
+      `/api/v2/users/${username}/workspace/${workspaceName}`,
+      { params },
+    );
+
+    return response.data;
+  };
+
+  getWorkspaceBuildByNumber = async (
+    username = "me",
+    workspaceName: string,
+    buildNumber: number,
+  ): Promise<TypesGen.WorkspaceBuild> => {
+    const response = await this.axios.get<TypesGen.WorkspaceBuild>(
+      `/api/v2/users/${username}/workspace/${workspaceName}/builds/${buildNumber}`,
+    );
+
+    return response.data;
+  };
+
+  waitForBuild = (build: TypesGen.WorkspaceBuild) => {
+    return new Promise<TypesGen.ProvisionerJob | undefined>((res, reject) => {
+      void (async () => {
+        let latestJobInfo: TypesGen.ProvisionerJob | undefined = undefined;
+
+        while (
+          !["succeeded", "canceled"].some(
+            (status) => latestJobInfo?.status.includes(status),
+          )
+        ) {
+          const { job } = await this.getWorkspaceBuildByNumber(
+            build.workspace_owner_name,
+            build.workspace_name,
+            build.build_number,
+          );
+
+          latestJobInfo = job;
+          if (latestJobInfo.status === "failed") {
+            return reject(latestJobInfo);
+          }
+
+          await delay(1000);
+        }
+
+        return res(latestJobInfo);
+      })();
+    });
+  };
+
+  postWorkspaceBuild = async (
+    workspaceId: string,
+    data: TypesGen.CreateWorkspaceBuildRequest,
+  ): Promise<TypesGen.WorkspaceBuild> => {
+    const response = await this.axios.post(
+      `/api/v2/workspaces/${workspaceId}/builds`,
+      data,
+    );
+
+    return response.data;
+  };
+
+  startWorkspace = (
+    workspaceId: string,
+    templateVersionId: string,
+    logLevel?: TypesGen.ProvisionerLogLevel,
+    buildParameters?: TypesGen.WorkspaceBuildParameter[],
+  ) => {
+    return this.postWorkspaceBuild(workspaceId, {
+      transition: "start",
+      template_version_id: templateVersionId,
+      log_level: logLevel,
+      rich_parameter_values: buildParameters,
+    });
+  };
+
+  stopWorkspace = (
+    workspaceId: string,
+    logLevel?: TypesGen.ProvisionerLogLevel,
+  ) => {
+    return this.postWorkspaceBuild(workspaceId, {
+      transition: "stop",
+      log_level: logLevel,
+    });
+  };
+
+  deleteWorkspace = (workspaceId: string, options?: DeleteWorkspaceOptions) => {
+    return postWorkspaceBuild(workspaceId, {
+      transition: "delete",
+      ...options,
+    });
+  };
+
+  cancelWorkspaceBuild = async (
+    workspaceBuildId: TypesGen.WorkspaceBuild["id"],
+  ): Promise<TypesGen.Response> => {
+    const response = await this.axios.patch(
+      `/api/v2/workspacebuilds/${workspaceBuildId}/cancel`,
+    );
+
+    return response.data;
+  };
+
+  updateWorkspaceDormancy = async (
+    workspaceId: string,
+    dormant: boolean,
+  ): Promise<TypesGen.Workspace> => {
+    const data: TypesGen.UpdateWorkspaceDormancy = { dormant };
+    const response = await this.axios.put(
+      `/api/v2/workspaces/${workspaceId}/dormant`,
+      data,
+    );
+
+    return response.data;
+  };
+
+  updateWorkspaceAutomaticUpdates = async (
+    workspaceId: string,
+    automaticUpdates: TypesGen.AutomaticUpdates,
+  ): Promise<void> => {
+    const req: TypesGen.UpdateWorkspaceAutomaticUpdatesRequest = {
+      automatic_updates: automaticUpdates,
+    };
+
+    const response = await this.axios.put(
+      `/api/v2/workspaces/${workspaceId}/autoupdates`,
+      req,
+    );
+
+    return response.data;
+  };
+
+  restartWorkspace = async ({
+    workspace,
+    buildParameters,
+  }: RestartWorkspaceParameters): Promise<void> => {
+    const stopBuild = await this.stopWorkspace(workspace.id);
+    const awaitedStopBuild = await this.waitForBuild(stopBuild);
+
+    // If the restart is canceled halfway through, make sure we bail
+    if (awaitedStopBuild?.status === "canceled") {
+      return;
+    }
+
+    const startBuild = await this.startWorkspace(
+      workspace.id,
+      workspace.latest_build.template_version_id,
+      undefined,
+      buildParameters,
+    );
+
+    await waitForBuild(startBuild);
+  };
+
+  cancelTemplateVersionBuild = async (
+    templateVersionId: TypesGen.TemplateVersion["id"],
+  ): Promise<TypesGen.Response> => {
+    const response = await axiosInstance.patch(
+      `/api/v2/templateversions/${templateVersionId}/cancel`,
+    );
+
+    return response.data;
+  };
+
+  createUser = async (
+    user: TypesGen.CreateUserRequest,
+  ): Promise<TypesGen.User> => {
+    const response = await this.axios.post<TypesGen.User>(
+      "/api/v2/users",
+      user,
+    );
+
+    return response.data;
+  };
+
+  createWorkspace = async (
+    organizationId: string,
+    userId = "me",
+    workspace: TypesGen.CreateWorkspaceRequest,
+  ): Promise<TypesGen.Workspace> => {
+    const response = await this.axios.post<TypesGen.Workspace>(
+      `/api/v2/organizations/${organizationId}/members/${userId}/workspaces`,
+      workspace,
+    );
+
+    return response.data;
+  };
+
+  patchWorkspace = async (
+    workspaceId: string,
+    data: TypesGen.UpdateWorkspaceRequest,
+  ): Promise<void> => {
+    await this.axios.patch(`/api/v2/workspaces/${workspaceId}`, data);
+  };
+
+  getBuildInfo = async (): Promise<TypesGen.BuildInfoResponse> => {
+    const response = await this.axios.get("/api/v2/buildinfo");
+    return response.data;
+  };
+
+  getUpdateCheck = async (): Promise<TypesGen.UpdateCheckResponse> => {
+    const response = await axiosInstance.get("/api/v2/updatecheck");
+    return response.data;
+  };
+
+  putWorkspaceAutostart = async (
+    workspaceID: string,
+    autostart: TypesGen.UpdateWorkspaceAutostartRequest,
+  ): Promise<void> => {
+    const payload = JSON.stringify(autostart);
+    await this.axios.put(
+      `/api/v2/workspaces/${workspaceID}/autostart`,
+      payload,
+      {
+        headers: { ...BASE_CONTENT_TYPE_JSON },
+      },
+    );
+  };
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -230,7 +873,7 @@ export const login = async (
     "/api/v2/users/login",
     payload,
     {
-      headers: { ...CONTENT_TYPE_JSON },
+      headers: { ...BASE_CONTENT_TYPE_JSON },
     },
   );
 
@@ -364,10 +1007,6 @@ export const getTemplate = async (
   return response.data;
 };
 
-export interface TemplateOptions {
-  readonly deprecated?: boolean;
-}
-
 export const getTemplates = async (
   organizationId: string,
   options?: TemplateOptions,
@@ -446,10 +1085,6 @@ export const getTemplateVersionByName = async (
   return response.data;
 };
 
-export type GetPreviousTemplateVersionByNameResponse =
-  | TypesGen.TemplateVersion
-  | undefined;
-
 export const getPreviousTemplateVersionByName = async (
   organizationId: string,
   templateName: string,
@@ -589,42 +1224,6 @@ export const getWorkspace = async (
   return response.data;
 };
 
-/**
- *
- * @param workspaceId
- * @returns An EventSource that emits workspace event objects (ServerSentEvent)
- */
-export const watchWorkspace = (workspaceId: string): EventSource => {
-  return new EventSource(
-    `${location.protocol}//${location.host}/api/v2/workspaces/${workspaceId}/watch`,
-    { withCredentials: true },
-  );
-};
-
-interface SearchParamOptions extends TypesGen.Pagination {
-  q?: string;
-}
-
-export const getURLWithSearchParams = (
-  basePath: string,
-  options?: SearchParamOptions,
-): string => {
-  if (options) {
-    const searchParams = new URLSearchParams();
-    const keys = Object.keys(options) as (keyof SearchParamOptions)[];
-    keys.forEach((key) => {
-      const value = options[key];
-      if (value !== undefined && value !== "") {
-        searchParams.append(key, value.toString());
-      }
-    });
-    const searchString = searchParams.toString();
-    return searchString ? `${basePath}?${searchString}` : basePath;
-  } else {
-    return basePath;
-  }
-};
-
 export const getWorkspaces = async (
   options: TypesGen.WorkspacesRequest,
 ): Promise<TypesGen.WorkspacesResponse> => {
@@ -708,11 +1307,6 @@ export const stopWorkspace = (
     log_level: logLevel,
   });
 
-export type DeleteWorkspaceOptions = Pick<
-  TypesGen.CreateWorkspaceBuildRequest,
-  "log_level" & "orphan"
->;
-
 export const deleteWorkspace = (
   workspaceId: string,
   options?: DeleteWorkspaceOptions,
@@ -843,7 +1437,7 @@ export const putWorkspaceAutostart = async (
     `/api/v2/workspaces/${workspaceID}/autostart`,
     payload,
     {
-      headers: { ...CONTENT_TYPE_JSON },
+      headers: { ...BASE_CONTENT_TYPE_JSON },
     },
   );
 };
@@ -854,7 +1448,7 @@ export const putWorkspaceAutostop = async (
 ): Promise<void> => {
   const payload = JSON.stringify(ttl);
   await axiosInstance.put(`/api/v2/workspaces/${workspaceID}/ttl`, payload, {
-    headers: { ...CONTENT_TYPE_JSON },
+    headers: { ...BASE_CONTENT_TYPE_JSON },
   });
 };
 

From 84b74061133b458e149e57d4c7a7076200225705 Mon Sep 17 00:00:00 2001
From: Parkreiner <throwawayclover@gmail.com>
Date: Wed, 1 May 2024 16:42:00 +0000
Subject: [PATCH 3/7] wip: finish initial version of class implementation

---
 site/src/api/api.ts | 2145 ++++++++++++++++++++++++++++++-------------
 1 file changed, 1512 insertions(+), 633 deletions(-)

diff --git a/site/src/api/api.ts b/site/src/api/api.ts
index 873d0addf4bdd..976a0ba984895 100644
--- a/site/src/api/api.ts
+++ b/site/src/api/api.ts
@@ -29,6 +29,92 @@ import * as TypesGen from "./typesGenerated";
 // START OF API FILE
 ////////////////////////////////////////////////////////////////////////////////
 
+const getMissingParameters = (
+  oldBuildParameters: TypesGen.WorkspaceBuildParameter[],
+  newBuildParameters: TypesGen.WorkspaceBuildParameter[],
+  templateParameters: TypesGen.TemplateVersionParameter[],
+) => {
+  const missingParameters: TypesGen.TemplateVersionParameter[] = [];
+  const requiredParameters: TypesGen.TemplateVersionParameter[] = [];
+
+  templateParameters.forEach((p) => {
+    // It is mutable and required. Mutable values can be changed after so we
+    // don't need to ask them if they are not required.
+    const isMutableAndRequired = p.mutable && p.required;
+    // Is immutable, so we can check if it is its first time on the build
+    const isImmutable = !p.mutable;
+
+    if (isMutableAndRequired || isImmutable) {
+      requiredParameters.push(p);
+    }
+  });
+
+  for (const parameter of requiredParameters) {
+    // Check if there is a new value
+    let buildParameter = newBuildParameters.find(
+      (p) => p.name === parameter.name,
+    );
+
+    // If not, get the old one
+    if (!buildParameter) {
+      buildParameter = oldBuildParameters.find(
+        (p) => p.name === parameter.name,
+      );
+    }
+
+    // If there is a value from the new or old one, it is not missed
+    if (buildParameter) {
+      continue;
+    }
+
+    missingParameters.push(parameter);
+  }
+
+  // Check if parameter "options" changed and we can't use old build parameters.
+  templateParameters.forEach((templateParameter) => {
+    if (templateParameter.options.length === 0) {
+      return;
+    }
+
+    // Check if there is a new value
+    let buildParameter = newBuildParameters.find(
+      (p) => p.name === templateParameter.name,
+    );
+
+    // If not, get the old one
+    if (!buildParameter) {
+      buildParameter = oldBuildParameters.find(
+        (p) => p.name === templateParameter.name,
+      );
+    }
+
+    if (!buildParameter) {
+      return;
+    }
+
+    const matchingOption = templateParameter.options.find(
+      (option) => option.value === buildParameter?.value,
+    );
+    if (!matchingOption) {
+      missingParameters.push(templateParameter);
+    }
+  });
+  return missingParameters;
+};
+
+/**
+ *
+ * @param agentId
+ * @returns An EventSource that emits agent metadata event objects
+ * (ServerSentEvent)
+ */
+export const watchAgentMetadata = (agentId: string): EventSource => {
+  return new EventSource(
+    `${location.protocol}//${location.host}/api/v2/workspaceagents/${agentId}/watch-metadata`,
+    { withCredentials: true },
+  );
+};
+
 /**
  * @returns {EventSource} An EventSource that emits workspace event objects
  * (ServerSentEvent)
@@ -81,6 +167,135 @@ export const withDefaultFeatures = (
   return fs as TypesGen.Entitlements["features"];
 };
 
+type WatchBuildLogsByTemplateVersionIdOptions = {
+  after?: number;
+  onMessage: (log: TypesGen.ProvisionerJobLog) => void;
+  onDone?: () => void;
+  onError: (error: Error) => void;
+};
+
+export const watchBuildLogsByTemplateVersionId = (
+  versionId: string,
+  {
+    onMessage,
+    onDone,
+    onError,
+    after,
+  }: WatchBuildLogsByTemplateVersionIdOptions,
+) => {
+  const searchParams = new URLSearchParams({ follow: "true" });
+  if (after !== undefined) {
+    searchParams.append("after", after.toString());
+  }
+
+  const proto = location.protocol === "https:" ? "wss:" : "ws:";
+  const socket = new WebSocket(
+    `${proto}//${
+      location.host
+    }/api/v2/templateversions/${versionId}/logs?${searchParams.toString()}`,
+  );
+
+  socket.binaryType = "blob";
+
+  socket.addEventListener("message", (event) =>
+    onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog),
+  );
+
+  socket.addEventListener("error", () => {
+    onError(new Error("Connection for logs failed."));
+    socket.close();
+  });
+
+  socket.addEventListener("close", () => {
+    // When the socket closes, logs have finished streaming!
+    onDone?.();
+  });
+
+  return socket;
+};
+
+export const watchWorkspaceAgentLogs = (
+  agentId: string,
+  { after, onMessage, onDone, onError }: WatchWorkspaceAgentLogsOptions,
+) => {
+  // WebSocket compression in Safari (confirmed in 16.5) is broken when
+  // the server sends large messages. The following error is seen:
+  //
+  //   WebSocket connection to 'wss://.../logs?follow&after=0' failed: The operation couldn’t be completed. Protocol error
+  //
+  const noCompression =
+    userAgentParser(navigator.userAgent).browser.name === "Safari"
+      ? "&no_compression"
+      : "";
+
+  const proto = location.protocol === "https:" ? "wss:" : "ws:";
+  const socket = new WebSocket(
+    `${proto}//${location.host}/api/v2/workspaceagents/${agentId}/logs?follow&after=${after}${noCompression}`,
+  );
+  socket.binaryType = "blob";
+
+  socket.addEventListener("message", (event) => {
+    const logs = JSON.parse(event.data) as TypesGen.WorkspaceAgentLog[];
+    onMessage(logs);
+  });
+
+  socket.addEventListener("error", () => {
+    onError(new Error("socket errored"));
+  });
+
+  socket.addEventListener("close", () => {
+    onDone && onDone();
+  });
+
+  return socket;
+};
+
+type WatchWorkspaceAgentLogsOptions = {
+  after: number;
+  onMessage: (logs: TypesGen.WorkspaceAgentLog[]) => void;
+  onDone?: () => void;
+  onError: (error: Error) => void;
+};
+
+type WatchBuildLogsByBuildIdOptions = {
+  after?: number;
+  onMessage: (log: TypesGen.ProvisionerJobLog) => void;
+  onDone?: () => void;
+  onError?: (error: Error) => void;
+};
+export const watchBuildLogsByBuildId = (
+  buildId: string,
+  { onMessage, onDone, onError, after }: WatchBuildLogsByBuildIdOptions,
+) => {
+  const searchParams = new URLSearchParams({ follow: "true" });
+  if (after !== undefined) {
+    searchParams.append("after", after.toString());
+  }
+  const proto = location.protocol === "https:" ? "wss:" : "ws:";
+  const socket = new WebSocket(
+    `${proto}//${
+      location.host
+    }/api/v2/workspacebuilds/${buildId}/logs?${searchParams.toString()}`,
+  );
+  socket.binaryType = "blob";
+
+  socket.addEventListener("message", (event) =>
+    onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog),
+  );
+
+  socket.addEventListener("error", () => {
+    onError && onError(new Error("Connection for logs failed."));
+    socket.close();
+  });
+
+  socket.addEventListener("close", () => {
+    // When the socket closes, logs have finished streaming!
+    onDone && onDone();
+  });
+
+  return socket;
+};
+
 // This is the base header that is used for several requests. This is defined as
 // a readonly value, but only copies of it should be passed into the API calls,
 // because Axios is able to mutate the headers
@@ -106,615 +321,1529 @@ export type DeleteWorkspaceOptions = Pick<
   "log_level" & "orphan"
 >;
 
-export class Api {
-  constructor(private readonly axios: AxiosInstance) {}
+export type DeploymentConfig = Readonly<{
+  config: TypesGen.DeploymentValues;
+  options: TypesGen.SerpentOption[];
+}>;
 
-  login = async (
-    email: string,
-    password: string,
-  ): Promise<TypesGen.LoginWithPasswordResponse> => {
-    const payload = JSON.stringify({ email, password });
-    const response = await this.axios.post<TypesGen.LoginWithPasswordResponse>(
-      "/api/v2/users/login",
-      payload,
-      { headers: { ...BASE_CONTENT_TYPE_JSON } },
-    );
+type Claims = {
+  license_expires: number;
+  account_type?: string;
+  account_id?: string;
+  trial: boolean;
+  all_features: boolean;
+  version: number;
+  features: Record<string, number>;
+  require_telemetry?: boolean;
+};
 
-    return response.data;
-  };
+export type GetLicensesResponse = Omit<TypesGen.License, "claims"> & {
+  claims: Claims;
+  expires_at: string;
+};
 
-  convertToOAUTH = async (request: TypesGen.ConvertLoginRequest) => {
-    const response = await this.axios.post<TypesGen.OAuthConversionResponse>(
-      "/api/v2/users/me/convert-login",
-      request,
-    );
+export type InsightsParams = {
+  start_time: string;
+  end_time: string;
+  template_ids: string;
+};
 
-    return response.data;
-  };
+export type InsightsTemplateParams = InsightsParams & {
+  interval: "day" | "week";
+};
+
+export type GetJFrogXRayScanParams = {
+  workspaceId: string;
+  agentId: string;
+};
+
+export class MissingBuildParameters extends Error {
+  parameters: TypesGen.TemplateVersionParameter[] = [];
+  versionId: string;
+
+  constructor(
+    parameters: TypesGen.TemplateVersionParameter[],
+    versionId: string,
+  ) {
+    super("Missing build parameters.");
+    this.parameters = parameters;
+    this.versionId = versionId;
+  }
+}
+
+export class Api {
+  constructor(private readonly axios: AxiosInstance) {}
+
+  login = async (
+    email: string,
+    password: string,
+  ): Promise<TypesGen.LoginWithPasswordResponse> => {
+    const payload = JSON.stringify({ email, password });
+    const response = await this.axios.post<TypesGen.LoginWithPasswordResponse>(
+      "/api/v2/users/login",
+      payload,
+      { headers: { ...BASE_CONTENT_TYPE_JSON } },
+    );
+
+    return response.data;
+  };
+
+  convertToOAUTH = async (request: TypesGen.ConvertLoginRequest) => {
+    const response = await this.axios.post<TypesGen.OAuthConversionResponse>(
+      "/api/v2/users/me/convert-login",
+      request,
+    );
+
+    return response.data;
+  };
 
   logout = async (): Promise<void> => {
     return this.axios.post("/api/v2/users/logout");
   };
 
-  getAuthenticatedUser = async () => {
-    const response = await this.axios.get<TypesGen.User>("/api/v2/users/me");
+  getAuthenticatedUser = async () => {
+    const response = await this.axios.get<TypesGen.User>("/api/v2/users/me");
+    return response.data;
+  };
+
+  getUserParameters = async (templateID: string) => {
+    const response = await this.axios.get<TypesGen.UserParameter[]>(
+      `/api/v2/users/me/autofill-parameters?template_id=${templateID}`,
+    );
+
+    return response.data;
+  };
+
+  getAuthMethods = async (): Promise<TypesGen.AuthMethods> => {
+    const response = await this.axios.get<TypesGen.AuthMethods>(
+      "/api/v2/users/authmethods",
+    );
+
+    return response.data;
+  };
+
+  getUserLoginType = async (): Promise<TypesGen.UserLoginType> => {
+    const response = await this.axios.get<TypesGen.UserLoginType>(
+      "/api/v2/users/me/login-type",
+    );
+
+    return response.data;
+  };
+
+  checkAuthorization = async (
+    params: TypesGen.AuthorizationRequest,
+  ): Promise<TypesGen.AuthorizationResponse> => {
+    const response = await this.axios.post<TypesGen.AuthorizationResponse>(
+      `/api/v2/authcheck`,
+      params,
+    );
+
+    return response.data;
+  };
+
+  getApiKey = async (): Promise<TypesGen.GenerateAPIKeyResponse> => {
+    const response = await this.axios.post<TypesGen.GenerateAPIKeyResponse>(
+      "/api/v2/users/me/keys",
+    );
+
+    return response.data;
+  };
+
+  getTokens = async (
+    params: TypesGen.TokensFilter,
+  ): Promise<TypesGen.APIKeyWithOwner[]> => {
+    const response = await this.axios.get<TypesGen.APIKeyWithOwner[]>(
+      `/api/v2/users/me/keys/tokens`,
+      { params },
+    );
+
+    return response.data;
+  };
+
+  deleteToken = async (keyId: string): Promise<void> => {
+    await this.axios.delete("/api/v2/users/me/keys/" + keyId);
+  };
+
+  createToken = async (
+    params: TypesGen.CreateTokenRequest,
+  ): Promise<TypesGen.GenerateAPIKeyResponse> => {
+    const response = await this.axios.post(
+      `/api/v2/users/me/keys/tokens`,
+      params,
+    );
+
+    return response.data;
+  };
+
+  getTokenConfig = async (): Promise<TypesGen.TokenConfig> => {
+    const response = await this.axios.get(
+      "/api/v2/users/me/keys/tokens/tokenconfig",
+    );
+
+    return response.data;
+  };
+
+  getUsers = async (
+    options: TypesGen.UsersRequest,
+    signal?: AbortSignal,
+  ): Promise<TypesGen.GetUsersResponse> => {
+    const url = getURLWithSearchParams("/api/v2/users", options);
+    const response = await this.axios.get<TypesGen.GetUsersResponse>(
+      url.toString(),
+      { signal },
+    );
+
+    return response.data;
+  };
+
+  getOrganization = async (
+    organizationId: string,
+  ): Promise<TypesGen.Organization> => {
+    const response = await this.axios.get<TypesGen.Organization>(
+      `/api/v2/organizations/${organizationId}`,
+    );
+
+    return response.data;
+  };
+
+  getOrganizations = async (): Promise<TypesGen.Organization[]> => {
+    const response = await this.axios.get<TypesGen.Organization[]>(
+      "/api/v2/users/me/organizations",
+    );
+    return response.data;
+  };
+
+  getTemplate = async (templateId: string): Promise<TypesGen.Template> => {
+    const response = await this.axios.get<TypesGen.Template>(
+      `/api/v2/templates/${templateId}`,
+    );
+
+    return response.data;
+  };
+
+  getTemplates = async (
+    organizationId: string,
+    options?: TemplateOptions,
+  ): Promise<TypesGen.Template[]> => {
+    const params: Record<string, string> = {};
+    if (options?.deprecated !== undefined) {
+      // Just want to check if it isn't undefined. If it has
+      // a boolean value, convert it to a string and include
+      // it as a param.
+      params["deprecated"] = String(options.deprecated);
+    }
+
+    const response = await this.axios.get<TypesGen.Template[]>(
+      `/api/v2/organizations/${organizationId}/templates`,
+      { params },
+    );
+
+    return response.data;
+  };
+
+  getTemplateByName = async (
+    organizationId: string,
+    name: string,
+  ): Promise<TypesGen.Template> => {
+    const response = await this.axios.get<TypesGen.Template>(
+      `/api/v2/organizations/${organizationId}/templates/${name}`,
+    );
+
+    return response.data;
+  };
+
+  getTemplateVersion = async (
+    versionId: string,
+  ): Promise<TypesGen.TemplateVersion> => {
+    const response = await this.axios.get<TypesGen.TemplateVersion>(
+      `/api/v2/templateversions/${versionId}`,
+    );
+
+    return response.data;
+  };
+
+  getTemplateVersionResources = async (
+    versionId: string,
+  ): Promise<TypesGen.WorkspaceResource[]> => {
+    const response = await this.axios.get<TypesGen.WorkspaceResource[]>(
+      `/api/v2/templateversions/${versionId}/resources`,
+    );
+
+    return response.data;
+  };
+
+  getTemplateVersionVariables = async (
+    versionId: string,
+  ): Promise<TypesGen.TemplateVersionVariable[]> => {
+    // Defined as separate variable to avoid wonky Prettier formatting because
+    // the type definition is so long
+    type VerArray = TypesGen.TemplateVersionVariable[];
+
+    const response = await this.axios.get<VerArray>(
+      `/api/v2/templateversions/${versionId}/variables`,
+    );
+
+    return response.data;
+  };
+
+  getTemplateVersions = async (
+    templateId: string,
+  ): Promise<TypesGen.TemplateVersion[]> => {
+    const response = await this.axios.get<TypesGen.TemplateVersion[]>(
+      `/api/v2/templates/${templateId}/versions`,
+    );
+    return response.data;
+  };
+
+  getTemplateVersionByName = async (
+    organizationId: string,
+    templateName: string,
+    versionName: string,
+  ): Promise<TypesGen.TemplateVersion> => {
+    const response = await this.axios.get<TypesGen.TemplateVersion>(
+      `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}`,
+    );
+
+    return response.data;
+  };
+
+  getPreviousTemplateVersionByName = async (
+    organizationId: string,
+    templateName: string,
+    versionName: string,
+  ) => {
+    try {
+      const response = await this.axios.get<TypesGen.TemplateVersion>(
+        `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}/previous`,
+      );
+
+      return response.data;
+    } catch (error) {
+      // When there is no previous version, like the first version of a
+      // template, the API returns 404 so in this case we can safely return
+      // undefined
+      const is404 =
+        isAxiosError(error) && error.response && error.response.status === 404;
+
+      if (is404) {
+        return undefined;
+      }
+
+      throw error;
+    }
+  };
+
+  createTemplateVersion = async (
+    organizationId: string,
+    data: TypesGen.CreateTemplateVersionRequest,
+  ): Promise<TypesGen.TemplateVersion> => {
+    const response = await this.axios.post<TypesGen.TemplateVersion>(
+      `/api/v2/organizations/${organizationId}/templateversions`,
+      data,
+    );
+
+    return response.data;
+  };
+
+  getTemplateVersionExternalAuth = async (
+    versionId: string,
+  ): Promise<TypesGen.TemplateVersionExternalAuth[]> => {
+    const response = await this.axios.get(
+      `/api/v2/templateversions/${versionId}/external-auth`,
+    );
+
+    return response.data;
+  };
+
+  getTemplateVersionRichParameters = async (
+    versionId: string,
+  ): Promise<TypesGen.TemplateVersionParameter[]> => {
+    const response = await this.axios.get(
+      `/api/v2/templateversions/${versionId}/rich-parameters`,
+    );
+    return response.data;
+  };
+
+  createTemplate = async (
+    organizationId: string,
+    data: TypesGen.CreateTemplateRequest,
+  ): Promise<TypesGen.Template> => {
+    const response = await this.axios.post(
+      `/api/v2/organizations/${organizationId}/templates`,
+      data,
+    );
+
+    return response.data;
+  };
+
+  updateActiveTemplateVersion = async (
+    templateId: string,
+    data: TypesGen.UpdateActiveTemplateVersion,
+  ) => {
+    const response = await this.axios.patch<TypesGen.Response>(
+      `/api/v2/templates/${templateId}/versions`,
+      data,
+    );
+    return response.data;
+  };
+
+  patchTemplateVersion = async (
+    templateVersionId: string,
+    data: TypesGen.PatchTemplateVersionRequest,
+  ) => {
+    const response = await this.axios.patch<TypesGen.TemplateVersion>(
+      `/api/v2/templateversions/${templateVersionId}`,
+      data,
+    );
+
+    return response.data;
+  };
+
+  archiveTemplateVersion = async (templateVersionId: string) => {
+    const response = await this.axios.post<TypesGen.TemplateVersion>(
+      `/api/v2/templateversions/${templateVersionId}/archive`,
+    );
+
+    return response.data;
+  };
+
+  unarchiveTemplateVersion = async (templateVersionId: string) => {
+    const response = await this.axios.post<TypesGen.TemplateVersion>(
+      `/api/v2/templateversions/${templateVersionId}/unarchive`,
+    );
+    return response.data;
+  };
+
+  updateTemplateMeta = async (
+    templateId: string,
+    data: TypesGen.UpdateTemplateMeta,
+  ): Promise<TypesGen.Template | null> => {
+    const response = await this.axios.patch<TypesGen.Template>(
+      `/api/v2/templates/${templateId}`,
+      data,
+    );
+
+    // On 304 response there is no data payload.
+    if (response.status === 304) {
+      return null;
+    }
+
+    return response.data;
+  };
+
+  deleteTemplate = async (templateId: string): Promise<TypesGen.Template> => {
+    const response = await this.axios.delete<TypesGen.Template>(
+      `/api/v2/templates/${templateId}`,
+    );
+
+    return response.data;
+  };
+
+  getWorkspace = async (
+    workspaceId: string,
+    params?: TypesGen.WorkspaceOptions,
+  ): Promise<TypesGen.Workspace> => {
+    const response = await this.axios.get<TypesGen.Workspace>(
+      `/api/v2/workspaces/${workspaceId}`,
+      { params },
+    );
+
+    return response.data;
+  };
+
+  getWorkspaces = async (
+    options: TypesGen.WorkspacesRequest,
+  ): Promise<TypesGen.WorkspacesResponse> => {
+    const url = getURLWithSearchParams("/api/v2/workspaces", options);
+    const response = await this.axios.get<TypesGen.WorkspacesResponse>(url);
+    return response.data;
+  };
+
+  getWorkspaceByOwnerAndName = async (
+    username = "me",
+    workspaceName: string,
+    params?: TypesGen.WorkspaceOptions,
+  ): Promise<TypesGen.Workspace> => {
+    const response = await this.axios.get<TypesGen.Workspace>(
+      `/api/v2/users/${username}/workspace/${workspaceName}`,
+      { params },
+    );
+
+    return response.data;
+  };
+
+  getWorkspaceBuildByNumber = async (
+    username = "me",
+    workspaceName: string,
+    buildNumber: number,
+  ): Promise<TypesGen.WorkspaceBuild> => {
+    const response = await this.axios.get<TypesGen.WorkspaceBuild>(
+      `/api/v2/users/${username}/workspace/${workspaceName}/builds/${buildNumber}`,
+    );
+
+    return response.data;
+  };
+
+  waitForBuild = (build: TypesGen.WorkspaceBuild) => {
+    return new Promise<TypesGen.ProvisionerJob | undefined>((res, reject) => {
+      void (async () => {
+        let latestJobInfo: TypesGen.ProvisionerJob | undefined = undefined;
+
+        while (
+          !["succeeded", "canceled"].some(
+            (status) => latestJobInfo?.status.includes(status),
+          )
+        ) {
+          const { job } = await this.getWorkspaceBuildByNumber(
+            build.workspace_owner_name,
+            build.workspace_name,
+            build.build_number,
+          );
+
+          latestJobInfo = job;
+          if (latestJobInfo.status === "failed") {
+            return reject(latestJobInfo);
+          }
+
+          await delay(1000);
+        }
+
+        return res(latestJobInfo);
+      })();
+    });
+  };
+
+  postWorkspaceBuild = async (
+    workspaceId: string,
+    data: TypesGen.CreateWorkspaceBuildRequest,
+  ): Promise<TypesGen.WorkspaceBuild> => {
+    const response = await this.axios.post(
+      `/api/v2/workspaces/${workspaceId}/builds`,
+      data,
+    );
+
+    return response.data;
+  };
+
+  startWorkspace = (
+    workspaceId: string,
+    templateVersionId: string,
+    logLevel?: TypesGen.ProvisionerLogLevel,
+    buildParameters?: TypesGen.WorkspaceBuildParameter[],
+  ) => {
+    return this.postWorkspaceBuild(workspaceId, {
+      transition: "start",
+      template_version_id: templateVersionId,
+      log_level: logLevel,
+      rich_parameter_values: buildParameters,
+    });
+  };
+
+  stopWorkspace = (
+    workspaceId: string,
+    logLevel?: TypesGen.ProvisionerLogLevel,
+  ) => {
+    return this.postWorkspaceBuild(workspaceId, {
+      transition: "stop",
+      log_level: logLevel,
+    });
+  };
+
+  deleteWorkspace = (workspaceId: string, options?: DeleteWorkspaceOptions) => {
+    return postWorkspaceBuild(workspaceId, {
+      transition: "delete",
+      ...options,
+    });
+  };
+
+  cancelWorkspaceBuild = async (
+    workspaceBuildId: TypesGen.WorkspaceBuild["id"],
+  ): Promise<TypesGen.Response> => {
+    const response = await this.axios.patch(
+      `/api/v2/workspacebuilds/${workspaceBuildId}/cancel`,
+    );
+
+    return response.data;
+  };
+
+  updateWorkspaceDormancy = async (
+    workspaceId: string,
+    dormant: boolean,
+  ): Promise<TypesGen.Workspace> => {
+    const data: TypesGen.UpdateWorkspaceDormancy = { dormant };
+    const response = await this.axios.put(
+      `/api/v2/workspaces/${workspaceId}/dormant`,
+      data,
+    );
+
+    return response.data;
+  };
+
+  updateWorkspaceAutomaticUpdates = async (
+    workspaceId: string,
+    automaticUpdates: TypesGen.AutomaticUpdates,
+  ): Promise<void> => {
+    const req: TypesGen.UpdateWorkspaceAutomaticUpdatesRequest = {
+      automatic_updates: automaticUpdates,
+    };
+
+    const response = await this.axios.put(
+      `/api/v2/workspaces/${workspaceId}/autoupdates`,
+      req,
+    );
+
+    return response.data;
+  };
+
+  restartWorkspace = async ({
+    workspace,
+    buildParameters,
+  }: RestartWorkspaceParameters): Promise<void> => {
+    const stopBuild = await this.stopWorkspace(workspace.id);
+    const awaitedStopBuild = await this.waitForBuild(stopBuild);
+
+    // If the restart is canceled halfway through, make sure we bail
+    if (awaitedStopBuild?.status === "canceled") {
+      return;
+    }
+
+    const startBuild = await this.startWorkspace(
+      workspace.id,
+      workspace.latest_build.template_version_id,
+      undefined,
+      buildParameters,
+    );
+
+    await waitForBuild(startBuild);
+  };
+
+  cancelTemplateVersionBuild = async (
+    templateVersionId: TypesGen.TemplateVersion["id"],
+  ): Promise<TypesGen.Response> => {
+    const response = await this.axios.patch(
+      `/api/v2/templateversions/${templateVersionId}/cancel`,
+    );
+
+    return response.data;
+  };
+
+  createUser = async (
+    user: TypesGen.CreateUserRequest,
+  ): Promise<TypesGen.User> => {
+    const response = await this.axios.post<TypesGen.User>(
+      "/api/v2/users",
+      user,
+    );
+
+    return response.data;
+  };
+
+  createWorkspace = async (
+    organizationId: string,
+    userId = "me",
+    workspace: TypesGen.CreateWorkspaceRequest,
+  ): Promise<TypesGen.Workspace> => {
+    const response = await this.axios.post<TypesGen.Workspace>(
+      `/api/v2/organizations/${organizationId}/members/${userId}/workspaces`,
+      workspace,
+    );
+
+    return response.data;
+  };
+
+  patchWorkspace = async (
+    workspaceId: string,
+    data: TypesGen.UpdateWorkspaceRequest,
+  ): Promise<void> => {
+    await this.axios.patch(`/api/v2/workspaces/${workspaceId}`, data);
+  };
+
+  getBuildInfo = async (): Promise<TypesGen.BuildInfoResponse> => {
+    const response = await this.axios.get("/api/v2/buildinfo");
+    return response.data;
+  };
+
+  getUpdateCheck = async (): Promise<TypesGen.UpdateCheckResponse> => {
+    const response = await this.axios.get("/api/v2/updatecheck");
+    return response.data;
+  };
+
+  putWorkspaceAutostart = async (
+    workspaceID: string,
+    autostart: TypesGen.UpdateWorkspaceAutostartRequest,
+  ): Promise<void> => {
+    const payload = JSON.stringify(autostart);
+    await this.axios.put(
+      `/api/v2/workspaces/${workspaceID}/autostart`,
+      payload,
+      { headers: { ...BASE_CONTENT_TYPE_JSON } },
+    );
+  };
+
+  putWorkspaceAutostop = async (
+    workspaceID: string,
+    ttl: TypesGen.UpdateWorkspaceTTLRequest,
+  ): Promise<void> => {
+    const payload = JSON.stringify(ttl);
+    await this.axios.put(`/api/v2/workspaces/${workspaceID}/ttl`, payload, {
+      headers: { ...BASE_CONTENT_TYPE_JSON },
+    });
+  };
+
+  updateProfile = async (
+    userId: string,
+    data: TypesGen.UpdateUserProfileRequest,
+  ): Promise<TypesGen.User> => {
+    const response = await this.axios.put(
+      `/api/v2/users/${userId}/profile`,
+      data,
+    );
+    return response.data;
+  };
+
+  updateAppearanceSettings = async (
+    userId: string,
+    data: TypesGen.UpdateUserAppearanceSettingsRequest,
+  ): Promise<TypesGen.User> => {
+    const response = await this.axios.put(
+      `/api/v2/users/${userId}/appearance`,
+      data,
+    );
+    return response.data;
+  };
+
+  getUserQuietHoursSchedule = async (
+    userId: TypesGen.User["id"],
+  ): Promise<TypesGen.UserQuietHoursScheduleResponse> => {
+    const response = await this.axios.get(
+      `/api/v2/users/${userId}/quiet-hours`,
+    );
+    return response.data;
+  };
+
+  updateUserQuietHoursSchedule = async (
+    userId: TypesGen.User["id"],
+    data: TypesGen.UpdateUserQuietHoursScheduleRequest,
+  ): Promise<TypesGen.UserQuietHoursScheduleResponse> => {
+    const response = await this.axios.put(
+      `/api/v2/users/${userId}/quiet-hours`,
+      data,
+    );
+
+    return response.data;
+  };
+
+  activateUser = async (
+    userId: TypesGen.User["id"],
+  ): Promise<TypesGen.User> => {
+    const response = await this.axios.put<TypesGen.User>(
+      `/api/v2/users/${userId}/status/activate`,
+    );
+    return response.data;
+  };
+
+  suspendUser = async (userId: TypesGen.User["id"]): Promise<TypesGen.User> => {
+    const response = await this.axios.put<TypesGen.User>(
+      `/api/v2/users/${userId}/status/suspend`,
+    );
+
+    return response.data;
+  };
+
+  deleteUser = async (userId: TypesGen.User["id"]): Promise<void> => {
+    await this.axios.delete(`/api/v2/users/${userId}`);
+  };
+
+  // API definition:
+  // https://github.com/coder/coder/blob/db665e7261f3c24a272ccec48233a3e276878239/coderd/users.go#L33-L53
+  hasFirstUser = async (): Promise<boolean> => {
+    try {
+      // If it is success, it is true
+      await this.axios.get("/api/v2/users/first");
+      return true;
+    } catch (error) {
+      // If it returns a 404, it is false
+      if (isAxiosError(error) && error.response?.status === 404) {
+        return false;
+      }
+
+      throw error;
+    }
+  };
+
+  createFirstUser = async (
+    req: TypesGen.CreateFirstUserRequest,
+  ): Promise<TypesGen.CreateFirstUserResponse> => {
+    const response = await this.axios.post(`/api/v2/users/first`, req);
+    return response.data;
+  };
+
+  updateUserPassword = async (
+    userId: TypesGen.User["id"],
+    updatePassword: TypesGen.UpdateUserPasswordRequest,
+  ): Promise<undefined> => {
+    await this.axios.put(`/api/v2/users/${userId}/password`, updatePassword);
+  };
+
+  getRoles = async (): Promise<Array<TypesGen.AssignableRoles>> => {
+    const response =
+      await this.axios.get<TypesGen.AssignableRoles[]>(`/api/v2/users/roles`);
+
     return response.data;
   };
 
-  getUserParameters = async (templateID: string) => {
-    const response = await this.axios.get<TypesGen.UserParameter[]>(
-      `/api/v2/users/me/autofill-parameters?template_id=${templateID}`,
+  updateUserRoles = async (
+    roles: TypesGen.Role["name"][],
+    userId: TypesGen.User["id"],
+  ): Promise<TypesGen.User> => {
+    const response = await this.axios.put<TypesGen.User>(
+      `/api/v2/users/${userId}/roles`,
+      { roles },
     );
 
     return response.data;
   };
 
-  getAuthMethods = async (): Promise<TypesGen.AuthMethods> => {
-    const response = await this.axios.get<TypesGen.AuthMethods>(
-      "/api/v2/users/authmethods",
+  getUserSSHKey = async (userId = "me"): Promise<TypesGen.GitSSHKey> => {
+    const response = await this.axios.get<TypesGen.GitSSHKey>(
+      `/api/v2/users/${userId}/gitsshkey`,
     );
 
     return response.data;
   };
 
-  getUserLoginType = async (): Promise<TypesGen.UserLoginType> => {
-    const response = await this.axios.get<TypesGen.UserLoginType>(
-      "/api/v2/users/me/login-type",
+  regenerateUserSSHKey = async (userId = "me"): Promise<TypesGen.GitSSHKey> => {
+    const response = await this.axios.put<TypesGen.GitSSHKey>(
+      `/api/v2/users/${userId}/gitsshkey`,
     );
 
     return response.data;
   };
 
-  checkAuthorization = async (
-    params: TypesGen.AuthorizationRequest,
-  ): Promise<TypesGen.AuthorizationResponse> => {
-    const response = await this.axios.post<TypesGen.AuthorizationResponse>(
-      `/api/v2/authcheck`,
-      params,
+  getWorkspaceBuilds = async (
+    workspaceId: string,
+    req?: TypesGen.WorkspaceBuildsRequest,
+  ) => {
+    const response = await this.axios.get<TypesGen.WorkspaceBuild[]>(
+      getURLWithSearchParams(`/api/v2/workspaces/${workspaceId}/builds`, req),
     );
 
     return response.data;
   };
 
-  getApiKey = async (): Promise<TypesGen.GenerateAPIKeyResponse> => {
-    const response = await this.axios.post<TypesGen.GenerateAPIKeyResponse>(
-      "/api/v2/users/me/keys",
+  getWorkspaceBuildLogs = async (
+    buildId: string,
+    before: Date,
+  ): Promise<TypesGen.ProvisionerJobLog[]> => {
+    const response = await this.axios.get<TypesGen.ProvisionerJobLog[]>(
+      `/api/v2/workspacebuilds/${buildId}/logs?before=${before.getTime()}`,
     );
 
     return response.data;
   };
 
-  getTokens = async (
-    params: TypesGen.TokensFilter,
-  ): Promise<TypesGen.APIKeyWithOwner[]> => {
-    const response = await this.axios.get<TypesGen.APIKeyWithOwner[]>(
-      `/api/v2/users/me/keys/tokens`,
-      { params },
+  getWorkspaceAgentLogs = async (
+    agentID: string,
+  ): Promise<TypesGen.WorkspaceAgentLog[]> => {
+    const response = await this.axios.get<TypesGen.WorkspaceAgentLog[]>(
+      `/api/v2/workspaceagents/${agentID}/logs`,
     );
 
     return response.data;
   };
 
-  deleteToken = async (keyId: string): Promise<void> => {
-    await this.axios.delete("/api/v2/users/me/keys/" + keyId);
+  putWorkspaceExtension = async (
+    workspaceId: string,
+    newDeadline: dayjs.Dayjs,
+  ): Promise<void> => {
+    await this.axios.put(`/api/v2/workspaces/${workspaceId}/extend`, {
+      deadline: newDeadline,
+    });
   };
 
-  createToken = async (
-    params: TypesGen.CreateTokenRequest,
-  ): Promise<TypesGen.GenerateAPIKeyResponse> => {
-    const response = await this.axios.post(
-      `/api/v2/users/me/keys/tokens`,
-      params,
-    );
+  refreshEntitlements = async (): Promise<void> => {
+    await this.axios.post("/api/v2/licenses/refresh-entitlements");
+  };
 
-    return response.data;
+  getEntitlements = async (): Promise<TypesGen.Entitlements> => {
+    try {
+      const response = await this.axios.get<TypesGen.Entitlements>(
+        "/api/v2/entitlements",
+      );
+
+      return response.data;
+    } catch (ex) {
+      if (isAxiosError(ex) && ex.response?.status === 404) {
+        return {
+          errors: [],
+          features: withDefaultFeatures({}),
+          has_license: false,
+          require_telemetry: false,
+          trial: false,
+          warnings: [],
+          refreshed_at: "",
+        };
+      }
+      throw ex;
+    }
   };
 
-  getTokenConfig = async (): Promise<TypesGen.TokenConfig> => {
-    const response = await this.axios.get(
-      "/api/v2/users/me/keys/tokens/tokenconfig",
-    );
+  getExperiments = async (): Promise<TypesGen.Experiment[]> => {
+    try {
+      const response = await this.axios.get<TypesGen.Experiment[]>(
+        "/api/v2/experiments",
+      );
 
-    return response.data;
+      return response.data;
+    } catch (error) {
+      if (isAxiosError(error) && error.response?.status === 404) {
+        return [];
+      }
+
+      throw error;
+    }
   };
 
-  getUsers = async (
-    options: TypesGen.UsersRequest,
-    signal?: AbortSignal,
-  ): Promise<TypesGen.GetUsersResponse> => {
-    const url = getURLWithSearchParams("/api/v2/users", options);
-    const response = await this.axios.get<TypesGen.GetUsersResponse>(
-      url.toString(),
-      { signal },
-    );
+  getAvailableExperiments =
+    async (): Promise<TypesGen.AvailableExperiments> => {
+      try {
+        const response = await this.axios.get("/api/v2/experiments/available");
 
-    return response.data;
+        return response.data;
+      } catch (error) {
+        if (isAxiosError(error) && error.response?.status === 404) {
+          return { safe: [] };
+        }
+        throw error;
+      }
+    };
+
+  getExternalAuthProvider = async (
+    provider: string,
+  ): Promise<TypesGen.ExternalAuth> => {
+    const res = await this.axios.get(`/api/v2/external-auth/${provider}`);
+    return res.data;
   };
 
-  getOrganization = async (
-    organizationId: string,
-  ): Promise<TypesGen.Organization> => {
-    const response = await this.axios.get<TypesGen.Organization>(
-      `/api/v2/organizations/${organizationId}`,
+  getExternalAuthDevice = async (
+    provider: string,
+  ): Promise<TypesGen.ExternalAuthDevice> => {
+    const resp = await this.axios.get(
+      `/api/v2/external-auth/${provider}/device`,
     );
+    return resp.data;
+  };
 
-    return response.data;
+  exchangeExternalAuthDevice = async (
+    provider: string,
+    req: TypesGen.ExternalAuthDeviceExchange,
+  ): Promise<void> => {
+    const resp = await this.axios.post(
+      `/api/v2/external-auth/${provider}/device`,
+      req,
+    );
+
+    return resp.data;
   };
 
-  getOrganizations = async (): Promise<TypesGen.Organization[]> => {
-    const response = await this.axios.get<TypesGen.Organization[]>(
-      "/api/v2/users/me/organizations",
+  getUserExternalAuthProviders =
+    async (): Promise<TypesGen.ListUserExternalAuthResponse> => {
+      const resp = await this.axios.get(`/api/v2/external-auth`);
+      return resp.data;
+    };
+
+  unlinkExternalAuthProvider = async (provider: string): Promise<string> => {
+    const resp = await this.axios.delete(`/api/v2/external-auth/${provider}`);
+    return resp.data;
+  };
+
+  getOAuth2ProviderApps = async (
+    filter?: TypesGen.OAuth2ProviderAppFilter,
+  ): Promise<TypesGen.OAuth2ProviderApp[]> => {
+    const params = filter?.user_id
+      ? new URLSearchParams({ user_id: filter.user_id }).toString()
+      : "";
+
+    const resp = await this.axios.get(`/api/v2/oauth2-provider/apps?${params}`);
+    return resp.data;
+  };
+
+  getOAuth2ProviderApp = async (
+    id: string,
+  ): Promise<TypesGen.OAuth2ProviderApp> => {
+    const resp = await this.axios.get(`/api/v2/oauth2-provider/apps/${id}`);
+    return resp.data;
+  };
+
+  postOAuth2ProviderApp = async (
+    data: TypesGen.PostOAuth2ProviderAppRequest,
+  ): Promise<TypesGen.OAuth2ProviderApp> => {
+    const response = await this.axios.post(
+      `/api/v2/oauth2-provider/apps`,
+      data,
     );
     return response.data;
   };
 
-  getTemplate = async (templateId: string): Promise<TypesGen.Template> => {
-    const response = await this.axios.get<TypesGen.Template>(
-      `/api/v2/templates/${templateId}`,
+  putOAuth2ProviderApp = async (
+    id: string,
+    data: TypesGen.PutOAuth2ProviderAppRequest,
+  ): Promise<TypesGen.OAuth2ProviderApp> => {
+    const response = await this.axios.put(
+      `/api/v2/oauth2-provider/apps/${id}`,
+      data,
     );
-
     return response.data;
   };
 
-  getTemplates = async (
-    organizationId: string,
-    options?: TemplateOptions,
-  ): Promise<TypesGen.Template[]> => {
-    const params: Record<string, string> = {};
-    if (options?.deprecated !== undefined) {
-      // Just want to check if it isn't undefined. If it has
-      // a boolean value, convert it to a string and include
-      // it as a param.
-      params["deprecated"] = String(options.deprecated);
-    }
+  deleteOAuth2ProviderApp = async (id: string): Promise<void> => {
+    await this.axios.delete(`/api/v2/oauth2-provider/apps/${id}`);
+  };
 
-    const response = await this.axios.get<TypesGen.Template[]>(
-      `/api/v2/organizations/${organizationId}/templates`,
-      { params },
+  getOAuth2ProviderAppSecrets = async (
+    id: string,
+  ): Promise<TypesGen.OAuth2ProviderAppSecret[]> => {
+    const resp = await this.axios.get(
+      `/api/v2/oauth2-provider/apps/${id}/secrets`,
     );
+    return resp.data;
+  };
 
-    return response.data;
+  postOAuth2ProviderAppSecret = async (
+    id: string,
+  ): Promise<TypesGen.OAuth2ProviderAppSecretFull> => {
+    const resp = await this.axios.post(
+      `/api/v2/oauth2-provider/apps/${id}/secrets`,
+    );
+    return resp.data;
   };
 
-  getTemplateByName = async (
-    organizationId: string,
-    name: string,
-  ): Promise<TypesGen.Template> => {
-    const response = await this.axios.get<TypesGen.Template>(
-      `/api/v2/organizations/${organizationId}/templates/${name}`,
+  deleteOAuth2ProviderAppSecret = async (
+    appId: string,
+    secretId: string,
+  ): Promise<void> => {
+    await this.axios.delete(
+      `/api/v2/oauth2-provider/apps/${appId}/secrets/${secretId}`,
     );
+  };
 
+  revokeOAuth2ProviderApp = async (appId: string): Promise<void> => {
+    await this.axios.delete(`/oauth2/tokens?client_id=${appId}`);
+  };
+
+  getAuditLogs = async (
+    options: TypesGen.AuditLogsRequest,
+  ): Promise<TypesGen.AuditLogResponse> => {
+    const url = getURLWithSearchParams("/api/v2/audit", options);
+    const response = await this.axios.get(url);
     return response.data;
   };
 
-  getTemplateVersion = async (
-    versionId: string,
-  ): Promise<TypesGen.TemplateVersion> => {
-    const response = await this.axios.get<TypesGen.TemplateVersion>(
-      `/api/v2/templateversions/${versionId}`,
+  getTemplateDAUs = async (
+    templateId: string,
+  ): Promise<TypesGen.DAUsResponse> => {
+    const response = await this.axios.get(
+      `/api/v2/templates/${templateId}/daus`,
     );
 
     return response.data;
   };
 
-  getTemplateVersionResources = async (
-    versionId: string,
-  ): Promise<TypesGen.WorkspaceResource[]> => {
-    const response = await this.axios.get<TypesGen.WorkspaceResource[]>(
-      `/api/v2/templateversions/${versionId}/resources`,
+  getDeploymentDAUs = async (
+    // Default to user's local timezone.
+    // As /api/v2/insights/daus only accepts whole-number values for tz_offset
+    // we truncate the tz offset down to the closest hour.
+    offset = Math.trunc(new Date().getTimezoneOffset() / 60),
+  ): Promise<TypesGen.DAUsResponse> => {
+    const response = await this.axios.get(
+      `/api/v2/insights/daus?tz_offset=${offset}`,
     );
 
     return response.data;
   };
 
-  getTemplateVersionVariables = async (
-    versionId: string,
-  ): Promise<TypesGen.TemplateVersionVariable[]> => {
-    // Defined as separate variable to avoid wonky Prettier formatting because
-    // the type definition is so long
-    type VerArray = TypesGen.TemplateVersionVariable[];
-
-    const response = await axiosInstance.get<VerArray>(
-      `/api/v2/templateversions/${versionId}/variables`,
-    );
+  getTemplateACLAvailable = async (
+    templateId: string,
+    options: TypesGen.UsersRequest,
+  ): Promise<TypesGen.ACLAvailable> => {
+    const url = getURLWithSearchParams(
+      `/api/v2/templates/${templateId}/acl/available`,
+      options,
+    ).toString();
 
+    const response = await this.axios.get(url);
     return response.data;
   };
 
-  getTemplateVersions = async (
+  getTemplateACL = async (
     templateId: string,
-  ): Promise<TypesGen.TemplateVersion[]> => {
-    const response = await this.axios.get<TypesGen.TemplateVersion[]>(
-      `/api/v2/templates/${templateId}/versions`,
+  ): Promise<TypesGen.TemplateACL> => {
+    const response = await this.axios.get(
+      `/api/v2/templates/${templateId}/acl`,
     );
+
     return response.data;
   };
 
-  getTemplateVersionByName = async (
-    organizationId: string,
-    templateName: string,
-    versionName: string,
-  ): Promise<TypesGen.TemplateVersion> => {
-    const response = await this.axios.get<TypesGen.TemplateVersion>(
-      `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}`,
+  updateTemplateACL = async (
+    templateId: string,
+    data: TypesGen.UpdateTemplateACL,
+  ): Promise<{ message: string }> => {
+    const response = await this.axios.patch(
+      `/api/v2/templates/${templateId}/acl`,
+      data,
     );
 
     return response.data;
   };
 
-  getPreviousTemplateVersionByName = async (
-    organizationId: string,
-    templateName: string,
-    versionName: string,
-  ) => {
-    try {
-      const response = await this.axios.get<TypesGen.TemplateVersion>(
-        `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}/previous`,
-      );
-
-      return response.data;
-    } catch (error) {
-      // When there is no previous version, like the first version of a
-      // template, the API returns 404 so in this case we can safely return
-      // undefined
-      const is404 =
-        isAxiosError(error) && error.response && error.response.status === 404;
+  getApplicationsHost = async (): Promise<TypesGen.AppHostResponse> => {
+    const response = await this.axios.get(`/api/v2/applications/host`);
+    return response.data;
+  };
 
-      if (is404) {
-        return undefined;
-      }
+  getGroups = async (organizationId: string): Promise<TypesGen.Group[]> => {
+    const response = await this.axios.get(
+      `/api/v2/organizations/${organizationId}/groups`,
+    );
 
-      throw error;
-    }
+    return response.data;
   };
 
-  createTemplateVersion = async (
+  createGroup = async (
     organizationId: string,
-    data: TypesGen.CreateTemplateVersionRequest,
-  ): Promise<TypesGen.TemplateVersion> => {
-    const response = await this.axios.post<TypesGen.TemplateVersion>(
-      `/api/v2/organizations/${organizationId}/templateversions`,
+    data: TypesGen.CreateGroupRequest,
+  ): Promise<TypesGen.Group> => {
+    const response = await this.axios.post(
+      `/api/v2/organizations/${organizationId}/groups`,
       data,
     );
+    return response.data;
+  };
 
+  getGroup = async (groupId: string): Promise<TypesGen.Group> => {
+    const response = await this.axios.get(`/api/v2/groups/${groupId}`);
     return response.data;
   };
 
-  getTemplateVersionExternalAuth = async (
-    versionId: string,
-  ): Promise<TypesGen.TemplateVersionExternalAuth[]> => {
+  patchGroup = async (
+    groupId: string,
+    data: TypesGen.PatchGroupRequest,
+  ): Promise<TypesGen.Group> => {
+    const response = await this.axios.patch(`/api/v2/groups/${groupId}`, data);
+    return response.data;
+  };
+
+  addMember = async (groupId: string, userId: string) => {
+    return patchGroup(groupId, {
+      name: "",
+      add_users: [userId],
+      remove_users: [],
+    });
+  };
+
+  removeMember = async (groupId: string, userId: string) => {
+    return patchGroup(groupId, {
+      name: "",
+      display_name: "",
+      add_users: [],
+      remove_users: [userId],
+    });
+  };
+
+  deleteGroup = async (groupId: string): Promise<void> => {
+    await this.axios.delete(`/api/v2/groups/${groupId}`);
+  };
+
+  getWorkspaceQuota = async (
+    username: string,
+  ): Promise<TypesGen.WorkspaceQuota> => {
     const response = await this.axios.get(
-      `/api/v2/templateversions/${versionId}/external-auth`,
+      `/api/v2/workspace-quota/${encodeURIComponent(username)}`,
     );
+    return response.data;
+  };
 
+  getAgentListeningPorts = async (
+    agentID: string,
+  ): Promise<TypesGen.WorkspaceAgentListeningPortsResponse> => {
+    const response = await this.axios.get(
+      `/api/v2/workspaceagents/${agentID}/listening-ports`,
+    );
     return response.data;
   };
 
-  getTemplateVersionRichParameters = async (
-    versionId: string,
-  ): Promise<TypesGen.TemplateVersionParameter[]> => {
+  getWorkspaceAgentSharedPorts = async (
+    workspaceID: string,
+  ): Promise<TypesGen.WorkspaceAgentPortShares> => {
     const response = await this.axios.get(
-      `/api/v2/templateversions/${versionId}/rich-parameters`,
+      `/api/v2/workspaces/${workspaceID}/port-share`,
     );
     return response.data;
   };
 
-  createTemplate = async (
-    organizationId: string,
-    data: TypesGen.CreateTemplateRequest,
-  ): Promise<TypesGen.Template> => {
+  upsertWorkspaceAgentSharedPort = async (
+    workspaceID: string,
+    req: TypesGen.UpsertWorkspaceAgentPortShareRequest,
+  ): Promise<TypesGen.WorkspaceAgentPortShares> => {
     const response = await this.axios.post(
-      `/api/v2/organizations/${organizationId}/templates`,
-      data,
+      `/api/v2/workspaces/${workspaceID}/port-share`,
+      req,
+    );
+    return response.data;
+  };
+
+  deleteWorkspaceAgentSharedPort = async (
+    workspaceID: string,
+    req: TypesGen.DeleteWorkspaceAgentPortShareRequest,
+  ): Promise<TypesGen.WorkspaceAgentPortShares> => {
+    const response = await this.axios.delete(
+      `/api/v2/workspaces/${workspaceID}/port-share`,
+      { data: req },
     );
 
     return response.data;
   };
 
-  updateActiveTemplateVersion = async (
-    templateId: string,
-    data: TypesGen.UpdateActiveTemplateVersion,
-  ) => {
-    const response = await this.axios.patch<TypesGen.Response>(
-      `/api/v2/templates/${templateId}/versions`,
-      data,
-    );
+  // getDeploymentSSHConfig is used by the VSCode-Extension.
+  getDeploymentSSHConfig = async (): Promise<TypesGen.SSHConfigResponse> => {
+    const response = await this.axios.get(`/api/v2/deployment/ssh`);
     return response.data;
   };
 
-  patchTemplateVersion = async (
-    templateVersionId: string,
-    data: TypesGen.PatchTemplateVersionRequest,
-  ) => {
-    const response = await this.axios.patch<TypesGen.TemplateVersion>(
-      `/api/v2/templateversions/${templateVersionId}`,
-      data,
-    );
+  getDeploymentConfig = async (): Promise<DeploymentConfig> => {
+    const response = await this.axios.get(`/api/v2/deployment/config`);
+    return response.data;
+  };
 
+  getDeploymentStats = async (): Promise<TypesGen.DeploymentStats> => {
+    const response = await this.axios.get(`/api/v2/deployment/stats`);
     return response.data;
   };
 
-  archiveTemplateVersion = async (templateVersionId: string) => {
-    const response = await this.axios.post<TypesGen.TemplateVersion>(
-      `/api/v2/templateversions/${templateVersionId}/archive`,
+  getReplicas = async (): Promise<TypesGen.Replica[]> => {
+    const response = await this.axios.get(`/api/v2/replicas`);
+    return response.data;
+  };
+
+  getFile = async (fileId: string): Promise<ArrayBuffer> => {
+    const response = await this.axios.get<ArrayBuffer>(
+      `/api/v2/files/${fileId}`,
+      { responseType: "arraybuffer" },
     );
 
     return response.data;
   };
 
-  unarchiveTemplateVersion = async (templateVersionId: string) => {
-    const response = await this.axios.post<TypesGen.TemplateVersion>(
-      `/api/v2/templateversions/${templateVersionId}/unarchive`,
-    );
+  getWorkspaceProxyRegions = async (): Promise<
+    TypesGen.RegionsResponse<TypesGen.Region>
+  > => {
+    const response =
+      await this.axios.get<TypesGen.RegionsResponse<TypesGen.Region>>(
+        `/api/v2/regions`,
+      );
+
     return response.data;
   };
 
-  updateTemplateMeta = async (
-    templateId: string,
-    data: TypesGen.UpdateTemplateMeta,
-  ): Promise<TypesGen.Template | null> => {
-    const response = await this.axios.patch<TypesGen.Template>(
-      `/api/v2/templates/${templateId}`,
-      data,
-    );
+  getWorkspaceProxies = async (): Promise<
+    TypesGen.RegionsResponse<TypesGen.WorkspaceProxy>
+  > => {
+    const response = await this.axios.get<
+      TypesGen.RegionsResponse<TypesGen.WorkspaceProxy>
+    >(`/api/v2/workspaceproxies`);
 
-    // On 304 response there is no data payload.
-    if (response.status === 304) {
-      return null;
-    }
+    return response.data;
+  };
 
+  createWorkspaceProxy = async (
+    b: TypesGen.CreateWorkspaceProxyRequest,
+  ): Promise<TypesGen.UpdateWorkspaceProxyResponse> => {
+    const response = await this.axios.post(`/api/v2/workspaceproxies`, b);
     return response.data;
   };
 
-  deleteTemplate = async (templateId: string): Promise<TypesGen.Template> => {
-    const response = await this.axios.delete<TypesGen.Template>(
-      `/api/v2/templates/${templateId}`,
-    );
+  getAppearance = async (): Promise<TypesGen.AppearanceConfig> => {
+    try {
+      const response = await this.axios.get(`/api/v2/appearance`);
+      return response.data || {};
+    } catch (ex) {
+      if (isAxiosError(ex) && ex.response?.status === 404) {
+        return {
+          application_name: "",
+          logo_url: "",
+          service_banner: {
+            enabled: false,
+          },
+        };
+      }
+
+      throw ex;
+    }
+  };
 
+  updateAppearance = async (
+    b: TypesGen.AppearanceConfig,
+  ): Promise<TypesGen.AppearanceConfig> => {
+    const response = await this.axios.put(`/api/v2/appearance`, b);
     return response.data;
   };
 
-  getWorkspace = async (
-    workspaceId: string,
-    params?: TypesGen.WorkspaceOptions,
-  ): Promise<TypesGen.Workspace> => {
-    const response = await this.axios.get<TypesGen.Workspace>(
-      `/api/v2/workspaces/${workspaceId}`,
-      { params },
+  getTemplateExamples = async (
+    organizationId: string,
+  ): Promise<TypesGen.TemplateExample[]> => {
+    const response = await this.axios.get(
+      `/api/v2/organizations/${organizationId}/templates/examples`,
     );
 
     return response.data;
   };
 
-  getWorkspaces = async (
-    options: TypesGen.WorkspacesRequest,
-  ): Promise<TypesGen.WorkspacesResponse> => {
-    const url = getURLWithSearchParams("/api/v2/workspaces", options);
-    const response = await this.axios.get<TypesGen.WorkspacesResponse>(url);
+  uploadFile = async (file: File): Promise<TypesGen.UploadResponse> => {
+    const response = await this.axios.post("/api/v2/files", file, {
+      headers: { "Content-Type": "application/x-tar" },
+    });
+
     return response.data;
   };
 
-  getWorkspaceByOwnerAndName = async (
-    username = "me",
-    workspaceName: string,
-    params?: TypesGen.WorkspaceOptions,
-  ): Promise<TypesGen.Workspace> => {
-    const response = await this.axios.get<TypesGen.Workspace>(
-      `/api/v2/users/${username}/workspace/${workspaceName}`,
-      { params },
+  getTemplateVersionLogs = async (
+    versionId: string,
+  ): Promise<TypesGen.ProvisionerJobLog[]> => {
+    const response = await this.axios.get<TypesGen.ProvisionerJobLog[]>(
+      `/api/v2/templateversions/${versionId}/logs`,
     );
-
     return response.data;
   };
 
-  getWorkspaceBuildByNumber = async (
-    username = "me",
-    workspaceName: string,
-    buildNumber: number,
+  updateWorkspaceVersion = async (
+    workspace: TypesGen.Workspace,
   ): Promise<TypesGen.WorkspaceBuild> => {
-    const response = await this.axios.get<TypesGen.WorkspaceBuild>(
-      `/api/v2/users/${username}/workspace/${workspaceName}/builds/${buildNumber}`,
+    const template = await getTemplate(workspace.template_id);
+    return startWorkspace(workspace.id, template.active_version_id);
+  };
+
+  getWorkspaceBuildParameters = async (
+    workspaceBuildId: TypesGen.WorkspaceBuild["id"],
+  ): Promise<TypesGen.WorkspaceBuildParameter[]> => {
+    const response = await this.axios.get<TypesGen.WorkspaceBuildParameter[]>(
+      `/api/v2/workspacebuilds/${workspaceBuildId}/parameters`,
     );
 
     return response.data;
   };
 
-  waitForBuild = (build: TypesGen.WorkspaceBuild) => {
-    return new Promise<TypesGen.ProvisionerJob | undefined>((res, reject) => {
-      void (async () => {
-        let latestJobInfo: TypesGen.ProvisionerJob | undefined = undefined;
-
-        while (
-          !["succeeded", "canceled"].some(
-            (status) => latestJobInfo?.status.includes(status),
-          )
-        ) {
-          const { job } = await this.getWorkspaceBuildByNumber(
-            build.workspace_owner_name,
-            build.workspace_name,
-            build.build_number,
-          );
-
-          latestJobInfo = job;
-          if (latestJobInfo.status === "failed") {
-            return reject(latestJobInfo);
-          }
+  getLicenses = async (): Promise<GetLicensesResponse[]> => {
+    const response = await this.axios.get(`/api/v2/licenses`);
+    return response.data;
+  };
 
-          await delay(1000);
-        }
+  createLicense = async (
+    data: TypesGen.AddLicenseRequest,
+  ): Promise<TypesGen.AddLicenseRequest> => {
+    const response = await this.axios.post(`/api/v2/licenses`, data);
+    return response.data;
+  };
 
-        return res(latestJobInfo);
-      })();
-    });
+  removeLicense = async (licenseId: number): Promise<void> => {
+    await this.axios.delete(`/api/v2/licenses/${licenseId}`);
   };
 
-  postWorkspaceBuild = async (
-    workspaceId: string,
-    data: TypesGen.CreateWorkspaceBuildRequest,
+  /** Steps to change the workspace version
+   * - Get the latest template to access the latest active version
+   * - Get the current build parameters
+   * - Get the template parameters
+   * - Update the build parameters and check if there are missed parameters for
+   *   the new version
+   *   - If there are missing parameters raise an error
+   * - Create a build with the version and updated build parameters
+   */
+  changeWorkspaceVersion = async (
+    workspace: TypesGen.Workspace,
+    templateVersionId: string,
+    newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
   ): Promise<TypesGen.WorkspaceBuild> => {
-    const response = await this.axios.post(
-      `/api/v2/workspaces/${workspaceId}/builds`,
-      data,
+    const [currentBuildParameters, templateParameters] = await Promise.all([
+      this.getWorkspaceBuildParameters(workspace.latest_build.id),
+      this.getTemplateVersionRichParameters(templateVersionId),
+    ]);
+
+    const missingParameters = getMissingParameters(
+      currentBuildParameters,
+      newBuildParameters,
+      templateParameters,
     );
 
-    return response.data;
-  };
+    if (missingParameters.length > 0) {
+      throw new MissingBuildParameters(missingParameters, templateVersionId);
+    }
 
-  startWorkspace = (
-    workspaceId: string,
-    templateVersionId: string,
-    logLevel?: TypesGen.ProvisionerLogLevel,
-    buildParameters?: TypesGen.WorkspaceBuildParameter[],
-  ) => {
-    return this.postWorkspaceBuild(workspaceId, {
+    return this.postWorkspaceBuild(workspace.id, {
       transition: "start",
       template_version_id: templateVersionId,
-      log_level: logLevel,
-      rich_parameter_values: buildParameters,
+      rich_parameter_values: newBuildParameters,
     });
   };
 
-  stopWorkspace = (
-    workspaceId: string,
-    logLevel?: TypesGen.ProvisionerLogLevel,
-  ) => {
-    return this.postWorkspaceBuild(workspaceId, {
-      transition: "stop",
-      log_level: logLevel,
-    });
-  };
+  /** Steps to update the workspace
+   * - Get the latest template to access the latest active version
+   * - Get the current build parameters
+   * - Get the template parameters
+   * - Update the build parameters and check if there are missed parameters for
+   *   the newest version
+   *   - If there are missing parameters raise an error
+   * - Create a build with the latest version and updated build parameters
+   */
+  updateWorkspace = async (
+    workspace: TypesGen.Workspace,
+    newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
+  ): Promise<TypesGen.WorkspaceBuild> => {
+    const [template, oldBuildParameters] = await Promise.all([
+      this.getTemplate(workspace.template_id),
+      this.getWorkspaceBuildParameters(workspace.latest_build.id),
+    ]);
+
+    const activeVersionId = template.active_version_id;
+    const templateParameters =
+      await getTemplateVersionRichParameters(activeVersionId);
+
+    const missingParameters = getMissingParameters(
+      oldBuildParameters,
+      newBuildParameters,
+      templateParameters,
+    );
 
-  deleteWorkspace = (workspaceId: string, options?: DeleteWorkspaceOptions) => {
-    return postWorkspaceBuild(workspaceId, {
-      transition: "delete",
-      ...options,
+    if (missingParameters.length > 0) {
+      throw new MissingBuildParameters(missingParameters, activeVersionId);
+    }
+
+    return postWorkspaceBuild(workspace.id, {
+      transition: "start",
+      template_version_id: activeVersionId,
+      rich_parameter_values: newBuildParameters,
     });
   };
 
-  cancelWorkspaceBuild = async (
-    workspaceBuildId: TypesGen.WorkspaceBuild["id"],
-  ): Promise<TypesGen.Response> => {
-    const response = await this.axios.patch(
-      `/api/v2/workspacebuilds/${workspaceBuildId}/cancel`,
+  getWorkspaceResolveAutostart = async (
+    workspaceId: string,
+  ): Promise<TypesGen.ResolveAutostartResponse> => {
+    const response = await this.axios.get(
+      `/api/v2/workspaces/${workspaceId}/resolve-autostart`,
     );
-
     return response.data;
   };
 
-  updateWorkspaceDormancy = async (
-    workspaceId: string,
-    dormant: boolean,
-  ): Promise<TypesGen.Workspace> => {
-    const data: TypesGen.UpdateWorkspaceDormancy = { dormant };
-    const response = await this.axios.put(
-      `/api/v2/workspaces/${workspaceId}/dormant`,
-      data,
+  issueReconnectingPTYSignedToken = async (
+    params: TypesGen.IssueReconnectingPTYSignedTokenRequest,
+  ): Promise<TypesGen.IssueReconnectingPTYSignedTokenResponse> => {
+    const response = await this.axios.post(
+      "/api/v2/applications/reconnecting-pty-signed-token",
+      params,
     );
 
     return response.data;
   };
 
-  updateWorkspaceAutomaticUpdates = async (
-    workspaceId: string,
-    automaticUpdates: TypesGen.AutomaticUpdates,
-  ): Promise<void> => {
-    const req: TypesGen.UpdateWorkspaceAutomaticUpdatesRequest = {
-      automatic_updates: automaticUpdates,
+  getWorkspaceParameters = async (workspace: TypesGen.Workspace) => {
+    const latestBuild = workspace.latest_build;
+    const [templateVersionRichParameters, buildParameters] = await Promise.all([
+      this.getTemplateVersionRichParameters(latestBuild.template_version_id),
+      this.getWorkspaceBuildParameters(latestBuild.id),
+    ]);
+
+    return {
+      templateVersionRichParameters,
+      buildParameters,
     };
+  };
 
-    const response = await this.axios.put(
-      `/api/v2/workspaces/${workspaceId}/autoupdates`,
-      req,
+  getInsightsUserLatency = async (
+    filters: InsightsParams,
+  ): Promise<TypesGen.UserLatencyInsightsResponse> => {
+    const params = new URLSearchParams(filters);
+    const response = await this.axios.get(
+      `/api/v2/insights/user-latency?${params}`,
     );
 
     return response.data;
   };
 
-  restartWorkspace = async ({
-    workspace,
-    buildParameters,
-  }: RestartWorkspaceParameters): Promise<void> => {
-    const stopBuild = await this.stopWorkspace(workspace.id);
-    const awaitedStopBuild = await this.waitForBuild(stopBuild);
+  getInsightsUserActivity = async (
+    filters: InsightsParams,
+  ): Promise<TypesGen.UserActivityInsightsResponse> => {
+    const params = new URLSearchParams(filters);
+    const response = await this.axios.get(
+      `/api/v2/insights/user-activity?${params}`,
+    );
 
-    // If the restart is canceled halfway through, make sure we bail
-    if (awaitedStopBuild?.status === "canceled") {
-      return;
-    }
+    return response.data;
+  };
 
-    const startBuild = await this.startWorkspace(
-      workspace.id,
-      workspace.latest_build.template_version_id,
-      undefined,
-      buildParameters,
+  getInsightsTemplate = async (
+    params: InsightsTemplateParams,
+  ): Promise<TypesGen.TemplateInsightsResponse> => {
+    const searchParams = new URLSearchParams(params);
+    const response = await this.axios.get(
+      `/api/v2/insights/templates?${searchParams}`,
     );
 
-    await waitForBuild(startBuild);
+    return response.data;
   };
 
-  cancelTemplateVersionBuild = async (
-    templateVersionId: TypesGen.TemplateVersion["id"],
-  ): Promise<TypesGen.Response> => {
-    const response = await axiosInstance.patch(
-      `/api/v2/templateversions/${templateVersionId}/cancel`,
+  getHealth = async (force: boolean = false) => {
+    const params = new URLSearchParams({ force: force.toString() });
+    const response = await this.axios.get<TypesGen.HealthcheckReport>(
+      `/api/v2/debug/health?${params}`,
     );
-
     return response.data;
   };
 
-  createUser = async (
-    user: TypesGen.CreateUserRequest,
-  ): Promise<TypesGen.User> => {
-    const response = await this.axios.post<TypesGen.User>(
-      "/api/v2/users",
-      user,
+  getHealthSettings = async (): Promise<TypesGen.HealthSettings> => {
+    const res = await this.axios.get<TypesGen.HealthSettings>(
+      `/api/v2/debug/health/settings`,
     );
 
-    return response.data;
+    return res.data;
   };
 
-  createWorkspace = async (
-    organizationId: string,
-    userId = "me",
-    workspace: TypesGen.CreateWorkspaceRequest,
-  ): Promise<TypesGen.Workspace> => {
-    const response = await this.axios.post<TypesGen.Workspace>(
-      `/api/v2/organizations/${organizationId}/members/${userId}/workspaces`,
-      workspace,
+  updateHealthSettings = async (data: TypesGen.UpdateHealthSettings) => {
+    const response = await this.axios.put<TypesGen.HealthSettings>(
+      `/api/v2/debug/health/settings`,
+      data,
     );
 
     return response.data;
   };
 
-  patchWorkspace = async (
-    workspaceId: string,
-    data: TypesGen.UpdateWorkspaceRequest,
-  ): Promise<void> => {
-    await this.axios.patch(`/api/v2/workspaces/${workspaceId}`, data);
+  putFavoriteWorkspace = async (workspaceID: string) => {
+    await this.axios.put(`/api/v2/workspaces/${workspaceID}/favorite`);
   };
 
-  getBuildInfo = async (): Promise<TypesGen.BuildInfoResponse> => {
-    const response = await this.axios.get("/api/v2/buildinfo");
-    return response.data;
+  deleteFavoriteWorkspace = async (workspaceID: string) => {
+    await this.axios.delete(`/api/v2/workspaces/${workspaceID}/favorite`);
   };
 
-  getUpdateCheck = async (): Promise<TypesGen.UpdateCheckResponse> => {
-    const response = await axiosInstance.get("/api/v2/updatecheck");
-    return response.data;
-  };
+  getJFrogXRayScan = async (options: GetJFrogXRayScanParams) => {
+    const searchParams = new URLSearchParams({
+      workspace_id: options.workspaceId,
+      agent_id: options.agentId,
+    });
 
-  putWorkspaceAutostart = async (
-    workspaceID: string,
-    autostart: TypesGen.UpdateWorkspaceAutostartRequest,
-  ): Promise<void> => {
-    const payload = JSON.stringify(autostart);
-    await this.axios.put(
-      `/api/v2/workspaces/${workspaceID}/autostart`,
-      payload,
-      {
-        headers: { ...BASE_CONTENT_TYPE_JSON },
-      },
-    );
+    try {
+      const res = await this.axios.get<TypesGen.JFrogXrayScan>(
+        `/api/v2/integrations/jfrog/xray-scan?${searchParams}`,
+      );
+
+      return res.data;
+    } catch (error) {
+      if (isAxiosError(error) && error.response?.status === 404) {
+        // react-query library does not allow undefined to be returned as a
+        // query result
+        return null;
+      }
+
+      throw error;
+    }
   };
 }
 
@@ -1977,11 +3106,6 @@ export const getDeploymentSSHConfig =
     return response.data;
   };
 
-export type DeploymentConfig = {
-  readonly config: TypesGen.DeploymentValues;
-  readonly options: TypesGen.SerpentOption[];
-};
-
 export const getDeploymentConfig = async (): Promise<DeploymentConfig> => {
   const response = await axiosInstance.get(`/api/v2/deployment/config`);
   return response.data;
@@ -2103,21 +3227,6 @@ export const getWorkspaceBuildParameters = async (
   );
   return response.data;
 };
-type Claims = {
-  license_expires: number;
-  account_type?: string;
-  account_id?: string;
-  trial: boolean;
-  all_features: boolean;
-  version: number;
-  features: Record<string, number>;
-  require_telemetry?: boolean;
-};
-
-export type GetLicensesResponse = Omit<TypesGen.License, "claims"> & {
-  claims: Claims;
-  expires_at: string;
-};
 
 export const getLicenses = async (): Promise<GetLicensesResponse[]> => {
   const response = await axiosInstance.get(`/api/v2/licenses`);
@@ -2135,20 +3244,6 @@ export const removeLicense = async (licenseId: number): Promise<void> => {
   await axiosInstance.delete(`/api/v2/licenses/${licenseId}`);
 };
 
-export class MissingBuildParameters extends Error {
-  parameters: TypesGen.TemplateVersionParameter[] = [];
-  versionId: string;
-
-  constructor(
-    parameters: TypesGen.TemplateVersionParameter[],
-    versionId: string,
-  ) {
-    super("Missing build parameters.");
-    this.parameters = parameters;
-    this.versionId = versionId;
-  }
-}
-
 /** Steps to change the workspace version
  * - Get the latest template to access the latest active version
  * - Get the current build parameters
@@ -2230,207 +3325,6 @@ export const getWorkspaceResolveAutostart = async (
   return response.data;
 };
 
-const getMissingParameters = (
-  oldBuildParameters: TypesGen.WorkspaceBuildParameter[],
-  newBuildParameters: TypesGen.WorkspaceBuildParameter[],
-  templateParameters: TypesGen.TemplateVersionParameter[],
-) => {
-  const missingParameters: TypesGen.TemplateVersionParameter[] = [];
-  const requiredParameters: TypesGen.TemplateVersionParameter[] = [];
-
-  templateParameters.forEach((p) => {
-    // It is mutable and required. Mutable values can be changed after so we
-    // don't need to ask them if they are not required.
-    const isMutableAndRequired = p.mutable && p.required;
-    // Is immutable, so we can check if it is its first time on the build
-    const isImmutable = !p.mutable;
-
-    if (isMutableAndRequired || isImmutable) {
-      requiredParameters.push(p);
-    }
-  });
-
-  for (const parameter of requiredParameters) {
-    // Check if there is a new value
-    let buildParameter = newBuildParameters.find(
-      (p) => p.name === parameter.name,
-    );
-
-    // If not, get the old one
-    if (!buildParameter) {
-      buildParameter = oldBuildParameters.find(
-        (p) => p.name === parameter.name,
-      );
-    }
-
-    // If there is a value from the new or old one, it is not missed
-    if (buildParameter) {
-      continue;
-    }
-
-    missingParameters.push(parameter);
-  }
-
-  // Check if parameter "options" changed and we can't use old build parameters.
-  templateParameters.forEach((templateParameter) => {
-    if (templateParameter.options.length === 0) {
-      return;
-    }
-
-    // Check if there is a new value
-    let buildParameter = newBuildParameters.find(
-      (p) => p.name === templateParameter.name,
-    );
-
-    // If not, get the old one
-    if (!buildParameter) {
-      buildParameter = oldBuildParameters.find(
-        (p) => p.name === templateParameter.name,
-      );
-    }
-
-    if (!buildParameter) {
-      return;
-    }
-
-    const matchingOption = templateParameter.options.find(
-      (option) => option.value === buildParameter?.value,
-    );
-    if (!matchingOption) {
-      missingParameters.push(templateParameter);
-    }
-  });
-  return missingParameters;
-};
-
-/**
- *
- * @param agentId
- * @returns An EventSource that emits agent metadata event objects
- * (ServerSentEvent)
- */
-export const watchAgentMetadata = (agentId: string): EventSource => {
-  return new EventSource(
-    `${location.protocol}//${location.host}/api/v2/workspaceagents/${agentId}/watch-metadata`,
-    { withCredentials: true },
-  );
-};
-
-type WatchBuildLogsByTemplateVersionIdOptions = {
-  after?: number;
-  onMessage: (log: TypesGen.ProvisionerJobLog) => void;
-  onDone?: () => void;
-  onError: (error: Error) => void;
-};
-export const watchBuildLogsByTemplateVersionId = (
-  versionId: string,
-  {
-    onMessage,
-    onDone,
-    onError,
-    after,
-  }: WatchBuildLogsByTemplateVersionIdOptions,
-) => {
-  const searchParams = new URLSearchParams({ follow: "true" });
-  if (after !== undefined) {
-    searchParams.append("after", after.toString());
-  }
-  const proto = location.protocol === "https:" ? "wss:" : "ws:";
-  const socket = new WebSocket(
-    `${proto}//${
-      location.host
-    }/api/v2/templateversions/${versionId}/logs?${searchParams.toString()}`,
-  );
-  socket.binaryType = "blob";
-  socket.addEventListener("message", (event) =>
-    onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog),
-  );
-  socket.addEventListener("error", () => {
-    onError(new Error("Connection for logs failed."));
-    socket.close();
-  });
-  socket.addEventListener("close", () => {
-    // When the socket closes, logs have finished streaming!
-    onDone?.();
-  });
-  return socket;
-};
-
-type WatchWorkspaceAgentLogsOptions = {
-  after: number;
-  onMessage: (logs: TypesGen.WorkspaceAgentLog[]) => void;
-  onDone?: () => void;
-  onError: (error: Error) => void;
-};
-
-export const watchWorkspaceAgentLogs = (
-  agentId: string,
-  { after, onMessage, onDone, onError }: WatchWorkspaceAgentLogsOptions,
-) => {
-  // WebSocket compression in Safari (confirmed in 16.5) is broken when
-  // the server sends large messages. The following error is seen:
-  //
-  //   WebSocket connection to 'wss://.../logs?follow&after=0' failed: The operation couldn’t be completed. Protocol error
-  //
-  const noCompression =
-    userAgentParser(navigator.userAgent).browser.name === "Safari"
-      ? "&no_compression"
-      : "";
-
-  const proto = location.protocol === "https:" ? "wss:" : "ws:";
-  const socket = new WebSocket(
-    `${proto}//${location.host}/api/v2/workspaceagents/${agentId}/logs?follow&after=${after}${noCompression}`,
-  );
-  socket.binaryType = "blob";
-  socket.addEventListener("message", (event) => {
-    const logs = JSON.parse(event.data) as TypesGen.WorkspaceAgentLog[];
-    onMessage(logs);
-  });
-  socket.addEventListener("error", () => {
-    onError(new Error("socket errored"));
-  });
-  socket.addEventListener("close", () => {
-    onDone && onDone();
-  });
-
-  return socket;
-};
-
-type WatchBuildLogsByBuildIdOptions = {
-  after?: number;
-  onMessage: (log: TypesGen.ProvisionerJobLog) => void;
-  onDone?: () => void;
-  onError?: (error: Error) => void;
-};
-export const watchBuildLogsByBuildId = (
-  buildId: string,
-  { onMessage, onDone, onError, after }: WatchBuildLogsByBuildIdOptions,
-) => {
-  const searchParams = new URLSearchParams({ follow: "true" });
-  if (after !== undefined) {
-    searchParams.append("after", after.toString());
-  }
-  const proto = location.protocol === "https:" ? "wss:" : "ws:";
-  const socket = new WebSocket(
-    `${proto}//${
-      location.host
-    }/api/v2/workspacebuilds/${buildId}/logs?${searchParams.toString()}`,
-  );
-  socket.binaryType = "blob";
-  socket.addEventListener("message", (event) =>
-    onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog),
-  );
-  socket.addEventListener("error", () => {
-    onError && onError(new Error("Connection for logs failed."));
-    socket.close();
-  });
-  socket.addEventListener("close", () => {
-    // When the socket closes, logs have finished streaming!
-    onDone && onDone();
-  });
-  return socket;
-};
-
 export const issueReconnectingPTYSignedToken = async (
   params: TypesGen.IssueReconnectingPTYSignedTokenRequest,
 ): Promise<TypesGen.IssueReconnectingPTYSignedTokenResponse> => {
@@ -2453,12 +3347,6 @@ export const getWorkspaceParameters = async (workspace: TypesGen.Workspace) => {
   };
 };
 
-export type InsightsParams = {
-  start_time: string;
-  end_time: string;
-  template_ids: string;
-};
-
 export const getInsightsUserLatency = async (
   filters: InsightsParams,
 ): Promise<TypesGen.UserLatencyInsightsResponse> => {
@@ -2479,10 +3367,6 @@ export const getInsightsUserActivity = async (
   return response.data;
 };
 
-export type InsightsTemplateParams = InsightsParams & {
-  interval: "day" | "week";
-};
-
 export const getInsightsTemplate = async (
   params: InsightsTemplateParams,
 ): Promise<TypesGen.TemplateInsightsResponse> => {
@@ -2527,11 +3411,6 @@ export const deleteFavoriteWorkspace = async (workspaceID: string) => {
   await axiosInstance.delete(`/api/v2/workspaces/${workspaceID}/favorite`);
 };
 
-export type GetJFrogXRayScanParams = {
-  workspaceId: string;
-  agentId: string;
-};
-
 export const getJFrogXRayScan = async (options: GetJFrogXRayScanParams) => {
   const searchParams = new URLSearchParams({
     workspace_id: options.workspaceId,

From f60627eb3f65adb3f889de1547eca5ff238bc798 Mon Sep 17 00:00:00 2001
From: Parkreiner <throwawayclover@gmail.com>
Date: Wed, 1 May 2024 21:45:12 +0000
Subject: [PATCH 4/7] chore: update all import paths to go through client
 instance

---
 site/e2e/api.ts                               |   27 +-
 site/e2e/global.setup.ts                      |    4 +-
 site/e2e/helpers.ts                           |    3 +-
 site/e2e/reporter.ts                          |    3 +-
 site/e2e/tests/deployment/general.spec.ts     |    4 +-
 site/e2e/tests/deployment/network.spec.ts     |    4 +-
 .../tests/deployment/observability.spec.ts    |    4 +-
 site/e2e/tests/deployment/security.spec.ts    |    4 +-
 site/e2e/tests/deployment/userAuth.spec.ts    |    4 +-
 .../tests/deployment/workspaceProxies.spec.ts |    4 +-
 site/e2e/tests/groups/removeMember.spec.ts    |    4 +-
 .../templates/updateTemplateSchedule.spec.ts  |    8 +-
 site/src/api/api.test.ts                      |   79 +-
 site/src/api/api.ts                           | 1520 +----------------
 site/src/api/queries/appearance.ts            |    6 +-
 site/src/api/queries/audits.ts                |    4 +-
 site/src/api/queries/authCheck.ts             |    4 +-
 site/src/api/queries/buildInfo.ts             |    4 +-
 site/src/api/queries/debug.ts                 |   10 +-
 site/src/api/queries/deployment.ts            |   10 +-
 site/src/api/queries/entitlements.ts          |    6 +-
 site/src/api/queries/experiments.ts           |    6 +-
 site/src/api/queries/externalAuth.ts          |   14 +-
 site/src/api/queries/files.ts                 |    6 +-
 site/src/api/queries/groups.ts                |   19 +-
 site/src/api/queries/insights.ts              |   18 +-
 site/src/api/queries/integrations.ts          |    4 +-
 site/src/api/queries/oauth2.ts                |   20 +-
 site/src/api/queries/roles.ts                 |    4 +-
 site/src/api/queries/settings.ts              |    6 +-
 site/src/api/queries/sshKeys.ts               |    6 +-
 site/src/api/queries/templates.ts             |   56 +-
 site/src/api/queries/updateCheck.ts           |    4 +-
 site/src/api/queries/users.ts                 |   40 +-
 site/src/api/queries/workspaceBuilds.ts       |   12 +-
 site/src/api/queries/workspaceQuota.ts        |    6 +-
 site/src/api/queries/workspaceportsharing.ts  |   12 +-
 site/src/api/queries/workspaces.ts            |   41 +-
 site/src/components/Filter/UserFilter.tsx     |    6 +-
 site/src/contexts/ProxyContext.tsx            |    6 +-
 site/src/contexts/auth/RequireAuth.tsx        |    3 +-
 site/src/contexts/useProxyLatency.ts          |    3 +-
 .../src/modules/resources/AppLink/AppLink.tsx |    4 +-
 .../modules/resources/PortForwardButton.tsx   |    4 +-
 .../VSCodeDesktopButton.tsx                   |    8 +-
 site/src/pages/AuditPage/AuditPage.test.tsx   |   16 +-
 .../CreateTemplatePage.test.tsx               |   59 +-
 .../CreateTokenPage/CreateTokenPage.test.tsx  |    4 +-
 .../pages/CreateTokenPage/CreateTokenPage.tsx |    6 +-
 .../CreateWorkspacePage.test.tsx              |   72 +-
 .../CreateWorkspacePage.tsx                   |    4 +-
 .../AddNewLicensePage.tsx                     |    4 +-
 .../LicensesSettingsPage.tsx                  |    6 +-
 .../TemplateEmbedPage.test.tsx                |    4 +-
 .../TemplateEmbedPage/TemplateEmbedPage.tsx   |    5 +-
 .../src/pages/TemplatePage/TemplateLayout.tsx |   16 +-
 .../TemplateSummaryPage.tsx                   |    4 +-
 .../TemplateVersionsPage.tsx                  |   12 +-
 .../useDeletionDialogState.test.ts            |    6 +-
 .../TemplatePage/useDeletionDialogState.ts    |    4 +-
 .../TemplateSettingsPage.test.tsx             |   22 +-
 .../TemplateSettingsPage.tsx                  |    6 +-
 .../TemplateSchedulePage.test.tsx             |   20 +-
 .../TemplateSchedulePage.tsx                  |    5 +-
 .../TemplateVariablesPage.test.tsx            |   28 +-
 .../TemplateVersionEditorPage.test.tsx        |   18 +-
 .../TemplateVersionEditorPage.tsx             |    6 +-
 .../pages/TerminalPage/TerminalPage.test.tsx  |    6 +-
 .../AccountPage/AccountPage.test.tsx          |   50 +-
 .../AppearancePage/AppearancePage.test.tsx    |   18 +-
 .../SSHKeysPage/SSHKeysPage.test.tsx          |   10 +-
 .../SecurityPage/SecurityPage.test.tsx        |   46 +-
 .../SecurityPage/SecurityPage.tsx             |    4 +-
 .../SecurityPage/SingleSignOnSection.tsx      |    4 +-
 .../UserSettingsPage/TokensPage/hooks.ts      |    6 +-
 site/src/pages/UsersPage/UsersPage.test.tsx   |   16 +-
 .../WorkspaceBuildPage.test.tsx               |    4 +-
 .../WorkspaceBuildPage/WorkspaceBuildPage.tsx |    4 +-
 .../BuildParametersPopover.tsx                |    4 +-
 .../WorkspacePage/WorkspacePage.test.tsx      |   58 +-
 .../WorkspacePage/WorkspaceReadyPage.tsx      |    4 +-
 .../WorkspaceScheduleControls.test.tsx        |   10 +-
 .../WorkspaceParametersPage.test.tsx          |   22 +-
 .../WorkspaceParametersPage.tsx               |   10 +-
 .../WorkspaceScheduleForm.test.tsx            |   16 +-
 .../WorkspaceSchedulePage.tsx                 |   15 +-
 .../WorkspaceSettingsPage.test.tsx            |    8 +-
 .../WorkspaceSettingsPage.tsx                 |    6 +-
 .../BatchUpdateConfirmation.tsx               |    4 +-
 .../WorkspacesPage/WorkspacesPage.test.tsx    |   30 +-
 .../src/pages/WorkspacesPage/batchActions.tsx |   23 +-
 site/src/pages/WorkspacesPage/data.ts         |    6 +-
 site/src/pages/WorkspacesPage/filter/menus.ts |    6 +-
 site/src/utils/terminal.ts                    |    4 +-
 94 files changed, 668 insertions(+), 2077 deletions(-)

diff --git a/site/e2e/api.ts b/site/e2e/api.ts
index 65a4aaa40a937..fb442ca7f5bd7 100644
--- a/site/e2e/api.ts
+++ b/site/e2e/api.ts
@@ -1,7 +1,7 @@
 import type { Page } from "@playwright/test";
 import { expect } from "@playwright/test";
 import { formatDuration, intervalToDuration } from "date-fns";
-import * as API from "api/api";
+import { type DeploymentConfig, client } from "api/api";
 import type { SerpentOption } from "api/typesGenerated";
 import { coderPort } from "./constants";
 import { findSessionToken, randomName } from "./helpers";
@@ -11,25 +11,26 @@ let currentOrgId: string;
 export const setupApiCalls = async (page: Page) => {
   try {
     const token = await findSessionToken(page);
-    API.setSessionToken(token);
+    client.setSessionToken(token);
   } catch {
     // If this fails, we have an unauthenticated client.
   }
-  API.setHost(`http://127.0.0.1:${coderPort}`);
+
+  client.setHost(`http://127.0.0.1:${coderPort}`);
 };
 
 export const getCurrentOrgId = async (): Promise<string> => {
   if (currentOrgId) {
     return currentOrgId;
   }
-  const currentUser = await API.getAuthenticatedUser();
+  const currentUser = await client.api.getAuthenticatedUser();
   currentOrgId = currentUser.organization_ids[0];
   return currentOrgId;
 };
 
 export const createUser = async (orgId: string) => {
   const name = randomName();
-  const user = await API.createUser({
+  const user = await client.api.createUser({
     email: `${name}@coder.com`,
     username: name,
     password: "s3cure&password!",
@@ -42,7 +43,7 @@ export const createUser = async (orgId: string) => {
 
 export const createGroup = async (orgId: string) => {
   const name = randomName();
-  const group = await API.createGroup(orgId, {
+  const group = await client.api.createGroup(orgId, {
     name,
     display_name: `Display ${name}`,
     avatar_url: "/emojis/1f60d.png",
@@ -53,7 +54,7 @@ export const createGroup = async (orgId: string) => {
 
 export async function verifyConfigFlagBoolean(
   page: Page,
-  config: API.DeploymentConfig,
+  config: DeploymentConfig,
   flag: string,
 ) {
   const opt = findConfigOption(config, flag);
@@ -68,7 +69,7 @@ export async function verifyConfigFlagBoolean(
 
 export async function verifyConfigFlagNumber(
   page: Page,
-  config: API.DeploymentConfig,
+  config: DeploymentConfig,
   flag: string,
 ) {
   const opt = findConfigOption(config, flag);
@@ -80,7 +81,7 @@ export async function verifyConfigFlagNumber(
 
 export async function verifyConfigFlagString(
   page: Page,
-  config: API.DeploymentConfig,
+  config: DeploymentConfig,
   flag: string,
 ) {
   const opt = findConfigOption(config, flag);
@@ -100,7 +101,7 @@ export async function verifyConfigFlagEmpty(page: Page, flag: string) {
 
 export async function verifyConfigFlagArray(
   page: Page,
-  config: API.DeploymentConfig,
+  config: DeploymentConfig,
   flag: string,
 ) {
   const opt = findConfigOption(config, flag);
@@ -116,7 +117,7 @@ export async function verifyConfigFlagArray(
 
 export async function verifyConfigFlagEntries(
   page: Page,
-  config: API.DeploymentConfig,
+  config: DeploymentConfig,
   flag: string,
 ) {
   const opt = findConfigOption(config, flag);
@@ -138,7 +139,7 @@ export async function verifyConfigFlagEntries(
 
 export async function verifyConfigFlagDuration(
   page: Page,
-  config: API.DeploymentConfig,
+  config: DeploymentConfig,
   flag: string,
 ) {
   const opt = findConfigOption(config, flag);
@@ -157,7 +158,7 @@ export async function verifyConfigFlagDuration(
 }
 
 export function findConfigOption(
-  config: API.DeploymentConfig,
+  config: DeploymentConfig,
   flag: string,
 ): SerpentOption {
   const opt = config.options.find((option) => option.flag === flag);
diff --git a/site/e2e/global.setup.ts b/site/e2e/global.setup.ts
index 8c8526af9acc1..e577e048ddb27 100644
--- a/site/e2e/global.setup.ts
+++ b/site/e2e/global.setup.ts
@@ -1,5 +1,5 @@
 import { expect, test } from "@playwright/test";
-import { hasFirstUser } from "api/api";
+import { client } from "api/api";
 import { Language } from "pages/CreateUserPage/CreateUserForm";
 import { setupApiCalls } from "./api";
 import * as constants from "./constants";
@@ -9,7 +9,7 @@ import { storageState } from "./playwright.config";
 test("setup deployment", async ({ page }) => {
   await page.goto("/", { waitUntil: "domcontentloaded" });
   await setupApiCalls(page);
-  const exists = await hasFirstUser();
+  const exists = await client.api.hasFirstUser();
   // First user already exists, abort early. All tests execute this as a dependency,
   // if you run multiple tests in the UI, this will fail unless we check this.
   if (exists) {
diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts
index 1c5349fbf5e5b..474b378904371 100644
--- a/site/e2e/helpers.ts
+++ b/site/e2e/helpers.ts
@@ -6,7 +6,7 @@ import capitalize from "lodash/capitalize";
 import path from "path";
 import * as ssh from "ssh2";
 import { Duplex } from "stream";
-import { axiosInstance } from "api/api";
+import { client } from "api/api";
 import type {
   WorkspaceBuildParameter,
   UpdateTemplateMeta,
@@ -396,6 +396,7 @@ export const waitUntilUrlIsNotResponding = async (url: string) => {
   const retryIntervalMs = 1000;
   let retries = 0;
 
+  const axiosInstance = client.getAxiosInstance();
   while (retries < maxRetries) {
     try {
       await axiosInstance.get(url);
diff --git a/site/e2e/reporter.ts b/site/e2e/reporter.ts
index f981dede78e66..edbe9b12bc72e 100644
--- a/site/e2e/reporter.ts
+++ b/site/e2e/reporter.ts
@@ -10,7 +10,7 @@ import type {
 } from "@playwright/test/reporter";
 import * as fs from "fs/promises";
 import type { Writable } from "stream";
-import { axiosInstance } from "api/api";
+import { client } from "api/api";
 import { coderdPProfPort, enterpriseLicense } from "./constants";
 
 class CoderReporter implements Reporter {
@@ -136,6 +136,7 @@ class CoderReporter implements Reporter {
 const logLines = (chunk: string): string[] => chunk.trimEnd().split("\n");
 
 const exportDebugPprof = async (outputFile: string) => {
+  const axiosInstance = client.getAxiosInstance();
   const response = await axiosInstance.get(
     `http://127.0.0.1:${coderdPProfPort}/debug/pprof/goroutine?debug=1`,
   );
diff --git a/site/e2e/tests/deployment/general.spec.ts b/site/e2e/tests/deployment/general.spec.ts
index de334a95b05e3..c52ec09b641f0 100644
--- a/site/e2e/tests/deployment/general.spec.ts
+++ b/site/e2e/tests/deployment/general.spec.ts
@@ -1,5 +1,5 @@
 import { expect, test } from "@playwright/test";
-import * as API from "api/api";
+import { client } from "api/api";
 import { setupApiCalls } from "../../api";
 import { e2eFakeExperiment1, e2eFakeExperiment2 } from "../../constants";
 
@@ -7,7 +7,7 @@ test("experiments", async ({ page }) => {
   await setupApiCalls(page);
 
   // Load experiments from backend API
-  const availableExperiments = await API.getAvailableExperiments();
+  const availableExperiments = await client.api.getAvailableExperiments();
 
   // Verify if the site lists the same experiments
   await page.goto("/deployment/general", { waitUntil: "networkidle" });
diff --git a/site/e2e/tests/deployment/network.spec.ts b/site/e2e/tests/deployment/network.spec.ts
index c979bb8e1022f..36ea46d7f45ac 100644
--- a/site/e2e/tests/deployment/network.spec.ts
+++ b/site/e2e/tests/deployment/network.spec.ts
@@ -1,5 +1,5 @@
 import { test } from "@playwright/test";
-import { getDeploymentConfig } from "api/api";
+import { client } from "api/api";
 import {
   setupApiCalls,
   verifyConfigFlagArray,
@@ -11,7 +11,7 @@ import {
 
 test("enabled network settings", async ({ page }) => {
   await setupApiCalls(page);
-  const config = await getDeploymentConfig();
+  const config = await client.api.getDeploymentConfig();
 
   await page.goto("/deployment/network", { waitUntil: "domcontentloaded" });
 
diff --git a/site/e2e/tests/deployment/observability.spec.ts b/site/e2e/tests/deployment/observability.spec.ts
index e94f14b6ceebc..0268c8baa3ab1 100644
--- a/site/e2e/tests/deployment/observability.spec.ts
+++ b/site/e2e/tests/deployment/observability.spec.ts
@@ -1,5 +1,5 @@
 import { test } from "@playwright/test";
-import { getDeploymentConfig } from "api/api";
+import { client } from "api/api";
 import {
   setupApiCalls,
   verifyConfigFlagArray,
@@ -11,7 +11,7 @@ import {
 
 test("enabled observability settings", async ({ page }) => {
   await setupApiCalls(page);
-  const config = await getDeploymentConfig();
+  const config = await client.api.getDeploymentConfig();
 
   await page.goto("/deployment/observability", {
     waitUntil: "domcontentloaded",
diff --git a/site/e2e/tests/deployment/security.spec.ts b/site/e2e/tests/deployment/security.spec.ts
index ede966260ca44..e8a79776a8ebc 100644
--- a/site/e2e/tests/deployment/security.spec.ts
+++ b/site/e2e/tests/deployment/security.spec.ts
@@ -1,7 +1,7 @@
 import type { Page } from "@playwright/test";
 import { expect, test } from "@playwright/test";
 import type * as API from "api/api";
-import { getDeploymentConfig } from "api/api";
+import { client } from "api/api";
 import {
   findConfigOption,
   setupApiCalls,
@@ -12,7 +12,7 @@ import {
 
 test("enabled security settings", async ({ page }) => {
   await setupApiCalls(page);
-  const config = await getDeploymentConfig();
+  const config = await client.api.getDeploymentConfig();
 
   await page.goto("/deployment/security", { waitUntil: "domcontentloaded" });
 
diff --git a/site/e2e/tests/deployment/userAuth.spec.ts b/site/e2e/tests/deployment/userAuth.spec.ts
index cf656c99fae3f..8083e4f66537d 100644
--- a/site/e2e/tests/deployment/userAuth.spec.ts
+++ b/site/e2e/tests/deployment/userAuth.spec.ts
@@ -1,5 +1,5 @@
 import { test } from "@playwright/test";
-import { getDeploymentConfig } from "api/api";
+import { client } from "api/api";
 import {
   setupApiCalls,
   verifyConfigFlagArray,
@@ -10,7 +10,7 @@ import {
 
 test("login with OIDC", async ({ page }) => {
   await setupApiCalls(page);
-  const config = await getDeploymentConfig();
+  const config = await client.api.getDeploymentConfig();
 
   await page.goto("/deployment/userauth", { waitUntil: "domcontentloaded" });
 
diff --git a/site/e2e/tests/deployment/workspaceProxies.spec.ts b/site/e2e/tests/deployment/workspaceProxies.spec.ts
index 5f67bda7d7ad4..ff02853f01b83 100644
--- a/site/e2e/tests/deployment/workspaceProxies.spec.ts
+++ b/site/e2e/tests/deployment/workspaceProxies.spec.ts
@@ -1,5 +1,5 @@
 import { test, expect, type Page } from "@playwright/test";
-import { createWorkspaceProxy } from "api/api";
+import { client } from "api/api";
 import { setupApiCalls } from "../../api";
 import { coderPort, workspaceProxyPort } from "../../constants";
 import { randomName, requiresEnterpriseLicense } from "../../helpers";
@@ -34,7 +34,7 @@ test("custom proxy is online", async ({ page }) => {
   const proxyName = randomName();
 
   // Register workspace proxy
-  const proxyResponse = await createWorkspaceProxy({
+  const proxyResponse = await client.api.createWorkspaceProxy({
     name: proxyName,
     display_name: "",
     icon: "/emojis/1f1e7-1f1f7.png",
diff --git a/site/e2e/tests/groups/removeMember.spec.ts b/site/e2e/tests/groups/removeMember.spec.ts
index 716c86af84a8d..3d2fb6534e846 100644
--- a/site/e2e/tests/groups/removeMember.spec.ts
+++ b/site/e2e/tests/groups/removeMember.spec.ts
@@ -1,5 +1,5 @@
 import { test, expect } from "@playwright/test";
-import * as API from "api/api";
+import { client } from "api/api";
 import {
   createGroup,
   createUser,
@@ -19,7 +19,7 @@ test("remove member", async ({ page, baseURL }) => {
     createGroup(orgId),
     createUser(orgId),
   ]);
-  await API.addMember(group.id, member.id);
+  await client.api.addMember(group.id, member.id);
 
   await page.goto(`${baseURL}/groups/${group.id}`, {
     waitUntil: "domcontentloaded",
diff --git a/site/e2e/tests/templates/updateTemplateSchedule.spec.ts b/site/e2e/tests/templates/updateTemplateSchedule.spec.ts
index 1eb272a665edb..768d41518d7a3 100644
--- a/site/e2e/tests/templates/updateTemplateSchedule.spec.ts
+++ b/site/e2e/tests/templates/updateTemplateSchedule.spec.ts
@@ -1,5 +1,5 @@
 import { expect, test } from "@playwright/test";
-import { createTemplate, createTemplateVersion, getTemplate } from "api/api";
+import { client } from "api/api";
 import { getCurrentOrgId, setupApiCalls } from "../../api";
 import { beforeCoderTest } from "../../hooks";
 
@@ -11,14 +11,14 @@ test("update template schedule settings without override other settings", async
 }) => {
   await setupApiCalls(page);
   const orgId = await getCurrentOrgId();
-  const templateVersion = await createTemplateVersion(orgId, {
+  const templateVersion = await client.api.createTemplateVersion(orgId, {
     storage_method: "file" as const,
     provisioner: "echo",
     user_variable_values: [],
     example_id: "docker",
     tags: {},
   });
-  const template = await createTemplate(orgId, {
+  const template = await client.api.createTemplate(orgId, {
     name: "test-template",
     display_name: "Test Template",
     template_version_id: templateVersion.id,
@@ -33,7 +33,7 @@ test("update template schedule settings without override other settings", async
   await page.getByRole("button", { name: "Submit" }).click();
   await expect(page.getByText("Template updated successfully")).toBeVisible();
 
-  const updatedTemplate = await getTemplate(template.id);
+  const updatedTemplate = await client.api.getTemplate(template.id);
   // Validate that the template data remains consistent, with the exception of
   // the 'default_ttl_ms' field (updated during the test) and the 'updated at'
   // field (automatically updated by the backend).
diff --git a/site/src/api/api.test.ts b/site/src/api/api.test.ts
index 18615306683c4..f2547a22cd80c 100644
--- a/site/src/api/api.test.ts
+++ b/site/src/api/api.test.ts
@@ -6,10 +6,11 @@ import {
   MockWorkspaceBuild,
   MockWorkspaceBuildParameter1,
 } from "testHelpers/entities";
-import * as api from "./api";
-import { axiosInstance } from "./api";
+import { client, getURLWithSearchParams, MissingBuildParameters } from "./api";
 import type * as TypesGen from "./typesGenerated";
 
+const axiosInstance = client.getAxiosInstance();
+
 describe("api.ts", () => {
   describe("login", () => {
     it("should return LoginResponse", async () => {
@@ -23,7 +24,7 @@ describe("api.ts", () => {
         .mockResolvedValueOnce({ data: loginResponse });
 
       // when
-      const result = await api.login("test", "123");
+      const result = await client.api.login("test", "123");
 
       // then
       expect(axiosInstance.post).toHaveBeenCalled();
@@ -44,7 +45,7 @@ describe("api.ts", () => {
       axiosInstance.post = axiosMockPost;
 
       try {
-        await api.login("test", "123");
+        await client.api.login("test", "123");
       } catch (error) {
         expect(error).toStrictEqual(expectedError);
       }
@@ -60,7 +61,7 @@ describe("api.ts", () => {
       axiosInstance.post = axiosMockPost;
 
       // when
-      await api.logout();
+      await client.api.logout();
 
       // then
       expect(axiosMockPost).toHaveBeenCalled();
@@ -80,7 +81,7 @@ describe("api.ts", () => {
       axiosInstance.post = axiosMockPost;
 
       try {
-        await api.logout();
+        await client.api.logout();
       } catch (error) {
         expect(error).toStrictEqual(expectedError);
       }
@@ -100,7 +101,7 @@ describe("api.ts", () => {
       axiosInstance.post = axiosMockPost;
 
       // when
-      const result = await api.getApiKey();
+      const result = await client.api.getApiKey();
 
       // then
       expect(axiosMockPost).toHaveBeenCalled();
@@ -121,7 +122,7 @@ describe("api.ts", () => {
       axiosInstance.post = axiosMockPost;
 
       try {
-        await api.getApiKey();
+        await client.api.getApiKey();
       } catch (error) {
         expect(error).toStrictEqual(expectedError);
       }
@@ -147,7 +148,7 @@ describe("api.ts", () => {
     ])(
       `Workspaces - getURLWithSearchParams(%p, %p) returns %p`,
       (basePath, filter, expected) => {
-        expect(api.getURLWithSearchParams(basePath, filter)).toBe(expected);
+        expect(getURLWithSearchParams(basePath, filter)).toBe(expected);
       },
     );
   });
@@ -164,7 +165,7 @@ describe("api.ts", () => {
     ])(
       `Users - getURLWithSearchParams(%p, %p) returns %p`,
       (basePath, filter, expected) => {
-        expect(api.getURLWithSearchParams(basePath, filter)).toBe(expected);
+        expect(getURLWithSearchParams(basePath, filter)).toBe(expected);
       },
     );
   });
@@ -172,25 +173,30 @@ describe("api.ts", () => {
   describe("update", () => {
     it("creates a build with start and the latest template", async () => {
       jest
-        .spyOn(api, "postWorkspaceBuild")
+        .spyOn(client.api, "postWorkspaceBuild")
         .mockResolvedValueOnce(MockWorkspaceBuild);
-      jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate);
-      await api.updateWorkspace(MockWorkspace);
-      expect(api.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, {
-        transition: "start",
-        template_version_id: MockTemplate.active_version_id,
-        rich_parameter_values: [],
-      });
+      jest.spyOn(client.api, "getTemplate").mockResolvedValueOnce(MockTemplate);
+      await client.api.updateWorkspace(MockWorkspace);
+      expect(client.api.postWorkspaceBuild).toHaveBeenCalledWith(
+        MockWorkspace.id,
+        {
+          transition: "start",
+          template_version_id: MockTemplate.active_version_id,
+          rich_parameter_values: [],
+        },
+      );
     });
 
     it("fails when having missing parameters", async () => {
       jest
-        .spyOn(api, "postWorkspaceBuild")
+        .spyOn(client.api, "postWorkspaceBuild")
         .mockResolvedValue(MockWorkspaceBuild);
-      jest.spyOn(api, "getTemplate").mockResolvedValue(MockTemplate);
-      jest.spyOn(api, "getWorkspaceBuildParameters").mockResolvedValue([]);
+      jest.spyOn(client.api, "getTemplate").mockResolvedValue(MockTemplate);
       jest
-        .spyOn(api, "getTemplateVersionRichParameters")
+        .spyOn(client.api, "getWorkspaceBuildParameters")
+        .mockResolvedValue([]);
+      jest
+        .spyOn(client.api, "getTemplateVersionRichParameters")
         .mockResolvedValue([
           MockTemplateVersionParameter1,
           { ...MockTemplateVersionParameter2, mutable: false },
@@ -198,14 +204,14 @@ describe("api.ts", () => {
 
       let error = new Error();
       try {
-        await api.updateWorkspace(MockWorkspace);
+        await client.api.updateWorkspace(MockWorkspace);
       } catch (e) {
         error = e as Error;
       }
 
-      expect(error).toBeInstanceOf(api.MissingBuildParameters);
+      expect(error).toBeInstanceOf(MissingBuildParameters);
       // Verify if the correct missing parameters are being passed
-      expect((error as api.MissingBuildParameters).parameters).toEqual([
+      expect((error as MissingBuildParameters).parameters).toEqual([
         MockTemplateVersionParameter1,
         { ...MockTemplateVersionParameter2, mutable: false },
       ]);
@@ -213,23 +219,26 @@ describe("api.ts", () => {
 
     it("creates a build with the no parameters if it is already filled", async () => {
       jest
-        .spyOn(api, "postWorkspaceBuild")
+        .spyOn(client.api, "postWorkspaceBuild")
         .mockResolvedValueOnce(MockWorkspaceBuild);
-      jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate);
+      jest.spyOn(client.api, "getTemplate").mockResolvedValueOnce(MockTemplate);
       jest
-        .spyOn(api, "getWorkspaceBuildParameters")
+        .spyOn(client.api, "getWorkspaceBuildParameters")
         .mockResolvedValue([MockWorkspaceBuildParameter1]);
       jest
-        .spyOn(api, "getTemplateVersionRichParameters")
+        .spyOn(client.api, "getTemplateVersionRichParameters")
         .mockResolvedValue([
           { ...MockTemplateVersionParameter1, required: true, mutable: false },
         ]);
-      await api.updateWorkspace(MockWorkspace);
-      expect(api.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, {
-        transition: "start",
-        template_version_id: MockTemplate.active_version_id,
-        rich_parameter_values: [],
-      });
+      await client.api.updateWorkspace(MockWorkspace);
+      expect(client.api.postWorkspaceBuild).toHaveBeenCalledWith(
+        MockWorkspace.id,
+        {
+          transition: "start",
+          template_version_id: MockTemplate.active_version_id,
+          rich_parameter_values: [],
+        },
+      );
     });
   });
 });
diff --git a/site/src/api/api.ts b/site/src/api/api.ts
index 976a0ba984895..5a8e4bfb1374f 100644
--- a/site/src/api/api.ts
+++ b/site/src/api/api.ts
@@ -850,7 +850,7 @@ export class Api {
   };
 
   deleteWorkspace = (workspaceId: string, options?: DeleteWorkspaceOptions) => {
-    return postWorkspaceBuild(workspaceId, {
+    return this.postWorkspaceBuild(workspaceId, {
       transition: "delete",
       ...options,
     });
@@ -914,7 +914,7 @@ export class Api {
       buildParameters,
     );
 
-    await waitForBuild(startBuild);
+    await this.waitForBuild(startBuild);
   };
 
   cancelTemplateVersionBuild = async (
@@ -1435,7 +1435,7 @@ export class Api {
   };
 
   addMember = async (groupId: string, userId: string) => {
-    return patchGroup(groupId, {
+    return this.patchGroup(groupId, {
       name: "",
       add_users: [userId],
       remove_users: [],
@@ -1443,7 +1443,7 @@ export class Api {
   };
 
   removeMember = async (groupId: string, userId: string) => {
-    return patchGroup(groupId, {
+    return this.patchGroup(groupId, {
       name: "",
       display_name: "",
       add_users: [],
@@ -1619,8 +1619,8 @@ export class Api {
   updateWorkspaceVersion = async (
     workspace: TypesGen.Workspace,
   ): Promise<TypesGen.WorkspaceBuild> => {
-    const template = await getTemplate(workspace.template_id);
-    return startWorkspace(workspace.id, template.active_version_id);
+    const template = await this.getTemplate(workspace.template_id);
+    return this.startWorkspace(workspace.id, template.active_version_id);
   };
 
   getWorkspaceBuildParameters = async (
@@ -1705,7 +1705,7 @@ export class Api {
 
     const activeVersionId = template.active_version_id;
     const templateParameters =
-      await getTemplateVersionRichParameters(activeVersionId);
+      await this.getTemplateVersionRichParameters(activeVersionId);
 
     const missingParameters = getMissingParameters(
       oldBuildParameters,
@@ -1717,7 +1717,7 @@ export class Api {
       throw new MissingBuildParameters(missingParameters, activeVersionId);
     }
 
-    return postWorkspaceBuild(workspace.id, {
+    return this.postWorkspaceBuild(workspace.id, {
       transition: "start",
       template_version_id: activeVersionId,
       rich_parameter_values: newBuildParameters,
@@ -1871,6 +1871,7 @@ interface ClientApi {
   getCsrfToken: () => string;
   setSessionToken: (token: string) => void;
   setHost: (host: string | undefined) => void;
+  getAxiosInstance: () => AxiosInstance;
 }
 
 export class Client implements ClientApi {
@@ -1927,1505 +1928,10 @@ export class Client implements ClientApi {
   setHost = (host: string | undefined): void => {
     this.axios.defaults.baseURL = host;
   };
-}
-
-export const client = new Client();
-
-////////////////////////////////////////////////////////////////////////////////
-// START OF OLD CODE
-////////////////////////////////////////////////////////////////////////////////
-
-export const axiosInstance = globalAxios.create();
-
-// Adds 304 for the default axios validateStatus function
-// https://github.com/axios/axios#handling-errors Check status here
-// https://httpstatusdogs.com/
-axiosInstance.defaults.validateStatus = (status) => {
-  return (status >= 200 && status < 300) || status === 304;
-};
-
-export const hardCodedCSRFCookie = (): string => {
-  // This is a hard coded CSRF token/cookie pair for local development. In prod,
-  // the GoLang webserver generates a random cookie with a new token for each
-  // document request. For local development, we don't use the Go webserver for
-  // static files, so this is the 'hack' to make local development work with
-  // remote apis. The CSRF cookie for this token is
-  // "JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4="
-  const csrfToken =
-    "KNKvagCBEHZK7ihe2t7fj6VeJ0UyTDco1yVUJE8N06oNqxLu5Zx1vRxZbgfC0mJJgeGkVjgs08mgPbcWPBkZ1A==";
-  axiosInstance.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken;
-  return csrfToken;
-};
-
-// Always attach CSRF token to all requests. In puppeteer the document is
-// undefined. In those cases, just do nothing.
-const token =
-  typeof document !== "undefined"
-    ? document.head.querySelector('meta[property="csrf-token"]')
-    : null;
-
-if (token !== null && token.getAttribute("content") !== null) {
-  if (process.env.NODE_ENV === "development") {
-    // Development mode uses a hard-coded CSRF token
-    axiosInstance.defaults.headers.common["X-CSRF-TOKEN"] =
-      hardCodedCSRFCookie();
-    token.setAttribute("content", hardCodedCSRFCookie());
-  } else {
-    axiosInstance.defaults.headers.common["X-CSRF-TOKEN"] =
-      token.getAttribute("content") ?? "";
-  }
-} else {
-  // Do not write error logs if we are in a FE unit test.
-  if (process.env.JEST_WORKER_ID === undefined) {
-    console.error("CSRF token not found");
-  }
-}
-
-export const setSessionToken = (token: string) => {
-  axiosInstance.defaults.headers.common["Coder-Session-Token"] = token;
-};
-
-export const setHost = (host?: string) => {
-  axiosInstance.defaults.baseURL = host;
-};
-
-export const login = async (
-  email: string,
-  password: string,
-): Promise<TypesGen.LoginWithPasswordResponse> => {
-  const payload = JSON.stringify({
-    email,
-    password,
-  });
-
-  const response = await axiosInstance.post<TypesGen.LoginWithPasswordResponse>(
-    "/api/v2/users/login",
-    payload,
-    {
-      headers: { ...BASE_CONTENT_TYPE_JSON },
-    },
-  );
-
-  return response.data;
-};
-
-export const convertToOAUTH = async (request: TypesGen.ConvertLoginRequest) => {
-  const response = await axiosInstance.post<TypesGen.OAuthConversionResponse>(
-    "/api/v2/users/me/convert-login",
-    request,
-  );
-  return response.data;
-};
-
-export const logout = async (): Promise<void> => {
-  await axiosInstance.post("/api/v2/users/logout");
-};
-
-export const getAuthenticatedUser = async () => {
-  const response = await axiosInstance.get<TypesGen.User>("/api/v2/users/me");
-  return response.data;
-};
-
-export const getUserParameters = async (templateID: string) => {
-  const response = await axiosInstance.get<TypesGen.UserParameter[]>(
-    "/api/v2/users/me/autofill-parameters?template_id=" + templateID,
-  );
-  return response.data;
-};
-
-export const getAuthMethods = async (): Promise<TypesGen.AuthMethods> => {
-  const response = await axiosInstance.get<TypesGen.AuthMethods>(
-    "/api/v2/users/authmethods",
-  );
-  return response.data;
-};
-
-export const getUserLoginType = async (): Promise<TypesGen.UserLoginType> => {
-  const response = await axiosInstance.get<TypesGen.UserLoginType>(
-    "/api/v2/users/me/login-type",
-  );
-  return response.data;
-};
-
-export const checkAuthorization = async (
-  params: TypesGen.AuthorizationRequest,
-): Promise<TypesGen.AuthorizationResponse> => {
-  const response = await axiosInstance.post<TypesGen.AuthorizationResponse>(
-    `/api/v2/authcheck`,
-    params,
-  );
-  return response.data;
-};
-
-export const getApiKey = async (): Promise<TypesGen.GenerateAPIKeyResponse> => {
-  const response = await axiosInstance.post<TypesGen.GenerateAPIKeyResponse>(
-    "/api/v2/users/me/keys",
-  );
-  return response.data;
-};
-
-export const getTokens = async (
-  params: TypesGen.TokensFilter,
-): Promise<TypesGen.APIKeyWithOwner[]> => {
-  const response = await axiosInstance.get<TypesGen.APIKeyWithOwner[]>(
-    `/api/v2/users/me/keys/tokens`,
-    {
-      params,
-    },
-  );
-  return response.data;
-};
-
-export const deleteToken = async (keyId: string): Promise<void> => {
-  await axiosInstance.delete("/api/v2/users/me/keys/" + keyId);
-};
-
-export const createToken = async (
-  params: TypesGen.CreateTokenRequest,
-): Promise<TypesGen.GenerateAPIKeyResponse> => {
-  const response = await axiosInstance.post(
-    `/api/v2/users/me/keys/tokens`,
-    params,
-  );
-  return response.data;
-};
-
-export const getTokenConfig = async (): Promise<TypesGen.TokenConfig> => {
-  const response = await axiosInstance.get(
-    "/api/v2/users/me/keys/tokens/tokenconfig",
-  );
-  return response.data;
-};
-
-export const getUsers = async (
-  options: TypesGen.UsersRequest,
-  signal?: AbortSignal,
-): Promise<TypesGen.GetUsersResponse> => {
-  const url = getURLWithSearchParams("/api/v2/users", options);
-  const response = await axiosInstance.get<TypesGen.GetUsersResponse>(
-    url.toString(),
-    {
-      signal,
-    },
-  );
-  return response.data;
-};
-
-export const getOrganization = async (
-  organizationId: string,
-): Promise<TypesGen.Organization> => {
-  const response = await axiosInstance.get<TypesGen.Organization>(
-    `/api/v2/organizations/${organizationId}`,
-  );
-  return response.data;
-};
-
-export const getOrganizations = async (): Promise<TypesGen.Organization[]> => {
-  const response = await axiosInstance.get<TypesGen.Organization[]>(
-    "/api/v2/users/me/organizations",
-  );
-  return response.data;
-};
-
-export const getTemplate = async (
-  templateId: string,
-): Promise<TypesGen.Template> => {
-  const response = await axiosInstance.get<TypesGen.Template>(
-    `/api/v2/templates/${templateId}`,
-  );
-  return response.data;
-};
-
-export const getTemplates = async (
-  organizationId: string,
-  options?: TemplateOptions,
-): Promise<TypesGen.Template[]> => {
-  const params = {} as Record<string, string>;
-  if (options && options.deprecated !== undefined) {
-    // Just want to check if it isn't undefined. If it has
-    // a boolean value, convert it to a string and include
-    // it as a param.
-    params["deprecated"] = String(options.deprecated);
-  }
-
-  const response = await axiosInstance.get<TypesGen.Template[]>(
-    `/api/v2/organizations/${organizationId}/templates`,
-    {
-      params,
-    },
-  );
-  return response.data;
-};
-
-export const getTemplateByName = async (
-  organizationId: string,
-  name: string,
-): Promise<TypesGen.Template> => {
-  const response = await axiosInstance.get<TypesGen.Template>(
-    `/api/v2/organizations/${organizationId}/templates/${name}`,
-  );
-  return response.data;
-};
-
-export const getTemplateVersion = async (
-  versionId: string,
-): Promise<TypesGen.TemplateVersion> => {
-  const response = await axiosInstance.get<TypesGen.TemplateVersion>(
-    `/api/v2/templateversions/${versionId}`,
-  );
-  return response.data;
-};
-
-export const getTemplateVersionResources = async (
-  versionId: string,
-): Promise<TypesGen.WorkspaceResource[]> => {
-  const response = await axiosInstance.get<TypesGen.WorkspaceResource[]>(
-    `/api/v2/templateversions/${versionId}/resources`,
-  );
-  return response.data;
-};
-
-export const getTemplateVersionVariables = async (
-  versionId: string,
-): Promise<TypesGen.TemplateVersionVariable[]> => {
-  const response = await axiosInstance.get<TypesGen.TemplateVersionVariable[]>(
-    `/api/v2/templateversions/${versionId}/variables`,
-  );
-  return response.data;
-};
-
-export const getTemplateVersions = async (
-  templateId: string,
-): Promise<TypesGen.TemplateVersion[]> => {
-  const response = await axiosInstance.get<TypesGen.TemplateVersion[]>(
-    `/api/v2/templates/${templateId}/versions`,
-  );
-  return response.data;
-};
-
-export const getTemplateVersionByName = async (
-  organizationId: string,
-  templateName: string,
-  versionName: string,
-): Promise<TypesGen.TemplateVersion> => {
-  const response = await axiosInstance.get<TypesGen.TemplateVersion>(
-    `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}`,
-  );
-  return response.data;
-};
-
-export const getPreviousTemplateVersionByName = async (
-  organizationId: string,
-  templateName: string,
-  versionName: string,
-) => {
-  try {
-    const response = await axiosInstance.get<TypesGen.TemplateVersion>(
-      `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}/previous`,
-    );
-    return response.data;
-  } catch (error) {
-    // When there is no previous version, like the first version of a template,
-    // the API returns 404 so in this case we can safely return undefined
-    if (
-      isAxiosError(error) &&
-      error.response &&
-      error.response.status === 404
-    ) {
-      return undefined;
-    }
-
-    throw error;
-  }
-};
-
-export const createTemplateVersion = async (
-  organizationId: string,
-  data: TypesGen.CreateTemplateVersionRequest,
-): Promise<TypesGen.TemplateVersion> => {
-  const response = await axiosInstance.post<TypesGen.TemplateVersion>(
-    `/api/v2/organizations/${organizationId}/templateversions`,
-    data,
-  );
-  return response.data;
-};
-
-export const getTemplateVersionExternalAuth = async (
-  versionId: string,
-): Promise<TypesGen.TemplateVersionExternalAuth[]> => {
-  const response = await axiosInstance.get(
-    `/api/v2/templateversions/${versionId}/external-auth`,
-  );
-  return response.data;
-};
-
-export const getTemplateVersionRichParameters = async (
-  versionId: string,
-): Promise<TypesGen.TemplateVersionParameter[]> => {
-  const response = await axiosInstance.get(
-    `/api/v2/templateversions/${versionId}/rich-parameters`,
-  );
-  return response.data;
-};
 
-export const createTemplate = async (
-  organizationId: string,
-  data: TypesGen.CreateTemplateRequest,
-): Promise<TypesGen.Template> => {
-  const response = await axiosInstance.post(
-    `/api/v2/organizations/${organizationId}/templates`,
-    data,
-  );
-  return response.data;
-};
-
-export const updateActiveTemplateVersion = async (
-  templateId: string,
-  data: TypesGen.UpdateActiveTemplateVersion,
-) => {
-  const response = await axiosInstance.patch<TypesGen.Response>(
-    `/api/v2/templates/${templateId}/versions`,
-    data,
-  );
-  return response.data;
-};
-
-export const patchTemplateVersion = async (
-  templateVersionId: string,
-  data: TypesGen.PatchTemplateVersionRequest,
-) => {
-  const response = await axiosInstance.patch<TypesGen.TemplateVersion>(
-    `/api/v2/templateversions/${templateVersionId}`,
-    data,
-  );
-  return response.data;
-};
-
-export const archiveTemplateVersion = async (templateVersionId: string) => {
-  const response = await axiosInstance.post<TypesGen.TemplateVersion>(
-    `/api/v2/templateversions/${templateVersionId}/archive`,
-  );
-  return response.data;
-};
-
-export const unarchiveTemplateVersion = async (templateVersionId: string) => {
-  const response = await axiosInstance.post<TypesGen.TemplateVersion>(
-    `/api/v2/templateversions/${templateVersionId}/unarchive`,
-  );
-  return response.data;
-};
-
-export const updateTemplateMeta = async (
-  templateId: string,
-  data: TypesGen.UpdateTemplateMeta,
-): Promise<TypesGen.Template | null> => {
-  const response = await axiosInstance.patch<TypesGen.Template>(
-    `/api/v2/templates/${templateId}`,
-    data,
-  );
-  // On 304 response there is no data payload.
-  if (response.status === 304) {
-    return null;
-  }
-
-  return response.data;
-};
-
-export const deleteTemplate = async (
-  templateId: string,
-): Promise<TypesGen.Template> => {
-  const response = await axiosInstance.delete<TypesGen.Template>(
-    `/api/v2/templates/${templateId}`,
-  );
-  return response.data;
-};
-
-export const getWorkspace = async (
-  workspaceId: string,
-  params?: TypesGen.WorkspaceOptions,
-): Promise<TypesGen.Workspace> => {
-  const response = await axiosInstance.get<TypesGen.Workspace>(
-    `/api/v2/workspaces/${workspaceId}`,
-    {
-      params,
-    },
-  );
-  return response.data;
-};
-
-export const getWorkspaces = async (
-  options: TypesGen.WorkspacesRequest,
-): Promise<TypesGen.WorkspacesResponse> => {
-  const url = getURLWithSearchParams("/api/v2/workspaces", options);
-  const response = await axiosInstance.get<TypesGen.WorkspacesResponse>(url);
-  return response.data;
-};
-
-export const getWorkspaceByOwnerAndName = async (
-  username = "me",
-  workspaceName: string,
-  params?: TypesGen.WorkspaceOptions,
-): Promise<TypesGen.Workspace> => {
-  const response = await axiosInstance.get<TypesGen.Workspace>(
-    `/api/v2/users/${username}/workspace/${workspaceName}`,
-    {
-      params,
-    },
-  );
-  return response.data;
-};
-
-export function waitForBuild(build: TypesGen.WorkspaceBuild) {
-  return new Promise<TypesGen.ProvisionerJob | undefined>((res, reject) => {
-    void (async () => {
-      let latestJobInfo: TypesGen.ProvisionerJob | undefined = undefined;
-
-      while (
-        !["succeeded", "canceled"].some(
-          (status) => latestJobInfo?.status.includes(status),
-        )
-      ) {
-        const { job } = await getWorkspaceBuildByNumber(
-          build.workspace_owner_name,
-          build.workspace_name,
-          build.build_number,
-        );
-        latestJobInfo = job;
-
-        if (latestJobInfo.status === "failed") {
-          return reject(latestJobInfo);
-        }
-
-        await delay(1000);
-      }
-
-      return res(latestJobInfo);
-    })();
-  });
-}
-
-export const postWorkspaceBuild = async (
-  workspaceId: string,
-  data: TypesGen.CreateWorkspaceBuildRequest,
-): Promise<TypesGen.WorkspaceBuild> => {
-  const response = await axiosInstance.post(
-    `/api/v2/workspaces/${workspaceId}/builds`,
-    data,
-  );
-  return response.data;
-};
-
-export const startWorkspace = (
-  workspaceId: string,
-  templateVersionId: string,
-  logLevel?: TypesGen.ProvisionerLogLevel,
-  buildParameters?: TypesGen.WorkspaceBuildParameter[],
-) =>
-  postWorkspaceBuild(workspaceId, {
-    transition: "start",
-    template_version_id: templateVersionId,
-    log_level: logLevel,
-    rich_parameter_values: buildParameters,
-  });
-export const stopWorkspace = (
-  workspaceId: string,
-  logLevel?: TypesGen.ProvisionerLogLevel,
-) =>
-  postWorkspaceBuild(workspaceId, {
-    transition: "stop",
-    log_level: logLevel,
-  });
-
-export const deleteWorkspace = (
-  workspaceId: string,
-  options?: DeleteWorkspaceOptions,
-) =>
-  postWorkspaceBuild(workspaceId, {
-    transition: "delete",
-    ...options,
-  });
-
-export const cancelWorkspaceBuild = async (
-  workspaceBuildId: TypesGen.WorkspaceBuild["id"],
-): Promise<TypesGen.Response> => {
-  const response = await axiosInstance.patch(
-    `/api/v2/workspacebuilds/${workspaceBuildId}/cancel`,
-  );
-  return response.data;
-};
-
-export const updateWorkspaceDormancy = async (
-  workspaceId: string,
-  dormant: boolean,
-): Promise<TypesGen.Workspace> => {
-  const data: TypesGen.UpdateWorkspaceDormancy = {
-    dormant: dormant,
+  getAxiosInstance = (): AxiosInstance => {
+    return this.axios;
   };
+}
 
-  const response = await axiosInstance.put(
-    `/api/v2/workspaces/${workspaceId}/dormant`,
-    data,
-  );
-  return response.data;
-};
-
-export const updateWorkspaceAutomaticUpdates = async (
-  workspaceId: string,
-  automaticUpdates: TypesGen.AutomaticUpdates,
-): Promise<void> => {
-  const req: TypesGen.UpdateWorkspaceAutomaticUpdatesRequest = {
-    automatic_updates: automaticUpdates,
-  };
-
-  const response = await axiosInstance.put(
-    `/api/v2/workspaces/${workspaceId}/autoupdates`,
-    req,
-  );
-  return response.data;
-};
-
-export const restartWorkspace = async ({
-  workspace,
-  buildParameters,
-}: {
-  workspace: TypesGen.Workspace;
-  buildParameters?: TypesGen.WorkspaceBuildParameter[];
-}) => {
-  const stopBuild = await stopWorkspace(workspace.id);
-  const awaitedStopBuild = await waitForBuild(stopBuild);
-
-  // If the restart is canceled halfway through, make sure we bail
-  if (awaitedStopBuild?.status === "canceled") {
-    return;
-  }
-
-  const startBuild = await startWorkspace(
-    workspace.id,
-    workspace.latest_build.template_version_id,
-    undefined,
-    buildParameters,
-  );
-  await waitForBuild(startBuild);
-};
-
-export const cancelTemplateVersionBuild = async (
-  templateVersionId: TypesGen.TemplateVersion["id"],
-): Promise<TypesGen.Response> => {
-  const response = await axiosInstance.patch(
-    `/api/v2/templateversions/${templateVersionId}/cancel`,
-  );
-  return response.data;
-};
-
-export const createUser = async (
-  user: TypesGen.CreateUserRequest,
-): Promise<TypesGen.User> => {
-  const response = await axiosInstance.post<TypesGen.User>(
-    "/api/v2/users",
-    user,
-  );
-  return response.data;
-};
-
-export const createWorkspace = async (
-  organizationId: string,
-  userId = "me",
-  workspace: TypesGen.CreateWorkspaceRequest,
-): Promise<TypesGen.Workspace> => {
-  const response = await axiosInstance.post<TypesGen.Workspace>(
-    `/api/v2/organizations/${organizationId}/members/${userId}/workspaces`,
-    workspace,
-  );
-  return response.data;
-};
-
-export const patchWorkspace = async (
-  workspaceId: string,
-  data: TypesGen.UpdateWorkspaceRequest,
-) => {
-  await axiosInstance.patch(`/api/v2/workspaces/${workspaceId}`, data);
-};
-
-export const getBuildInfo = async (): Promise<TypesGen.BuildInfoResponse> => {
-  const response = await axiosInstance.get("/api/v2/buildinfo");
-  return response.data;
-};
-
-export const getUpdateCheck =
-  async (): Promise<TypesGen.UpdateCheckResponse> => {
-    const response = await axiosInstance.get("/api/v2/updatecheck");
-    return response.data;
-  };
-
-export const putWorkspaceAutostart = async (
-  workspaceID: string,
-  autostart: TypesGen.UpdateWorkspaceAutostartRequest,
-): Promise<void> => {
-  const payload = JSON.stringify(autostart);
-  await axiosInstance.put(
-    `/api/v2/workspaces/${workspaceID}/autostart`,
-    payload,
-    {
-      headers: { ...BASE_CONTENT_TYPE_JSON },
-    },
-  );
-};
-
-export const putWorkspaceAutostop = async (
-  workspaceID: string,
-  ttl: TypesGen.UpdateWorkspaceTTLRequest,
-): Promise<void> => {
-  const payload = JSON.stringify(ttl);
-  await axiosInstance.put(`/api/v2/workspaces/${workspaceID}/ttl`, payload, {
-    headers: { ...BASE_CONTENT_TYPE_JSON },
-  });
-};
-
-export const updateProfile = async (
-  userId: string,
-  data: TypesGen.UpdateUserProfileRequest,
-): Promise<TypesGen.User> => {
-  const response = await axiosInstance.put(
-    `/api/v2/users/${userId}/profile`,
-    data,
-  );
-  return response.data;
-};
-
-export const updateAppearanceSettings = async (
-  userId: string,
-  data: TypesGen.UpdateUserAppearanceSettingsRequest,
-): Promise<TypesGen.User> => {
-  const response = await axiosInstance.put(
-    `/api/v2/users/${userId}/appearance`,
-    data,
-  );
-  return response.data;
-};
-
-export const getUserQuietHoursSchedule = async (
-  userId: TypesGen.User["id"],
-): Promise<TypesGen.UserQuietHoursScheduleResponse> => {
-  const response = await axiosInstance.get(
-    `/api/v2/users/${userId}/quiet-hours`,
-  );
-  return response.data;
-};
-
-export const updateUserQuietHoursSchedule = async (
-  userId: TypesGen.User["id"],
-  data: TypesGen.UpdateUserQuietHoursScheduleRequest,
-): Promise<TypesGen.UserQuietHoursScheduleResponse> => {
-  const response = await axiosInstance.put(
-    `/api/v2/users/${userId}/quiet-hours`,
-    data,
-  );
-  return response.data;
-};
-
-export const activateUser = async (
-  userId: TypesGen.User["id"],
-): Promise<TypesGen.User> => {
-  const response = await axiosInstance.put<TypesGen.User>(
-    `/api/v2/users/${userId}/status/activate`,
-  );
-  return response.data;
-};
-
-export const suspendUser = async (
-  userId: TypesGen.User["id"],
-): Promise<TypesGen.User> => {
-  const response = await axiosInstance.put<TypesGen.User>(
-    `/api/v2/users/${userId}/status/suspend`,
-  );
-  return response.data;
-};
-
-export const deleteUser = async (
-  userId: TypesGen.User["id"],
-): Promise<undefined> => {
-  return await axiosInstance.delete(`/api/v2/users/${userId}`);
-};
-
-// API definition:
-// https://github.com/coder/coder/blob/db665e7261f3c24a272ccec48233a3e276878239/coderd/users.go#L33-L53
-export const hasFirstUser = async (): Promise<boolean> => {
-  try {
-    // If it is success, it is true
-    await axiosInstance.get("/api/v2/users/first");
-    return true;
-  } catch (error) {
-    // If it returns a 404, it is false
-    if (isAxiosError(error) && error.response?.status === 404) {
-      return false;
-    }
-
-    throw error;
-  }
-};
-
-export const createFirstUser = async (
-  req: TypesGen.CreateFirstUserRequest,
-): Promise<TypesGen.CreateFirstUserResponse> => {
-  const response = await axiosInstance.post(`/api/v2/users/first`, req);
-  return response.data;
-};
-
-export const updateUserPassword = async (
-  userId: TypesGen.User["id"],
-  updatePassword: TypesGen.UpdateUserPasswordRequest,
-): Promise<undefined> =>
-  axiosInstance.put(`/api/v2/users/${userId}/password`, updatePassword);
-
-export const getRoles = async (): Promise<Array<TypesGen.AssignableRoles>> => {
-  const response =
-    await axiosInstance.get<Array<TypesGen.AssignableRoles>>(
-      `/api/v2/users/roles`,
-    );
-  return response.data;
-};
-
-export const updateUserRoles = async (
-  roles: TypesGen.Role["name"][],
-  userId: TypesGen.User["id"],
-): Promise<TypesGen.User> => {
-  const response = await axiosInstance.put<TypesGen.User>(
-    `/api/v2/users/${userId}/roles`,
-    { roles },
-  );
-  return response.data;
-};
-
-export const getUserSSHKey = async (
-  userId = "me",
-): Promise<TypesGen.GitSSHKey> => {
-  const response = await axiosInstance.get<TypesGen.GitSSHKey>(
-    `/api/v2/users/${userId}/gitsshkey`,
-  );
-  return response.data;
-};
-
-export const regenerateUserSSHKey = async (
-  userId = "me",
-): Promise<TypesGen.GitSSHKey> => {
-  const response = await axiosInstance.put<TypesGen.GitSSHKey>(
-    `/api/v2/users/${userId}/gitsshkey`,
-  );
-  return response.data;
-};
-
-export const getWorkspaceBuilds = async (
-  workspaceId: string,
-  req?: TypesGen.WorkspaceBuildsRequest,
-) => {
-  const response = await axiosInstance.get<TypesGen.WorkspaceBuild[]>(
-    getURLWithSearchParams(`/api/v2/workspaces/${workspaceId}/builds`, req),
-  );
-  return response.data;
-};
-
-export const getWorkspaceBuildByNumber = async (
-  username = "me",
-  workspaceName: string,
-  buildNumber: number,
-): Promise<TypesGen.WorkspaceBuild> => {
-  const response = await axiosInstance.get<TypesGen.WorkspaceBuild>(
-    `/api/v2/users/${username}/workspace/${workspaceName}/builds/${buildNumber}`,
-  );
-  return response.data;
-};
-
-export const getWorkspaceBuildLogs = async (
-  buildId: string,
-  before: Date,
-): Promise<TypesGen.ProvisionerJobLog[]> => {
-  const response = await axiosInstance.get<TypesGen.ProvisionerJobLog[]>(
-    `/api/v2/workspacebuilds/${buildId}/logs?before=${before.getTime()}`,
-  );
-  return response.data;
-};
-
-export const getWorkspaceAgentLogs = async (
-  agentID: string,
-): Promise<TypesGen.WorkspaceAgentLog[]> => {
-  const response = await axiosInstance.get<TypesGen.WorkspaceAgentLog[]>(
-    `/api/v2/workspaceagents/${agentID}/logs`,
-  );
-  return response.data;
-};
-
-export const putWorkspaceExtension = async (
-  workspaceId: string,
-  newDeadline: dayjs.Dayjs,
-): Promise<void> => {
-  await axiosInstance.put(`/api/v2/workspaces/${workspaceId}/extend`, {
-    deadline: newDeadline,
-  });
-};
-
-export const refreshEntitlements = async (): Promise<void> => {
-  await axiosInstance.post("/api/v2/licenses/refresh-entitlements");
-};
-
-export const getEntitlements = async (): Promise<TypesGen.Entitlements> => {
-  try {
-    const response = await axiosInstance.get("/api/v2/entitlements");
-    return response.data;
-  } catch (ex) {
-    if (isAxiosError(ex) && ex.response?.status === 404) {
-      return {
-        errors: [],
-        features: withDefaultFeatures({}),
-        has_license: false,
-        require_telemetry: false,
-        trial: false,
-        warnings: [],
-        refreshed_at: "",
-      };
-    }
-    throw ex;
-  }
-};
-
-export const getExperiments = async (): Promise<TypesGen.Experiment[]> => {
-  try {
-    const response = await axiosInstance.get("/api/v2/experiments");
-    return response.data;
-  } catch (error) {
-    if (isAxiosError(error) && error.response?.status === 404) {
-      return [];
-    }
-    throw error;
-  }
-};
-
-export const getAvailableExperiments =
-  async (): Promise<TypesGen.AvailableExperiments> => {
-    try {
-      const response = await axiosInstance.get("/api/v2/experiments/available");
-      return response.data;
-    } catch (error) {
-      if (isAxiosError(error) && error.response?.status === 404) {
-        return { safe: [] };
-      }
-      throw error;
-    }
-  };
-
-export const getExternalAuthProvider = async (
-  provider: string,
-): Promise<TypesGen.ExternalAuth> => {
-  const resp = await axiosInstance.get(`/api/v2/external-auth/${provider}`);
-  return resp.data;
-};
-
-export const getExternalAuthDevice = async (
-  provider: string,
-): Promise<TypesGen.ExternalAuthDevice> => {
-  const resp = await axiosInstance.get(
-    `/api/v2/external-auth/${provider}/device`,
-  );
-  return resp.data;
-};
-
-export const exchangeExternalAuthDevice = async (
-  provider: string,
-  req: TypesGen.ExternalAuthDeviceExchange,
-): Promise<void> => {
-  const resp = await axiosInstance.post(
-    `/api/v2/external-auth/${provider}/device`,
-    req,
-  );
-  return resp.data;
-};
-
-export const getUserExternalAuthProviders =
-  async (): Promise<TypesGen.ListUserExternalAuthResponse> => {
-    const resp = await axiosInstance.get(`/api/v2/external-auth`);
-    return resp.data;
-  };
-
-export const unlinkExternalAuthProvider = async (
-  provider: string,
-): Promise<string> => {
-  const resp = await axiosInstance.delete(`/api/v2/external-auth/${provider}`);
-  return resp.data;
-};
-
-export const getOAuth2ProviderApps = async (
-  filter?: TypesGen.OAuth2ProviderAppFilter,
-): Promise<TypesGen.OAuth2ProviderApp[]> => {
-  const params = filter?.user_id
-    ? new URLSearchParams({ user_id: filter.user_id })
-    : "";
-  const resp = await axiosInstance.get(
-    `/api/v2/oauth2-provider/apps?${params}`,
-  );
-  return resp.data;
-};
-
-export const getOAuth2ProviderApp = async (
-  id: string,
-): Promise<TypesGen.OAuth2ProviderApp> => {
-  const resp = await axiosInstance.get(`/api/v2/oauth2-provider/apps/${id}`);
-  return resp.data;
-};
-
-export const postOAuth2ProviderApp = async (
-  data: TypesGen.PostOAuth2ProviderAppRequest,
-): Promise<TypesGen.OAuth2ProviderApp> => {
-  const response = await axiosInstance.post(
-    `/api/v2/oauth2-provider/apps`,
-    data,
-  );
-  return response.data;
-};
-
-export const putOAuth2ProviderApp = async (
-  id: string,
-  data: TypesGen.PutOAuth2ProviderAppRequest,
-): Promise<TypesGen.OAuth2ProviderApp> => {
-  const response = await axiosInstance.put(
-    `/api/v2/oauth2-provider/apps/${id}`,
-    data,
-  );
-  return response.data;
-};
-
-export const deleteOAuth2ProviderApp = async (id: string): Promise<void> => {
-  await axiosInstance.delete(`/api/v2/oauth2-provider/apps/${id}`);
-};
-
-export const getOAuth2ProviderAppSecrets = async (
-  id: string,
-): Promise<TypesGen.OAuth2ProviderAppSecret[]> => {
-  const resp = await axiosInstance.get(
-    `/api/v2/oauth2-provider/apps/${id}/secrets`,
-  );
-  return resp.data;
-};
-
-export const postOAuth2ProviderAppSecret = async (
-  id: string,
-): Promise<TypesGen.OAuth2ProviderAppSecretFull> => {
-  const resp = await axiosInstance.post(
-    `/api/v2/oauth2-provider/apps/${id}/secrets`,
-  );
-  return resp.data;
-};
-
-export const deleteOAuth2ProviderAppSecret = async (
-  appId: string,
-  secretId: string,
-): Promise<void> => {
-  await axiosInstance.delete(
-    `/api/v2/oauth2-provider/apps/${appId}/secrets/${secretId}`,
-  );
-};
-
-export const revokeOAuth2ProviderApp = async (appId: string): Promise<void> => {
-  await axiosInstance.delete(`/oauth2/tokens?client_id=${appId}`);
-};
-
-export const getAuditLogs = async (
-  options: TypesGen.AuditLogsRequest,
-): Promise<TypesGen.AuditLogResponse> => {
-  const url = getURLWithSearchParams("/api/v2/audit", options);
-  const response = await axiosInstance.get(url);
-  return response.data;
-};
-
-export const getTemplateDAUs = async (
-  templateId: string,
-): Promise<TypesGen.DAUsResponse> => {
-  const response = await axiosInstance.get(
-    `/api/v2/templates/${templateId}/daus`,
-  );
-  return response.data;
-};
-
-export const getDeploymentDAUs = async (
-  // Default to user's local timezone.
-  // As /api/v2/insights/daus only accepts whole-number values for tz_offset
-  // we truncate the tz offset down to the closest hour.
-  offset = Math.trunc(new Date().getTimezoneOffset() / 60),
-): Promise<TypesGen.DAUsResponse> => {
-  const response = await axiosInstance.get(
-    `/api/v2/insights/daus?tz_offset=${offset}`,
-  );
-  return response.data;
-};
-
-export const getTemplateACLAvailable = async (
-  templateId: string,
-  options: TypesGen.UsersRequest,
-): Promise<TypesGen.ACLAvailable> => {
-  const url = getURLWithSearchParams(
-    `/api/v2/templates/${templateId}/acl/available`,
-    options,
-  );
-  const response = await axiosInstance.get(url.toString());
-  return response.data;
-};
-
-export const getTemplateACL = async (
-  templateId: string,
-): Promise<TypesGen.TemplateACL> => {
-  const response = await axiosInstance.get(
-    `/api/v2/templates/${templateId}/acl`,
-  );
-  return response.data;
-};
-
-export const updateTemplateACL = async (
-  templateId: string,
-  data: TypesGen.UpdateTemplateACL,
-): Promise<{ message: string }> => {
-  const response = await axiosInstance.patch(
-    `/api/v2/templates/${templateId}/acl`,
-    data,
-  );
-  return response.data;
-};
-
-export const getApplicationsHost =
-  async (): Promise<TypesGen.AppHostResponse> => {
-    const response = await axiosInstance.get(`/api/v2/applications/host`);
-    return response.data;
-  };
-
-export const getGroups = async (
-  organizationId: string,
-): Promise<TypesGen.Group[]> => {
-  const response = await axiosInstance.get(
-    `/api/v2/organizations/${organizationId}/groups`,
-  );
-  return response.data;
-};
-
-export const createGroup = async (
-  organizationId: string,
-  data: TypesGen.CreateGroupRequest,
-): Promise<TypesGen.Group> => {
-  const response = await axiosInstance.post(
-    `/api/v2/organizations/${organizationId}/groups`,
-    data,
-  );
-  return response.data;
-};
-
-export const getGroup = async (groupId: string): Promise<TypesGen.Group> => {
-  const response = await axiosInstance.get(`/api/v2/groups/${groupId}`);
-  return response.data;
-};
-
-export const patchGroup = async (
-  groupId: string,
-  data: TypesGen.PatchGroupRequest,
-): Promise<TypesGen.Group> => {
-  const response = await axiosInstance.patch(`/api/v2/groups/${groupId}`, data);
-  return response.data;
-};
-
-export const addMember = async (groupId: string, userId: string) => {
-  return patchGroup(groupId, {
-    name: "",
-    add_users: [userId],
-    remove_users: [],
-  });
-};
-
-export const removeMember = async (groupId: string, userId: string) => {
-  return patchGroup(groupId, {
-    name: "",
-    display_name: "",
-    add_users: [],
-    remove_users: [userId],
-  });
-};
-
-export const deleteGroup = async (groupId: string): Promise<void> => {
-  await axiosInstance.delete(`/api/v2/groups/${groupId}`);
-};
-
-export const getWorkspaceQuota = async (
-  username: string,
-): Promise<TypesGen.WorkspaceQuota> => {
-  const response = await axiosInstance.get(
-    `/api/v2/workspace-quota/${encodeURIComponent(username)}`,
-  );
-  return response.data;
-};
-
-export const getAgentListeningPorts = async (
-  agentID: string,
-): Promise<TypesGen.WorkspaceAgentListeningPortsResponse> => {
-  const response = await axiosInstance.get(
-    `/api/v2/workspaceagents/${agentID}/listening-ports`,
-  );
-  return response.data;
-};
-
-export const getWorkspaceAgentSharedPorts = async (
-  workspaceID: string,
-): Promise<TypesGen.WorkspaceAgentPortShares> => {
-  const response = await axiosInstance.get(
-    `/api/v2/workspaces/${workspaceID}/port-share`,
-  );
-  return response.data;
-};
-
-export const upsertWorkspaceAgentSharedPort = async (
-  workspaceID: string,
-  req: TypesGen.UpsertWorkspaceAgentPortShareRequest,
-): Promise<TypesGen.WorkspaceAgentPortShares> => {
-  const response = await axiosInstance.post(
-    `/api/v2/workspaces/${workspaceID}/port-share`,
-    req,
-  );
-  return response.data;
-};
-
-export const deleteWorkspaceAgentSharedPort = async (
-  workspaceID: string,
-  req: TypesGen.DeleteWorkspaceAgentPortShareRequest,
-): Promise<TypesGen.WorkspaceAgentPortShares> => {
-  const response = await axiosInstance.delete(
-    `/api/v2/workspaces/${workspaceID}/port-share`,
-    {
-      data: req,
-    },
-  );
-  return response.data;
-};
-
-// getDeploymentSSHConfig is used by the VSCode-Extension.
-export const getDeploymentSSHConfig =
-  async (): Promise<TypesGen.SSHConfigResponse> => {
-    const response = await axiosInstance.get(`/api/v2/deployment/ssh`);
-    return response.data;
-  };
-
-export const getDeploymentConfig = async (): Promise<DeploymentConfig> => {
-  const response = await axiosInstance.get(`/api/v2/deployment/config`);
-  return response.data;
-};
-
-export const getDeploymentStats =
-  async (): Promise<TypesGen.DeploymentStats> => {
-    const response = await axiosInstance.get(`/api/v2/deployment/stats`);
-    return response.data;
-  };
-
-export const getReplicas = async (): Promise<TypesGen.Replica[]> => {
-  const response = await axiosInstance.get(`/api/v2/replicas`);
-  return response.data;
-};
-
-export const getFile = async (fileId: string): Promise<ArrayBuffer> => {
-  const response = await axiosInstance.get<ArrayBuffer>(
-    `/api/v2/files/${fileId}`,
-    {
-      responseType: "arraybuffer",
-    },
-  );
-  return response.data;
-};
-
-export const getWorkspaceProxyRegions = async (): Promise<
-  TypesGen.RegionsResponse<TypesGen.Region>
-> => {
-  const response =
-    await axiosInstance.get<TypesGen.RegionsResponse<TypesGen.Region>>(
-      `/api/v2/regions`,
-    );
-  return response.data;
-};
-
-export const getWorkspaceProxies = async (): Promise<
-  TypesGen.RegionsResponse<TypesGen.WorkspaceProxy>
-> => {
-  const response = await axiosInstance.get<
-    TypesGen.RegionsResponse<TypesGen.WorkspaceProxy>
-  >(`/api/v2/workspaceproxies`);
-  return response.data;
-};
-
-export const createWorkspaceProxy = async (
-  b: TypesGen.CreateWorkspaceProxyRequest,
-): Promise<TypesGen.UpdateWorkspaceProxyResponse> => {
-  const response = await axiosInstance.post(`/api/v2/workspaceproxies`, b);
-  return response.data;
-};
-
-export const getAppearance = async (): Promise<TypesGen.AppearanceConfig> => {
-  try {
-    const response = await axiosInstance.get(`/api/v2/appearance`);
-    return response.data || {};
-  } catch (ex) {
-    if (isAxiosError(ex) && ex.response?.status === 404) {
-      return {
-        application_name: "",
-        logo_url: "",
-        service_banner: {
-          enabled: false,
-        },
-      };
-    }
-    throw ex;
-  }
-};
-
-export const updateAppearance = async (
-  b: TypesGen.AppearanceConfig,
-): Promise<TypesGen.AppearanceConfig> => {
-  const response = await axiosInstance.put(`/api/v2/appearance`, b);
-  return response.data;
-};
-
-export const getTemplateExamples = async (
-  organizationId: string,
-): Promise<TypesGen.TemplateExample[]> => {
-  const response = await axiosInstance.get(
-    `/api/v2/organizations/${organizationId}/templates/examples`,
-  );
-  return response.data;
-};
-
-export const uploadFile = async (
-  file: File,
-): Promise<TypesGen.UploadResponse> => {
-  const response = await axiosInstance.post("/api/v2/files", file, {
-    headers: {
-      "Content-Type": "application/x-tar",
-    },
-  });
-  return response.data;
-};
-
-export const getTemplateVersionLogs = async (
-  versionId: string,
-): Promise<TypesGen.ProvisionerJobLog[]> => {
-  const response = await axiosInstance.get<TypesGen.ProvisionerJobLog[]>(
-    `/api/v2/templateversions/${versionId}/logs`,
-  );
-  return response.data;
-};
-
-export const updateWorkspaceVersion = async (
-  workspace: TypesGen.Workspace,
-): Promise<TypesGen.WorkspaceBuild> => {
-  const template = await getTemplate(workspace.template_id);
-  return startWorkspace(workspace.id, template.active_version_id);
-};
-
-export const getWorkspaceBuildParameters = async (
-  workspaceBuildId: TypesGen.WorkspaceBuild["id"],
-): Promise<TypesGen.WorkspaceBuildParameter[]> => {
-  const response = await axiosInstance.get<TypesGen.WorkspaceBuildParameter[]>(
-    `/api/v2/workspacebuilds/${workspaceBuildId}/parameters`,
-  );
-  return response.data;
-};
-
-export const getLicenses = async (): Promise<GetLicensesResponse[]> => {
-  const response = await axiosInstance.get(`/api/v2/licenses`);
-  return response.data;
-};
-
-export const createLicense = async (
-  data: TypesGen.AddLicenseRequest,
-): Promise<TypesGen.AddLicenseRequest> => {
-  const response = await axiosInstance.post(`/api/v2/licenses`, data);
-  return response.data;
-};
-
-export const removeLicense = async (licenseId: number): Promise<void> => {
-  await axiosInstance.delete(`/api/v2/licenses/${licenseId}`);
-};
-
-/** Steps to change the workspace version
- * - Get the latest template to access the latest active version
- * - Get the current build parameters
- * - Get the template parameters
- * - Update the build parameters and check if there are missed parameters for the new version
- *   - If there are missing parameters raise an error
- * - Create a build with the version and updated build parameters
- */
-export const changeWorkspaceVersion = async (
-  workspace: TypesGen.Workspace,
-  templateVersionId: string,
-  newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
-): Promise<TypesGen.WorkspaceBuild> => {
-  const [currentBuildParameters, templateParameters] = await Promise.all([
-    getWorkspaceBuildParameters(workspace.latest_build.id),
-    getTemplateVersionRichParameters(templateVersionId),
-  ]);
-
-  const missingParameters = getMissingParameters(
-    currentBuildParameters,
-    newBuildParameters,
-    templateParameters,
-  );
-
-  if (missingParameters.length > 0) {
-    throw new MissingBuildParameters(missingParameters, templateVersionId);
-  }
-
-  return postWorkspaceBuild(workspace.id, {
-    transition: "start",
-    template_version_id: templateVersionId,
-    rich_parameter_values: newBuildParameters,
-  });
-};
-
-/** Steps to update the workspace
- * - Get the latest template to access the latest active version
- * - Get the current build parameters
- * - Get the template parameters
- * - Update the build parameters and check if there are missed parameters for
- *   the newest version
- *   - If there are missing parameters raise an error
- * - Create a build with the latest version and updated build parameters
- */
-export const updateWorkspace = async (
-  workspace: TypesGen.Workspace,
-  newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
-): Promise<TypesGen.WorkspaceBuild> => {
-  const [template, oldBuildParameters] = await Promise.all([
-    getTemplate(workspace.template_id),
-    getWorkspaceBuildParameters(workspace.latest_build.id),
-  ]);
-  const activeVersionId = template.active_version_id;
-  const templateParameters =
-    await getTemplateVersionRichParameters(activeVersionId);
-  const missingParameters = getMissingParameters(
-    oldBuildParameters,
-    newBuildParameters,
-    templateParameters,
-  );
-
-  if (missingParameters.length > 0) {
-    throw new MissingBuildParameters(missingParameters, activeVersionId);
-  }
-
-  return postWorkspaceBuild(workspace.id, {
-    transition: "start",
-    template_version_id: activeVersionId,
-    rich_parameter_values: newBuildParameters,
-  });
-};
-
-export const getWorkspaceResolveAutostart = async (
-  workspaceId: string,
-): Promise<TypesGen.ResolveAutostartResponse> => {
-  const response = await axiosInstance.get(
-    `/api/v2/workspaces/${workspaceId}/resolve-autostart`,
-  );
-  return response.data;
-};
-
-export const issueReconnectingPTYSignedToken = async (
-  params: TypesGen.IssueReconnectingPTYSignedTokenRequest,
-): Promise<TypesGen.IssueReconnectingPTYSignedTokenResponse> => {
-  const response = await axiosInstance.post(
-    "/api/v2/applications/reconnecting-pty-signed-token",
-    params,
-  );
-  return response.data;
-};
-
-export const getWorkspaceParameters = async (workspace: TypesGen.Workspace) => {
-  const latestBuild = workspace.latest_build;
-  const [templateVersionRichParameters, buildParameters] = await Promise.all([
-    getTemplateVersionRichParameters(latestBuild.template_version_id),
-    getWorkspaceBuildParameters(latestBuild.id),
-  ]);
-  return {
-    templateVersionRichParameters,
-    buildParameters,
-  };
-};
-
-export const getInsightsUserLatency = async (
-  filters: InsightsParams,
-): Promise<TypesGen.UserLatencyInsightsResponse> => {
-  const params = new URLSearchParams(filters);
-  const response = await axiosInstance.get(
-    `/api/v2/insights/user-latency?${params}`,
-  );
-  return response.data;
-};
-
-export const getInsightsUserActivity = async (
-  filters: InsightsParams,
-): Promise<TypesGen.UserActivityInsightsResponse> => {
-  const params = new URLSearchParams(filters);
-  const response = await axiosInstance.get(
-    `/api/v2/insights/user-activity?${params}`,
-  );
-  return response.data;
-};
-
-export const getInsightsTemplate = async (
-  params: InsightsTemplateParams,
-): Promise<TypesGen.TemplateInsightsResponse> => {
-  const searchParams = new URLSearchParams(params);
-  const response = await axiosInstance.get(
-    `/api/v2/insights/templates?${searchParams}`,
-  );
-  return response.data;
-};
-
-export const getHealth = async (force: boolean = false) => {
-  const params = new URLSearchParams({ force: force.toString() });
-  const response = await axiosInstance.get<TypesGen.HealthcheckReport>(
-    `/api/v2/debug/health?${params}`,
-  );
-  return response.data;
-};
-
-export const getHealthSettings = async () => {
-  return (
-    await axiosInstance.get<TypesGen.HealthSettings>(
-      `/api/v2/debug/health/settings`,
-    )
-  ).data;
-};
-
-export const updateHealthSettings = async (
-  data: TypesGen.UpdateHealthSettings,
-) => {
-  const response = await axiosInstance.put<TypesGen.HealthSettings>(
-    `/api/v2/debug/health/settings`,
-    data,
-  );
-  return response.data;
-};
-
-export const putFavoriteWorkspace = async (workspaceID: string) => {
-  await axiosInstance.put(`/api/v2/workspaces/${workspaceID}/favorite`);
-};
-
-export const deleteFavoriteWorkspace = async (workspaceID: string) => {
-  await axiosInstance.delete(`/api/v2/workspaces/${workspaceID}/favorite`);
-};
-
-export const getJFrogXRayScan = async (options: GetJFrogXRayScanParams) => {
-  const searchParams = new URLSearchParams({
-    workspace_id: options.workspaceId,
-    agent_id: options.agentId,
-  });
-
-  try {
-    const res = await axiosInstance.get<TypesGen.JFrogXrayScan>(
-      `/api/v2/integrations/jfrog/xray-scan?${searchParams}`,
-    );
-    return res.data;
-  } catch (error) {
-    if (isAxiosError(error) && error.response?.status === 404) {
-      // react-query library does not allow undefined to be returned as a query result
-      return null;
-    }
-  }
-};
+export const client = new Client();
diff --git a/site/src/api/queries/appearance.ts b/site/src/api/queries/appearance.ts
index 8178ed602abb7..a60ebd32f0c17 100644
--- a/site/src/api/queries/appearance.ts
+++ b/site/src/api/queries/appearance.ts
@@ -1,5 +1,5 @@
 import type { QueryClient, UseQueryOptions } from "react-query";
-import * as API from "api/api";
+import { client } from "api/api";
 import type { AppearanceConfig } from "api/typesGenerated";
 import { getMetadataAsJSON } from "utils/metadata";
 import { cachedQuery } from "./util";
@@ -12,13 +12,13 @@ export const appearance = (): UseQueryOptions<AppearanceConfig> => {
   return cachedQuery({
     initialData: initialAppearanceData,
     queryKey: ["appearance"],
-    queryFn: () => API.getAppearance(),
+    queryFn: () => client.api.getAppearance(),
   });
 };
 
 export const updateAppearance = (queryClient: QueryClient) => {
   return {
-    mutationFn: API.updateAppearance,
+    mutationFn: client.api.updateAppearance,
     onSuccess: (newConfig: AppearanceConfig) => {
       queryClient.setQueryData(appearanceConfigKey, newConfig);
     },
diff --git a/site/src/api/queries/audits.ts b/site/src/api/queries/audits.ts
index 6430767480714..22326c1c8d4a8 100644
--- a/site/src/api/queries/audits.ts
+++ b/site/src/api/queries/audits.ts
@@ -1,4 +1,4 @@
-import { getAuditLogs } from "api/api";
+import { client } from "api/api";
 import type { AuditLogResponse } from "api/typesGenerated";
 import { useFilterParamsKey } from "components/Filter/filter";
 import type { UsePaginatedQueryOptions } from "hooks/usePaginatedQuery";
@@ -13,7 +13,7 @@ export function paginatedAudits(
       return ["auditLogs", payload, pageNumber] as const;
     },
     queryFn: ({ payload, limit, offset }) => {
-      return getAuditLogs({
+      return client.api.getAuditLogs({
         offset,
         limit,
         q: payload,
diff --git a/site/src/api/queries/authCheck.ts b/site/src/api/queries/authCheck.ts
index be9e726ae074d..470685f354789 100644
--- a/site/src/api/queries/authCheck.ts
+++ b/site/src/api/queries/authCheck.ts
@@ -1,4 +1,4 @@
-import * as API from "api/api";
+import { client } from "api/api";
 import type { AuthorizationRequest } from "api/typesGenerated";
 
 export const AUTHORIZATION_KEY = "authorization";
@@ -9,6 +9,6 @@ export const getAuthorizationKey = (req: AuthorizationRequest) =>
 export const checkAuthorization = (req: AuthorizationRequest) => {
   return {
     queryKey: getAuthorizationKey(req),
-    queryFn: () => API.checkAuthorization(req),
+    queryFn: () => client.api.checkAuthorization(req),
   };
 };
diff --git a/site/src/api/queries/buildInfo.ts b/site/src/api/queries/buildInfo.ts
index 504b59bd1d341..b3f43928c6224 100644
--- a/site/src/api/queries/buildInfo.ts
+++ b/site/src/api/queries/buildInfo.ts
@@ -1,5 +1,5 @@
 import type { UseQueryOptions } from "react-query";
-import * as API from "api/api";
+import { client } from "api/api";
 import type { BuildInfoResponse } from "api/typesGenerated";
 import { getMetadataAsJSON } from "utils/metadata";
 import { cachedQuery } from "./util";
@@ -12,6 +12,6 @@ export const buildInfo = (): UseQueryOptions<BuildInfoResponse> => {
   return cachedQuery({
     initialData: initialBuildInfoData,
     queryKey: buildInfoKey,
-    queryFn: () => API.getBuildInfo(),
+    queryFn: () => client.api.getBuildInfo(),
   });
 };
diff --git a/site/src/api/queries/debug.ts b/site/src/api/queries/debug.ts
index 1fba00c172c51..5cd4f40c7b635 100644
--- a/site/src/api/queries/debug.ts
+++ b/site/src/api/queries/debug.ts
@@ -1,5 +1,5 @@
 import type { QueryClient, UseMutationOptions } from "react-query";
-import * as API from "api/api";
+import { client } from "api/api";
 import type { HealthSettings, UpdateHealthSettings } from "api/typesGenerated";
 
 export const HEALTH_QUERY_KEY = ["health"];
@@ -7,14 +7,14 @@ export const HEALTH_QUERY_SETTINGS_KEY = ["health", "settings"];
 
 export const health = () => ({
   queryKey: HEALTH_QUERY_KEY,
-  queryFn: async () => API.getHealth(),
+  queryFn: async () => client.api.getHealth(),
 });
 
 export const refreshHealth = (queryClient: QueryClient) => {
   return {
     mutationFn: async () => {
       await queryClient.cancelQueries(HEALTH_QUERY_KEY);
-      const newHealthData = await API.getHealth(true);
+      const newHealthData = await client.api.getHealth(true);
       queryClient.setQueryData(HEALTH_QUERY_KEY, newHealthData);
     },
   };
@@ -23,7 +23,7 @@ export const refreshHealth = (queryClient: QueryClient) => {
 export const healthSettings = () => {
   return {
     queryKey: HEALTH_QUERY_SETTINGS_KEY,
-    queryFn: API.getHealthSettings,
+    queryFn: client.api.getHealthSettings,
   };
 };
 
@@ -36,7 +36,7 @@ export const updateHealthSettings = (
   unknown
 > => {
   return {
-    mutationFn: API.updateHealthSettings,
+    mutationFn: client.api.updateHealthSettings,
     onSuccess: async (_, newSettings) => {
       await queryClient.invalidateQueries(HEALTH_QUERY_KEY);
       queryClient.setQueryData(HEALTH_QUERY_SETTINGS_KEY, newSettings);
diff --git a/site/src/api/queries/deployment.ts b/site/src/api/queries/deployment.ts
index 540c76ebd79e2..e4ef76c623b34 100644
--- a/site/src/api/queries/deployment.ts
+++ b/site/src/api/queries/deployment.ts
@@ -1,29 +1,29 @@
-import * as API from "api/api";
+import { client } from "api/api";
 
 export const deploymentConfig = () => {
   return {
     queryKey: ["deployment", "config"],
-    queryFn: API.getDeploymentConfig,
+    queryFn: client.api.getDeploymentConfig,
   };
 };
 
 export const deploymentDAUs = () => {
   return {
     queryKey: ["deployment", "daus"],
-    queryFn: () => API.getDeploymentDAUs(),
+    queryFn: () => client.api.getDeploymentDAUs(),
   };
 };
 
 export const deploymentStats = () => {
   return {
     queryKey: ["deployment", "stats"],
-    queryFn: API.getDeploymentStats,
+    queryFn: client.api.getDeploymentStats,
   };
 };
 
 export const deploymentSSHConfig = () => {
   return {
     queryKey: ["deployment", "sshConfig"],
-    queryFn: API.getDeploymentSSHConfig,
+    queryFn: client.api.getDeploymentSSHConfig,
   };
 };
diff --git a/site/src/api/queries/entitlements.ts b/site/src/api/queries/entitlements.ts
index 46b41133bd476..bdb483073c5d3 100644
--- a/site/src/api/queries/entitlements.ts
+++ b/site/src/api/queries/entitlements.ts
@@ -1,5 +1,5 @@
 import type { QueryClient, UseQueryOptions } from "react-query";
-import * as API from "api/api";
+import { client } from "api/api";
 import type { Entitlements } from "api/typesGenerated";
 import { getMetadataAsJSON } from "utils/metadata";
 import { cachedQuery } from "./util";
@@ -11,13 +11,13 @@ export const entitlements = (): UseQueryOptions<Entitlements> => {
   return cachedQuery({
     initialData: initialEntitlementsData,
     queryKey: entitlementsQueryKey,
-    queryFn: () => API.getEntitlements(),
+    queryFn: () => client.api.getEntitlements(),
   });
 };
 
 export const refreshEntitlements = (queryClient: QueryClient) => {
   return {
-    mutationFn: API.refreshEntitlements,
+    mutationFn: client.api.refreshEntitlements,
     onSuccess: async () => {
       await queryClient.invalidateQueries({
         queryKey: entitlementsQueryKey,
diff --git a/site/src/api/queries/experiments.ts b/site/src/api/queries/experiments.ts
index 934e44b863437..38c7ed4142b9f 100644
--- a/site/src/api/queries/experiments.ts
+++ b/site/src/api/queries/experiments.ts
@@ -1,5 +1,5 @@
 import type { UseQueryOptions } from "react-query";
-import * as API from "api/api";
+import { client } from "api/api";
 import type { Experiments } from "api/typesGenerated";
 import { getMetadataAsJSON } from "utils/metadata";
 import { cachedQuery } from "./util";
@@ -11,13 +11,13 @@ export const experiments = (): UseQueryOptions<Experiments> => {
   return cachedQuery({
     initialData: initialExperimentsData,
     queryKey: experimentsKey,
-    queryFn: () => API.getExperiments(),
+    queryFn: () => client.api.getExperiments(),
   });
 };
 
 export const availableExperiments = () => {
   return {
     queryKey: ["availableExperiments"],
-    queryFn: async () => API.getAvailableExperiments(),
+    queryFn: async () => client.api.getAvailableExperiments(),
   };
 };
diff --git a/site/src/api/queries/externalAuth.ts b/site/src/api/queries/externalAuth.ts
index 18cc95a8839ff..d3d905d9844d3 100644
--- a/site/src/api/queries/externalAuth.ts
+++ b/site/src/api/queries/externalAuth.ts
@@ -1,25 +1,25 @@
 import type { QueryClient, UseMutationOptions } from "react-query";
-import * as API from "api/api";
+import { client } from "api/api";
 import type { ExternalAuth } from "api/typesGenerated";
 
 // Returns all configured external auths for a given user.
 export const externalAuths = () => {
   return {
     queryKey: ["external-auth"],
-    queryFn: () => API.getUserExternalAuthProviders(),
+    queryFn: () => client.api.getUserExternalAuthProviders(),
   };
 };
 
 export const externalAuthProvider = (providerId: string) => {
   return {
     queryKey: ["external-auth", providerId],
-    queryFn: () => API.getExternalAuthProvider(providerId),
+    queryFn: () => client.api.getExternalAuthProvider(providerId),
   };
 };
 
 export const externalAuthDevice = (providerId: string) => {
   return {
-    queryFn: () => API.getExternalAuthDevice(providerId),
+    queryFn: () => client.api.getExternalAuthDevice(providerId),
     queryKey: ["external-auth", providerId, "device"],
   };
 };
@@ -31,7 +31,7 @@ export const exchangeExternalAuthDevice = (
 ) => {
   return {
     queryFn: () =>
-      API.exchangeExternalAuthDevice(providerId, {
+      client.api.exchangeExternalAuthDevice(providerId, {
         device_code: deviceCode,
       }),
     queryKey: ["external-auth", providerId, "device", deviceCode],
@@ -46,7 +46,7 @@ export const validateExternalAuth = (
   queryClient: QueryClient,
 ): UseMutationOptions<ExternalAuth, unknown, string> => {
   return {
-    mutationFn: API.getExternalAuthProvider,
+    mutationFn: client.api.getExternalAuthProvider,
     onSuccess: (data, providerId) => {
       queryClient.setQueryData(["external-auth", providerId], data);
     },
@@ -55,7 +55,7 @@ export const validateExternalAuth = (
 
 export const unlinkExternalAuths = (queryClient: QueryClient) => {
   return {
-    mutationFn: API.unlinkExternalAuthProvider,
+    mutationFn: client.api.unlinkExternalAuthProvider,
     onSuccess: async () => {
       await queryClient.invalidateQueries(["external-auth"]);
     },
diff --git a/site/src/api/queries/files.ts b/site/src/api/queries/files.ts
index cc840b52eb63f..c8e30e3b9bd0d 100644
--- a/site/src/api/queries/files.ts
+++ b/site/src/api/queries/files.ts
@@ -1,14 +1,14 @@
-import * as API from "api/api";
+import { client } from "api/api";
 
 export const uploadFile = () => {
   return {
-    mutationFn: API.uploadFile,
+    mutationFn: client.api.uploadFile,
   };
 };
 
 export const file = (fileId: string) => {
   return {
     queryKey: ["files", fileId],
-    queryFn: () => API.getFile(fileId),
+    queryFn: () => client.api.getFile(fileId),
   };
 };
diff --git a/site/src/api/queries/groups.ts b/site/src/api/queries/groups.ts
index 71cba33354b9b..1bd4d1554ca1b 100644
--- a/site/src/api/queries/groups.ts
+++ b/site/src/api/queries/groups.ts
@@ -1,6 +1,5 @@
 import type { QueryClient, UseQueryOptions } from "react-query";
-import * as API from "api/api";
-import { checkAuthorization } from "api/api";
+import { client } from "api/api";
 import type {
   CreateGroupRequest,
   Group,
@@ -15,14 +14,14 @@ const getGroupQueryKey = (groupId: string) => ["group", groupId];
 export const groups = (organizationId: string) => {
   return {
     queryKey: GROUPS_QUERY_KEY,
-    queryFn: () => API.getGroups(organizationId),
+    queryFn: () => client.api.getGroups(organizationId),
   } satisfies UseQueryOptions<Group[]>;
 };
 
 export const group = (groupId: string) => {
   return {
     queryKey: getGroupQueryKey(groupId),
-    queryFn: () => API.getGroup(groupId),
+    queryFn: () => client.api.getGroup(groupId),
   };
 };
 
@@ -72,7 +71,7 @@ export const groupPermissions = (groupId: string) => {
   return {
     queryKey: [...getGroupQueryKey(groupId), "permissions"],
     queryFn: () =>
-      checkAuthorization({
+      client.api.checkAuthorization({
         checks: {
           canUpdateGroup: {
             object: {
@@ -92,7 +91,7 @@ export const createGroup = (queryClient: QueryClient) => {
       organizationId,
       ...request
     }: CreateGroupRequest & { organizationId: string }) =>
-      API.createGroup(organizationId, request),
+      client.api.createGroup(organizationId, request),
     onSuccess: async () => {
       await queryClient.invalidateQueries(GROUPS_QUERY_KEY);
     },
@@ -105,7 +104,7 @@ export const patchGroup = (queryClient: QueryClient) => {
       groupId,
       ...request
     }: PatchGroupRequest & { groupId: string }) =>
-      API.patchGroup(groupId, request),
+      client.api.patchGroup(groupId, request),
     onSuccess: async (updatedGroup: Group) =>
       invalidateGroup(queryClient, updatedGroup.id),
   };
@@ -113,7 +112,7 @@ export const patchGroup = (queryClient: QueryClient) => {
 
 export const deleteGroup = (queryClient: QueryClient) => {
   return {
-    mutationFn: API.deleteGroup,
+    mutationFn: client.api.deleteGroup,
     onSuccess: async (_: void, groupId: string) =>
       invalidateGroup(queryClient, groupId),
   };
@@ -122,7 +121,7 @@ export const deleteGroup = (queryClient: QueryClient) => {
 export const addMember = (queryClient: QueryClient) => {
   return {
     mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) =>
-      API.addMember(groupId, userId),
+      client.api.addMember(groupId, userId),
     onSuccess: async (updatedGroup: Group) =>
       invalidateGroup(queryClient, updatedGroup.id),
   };
@@ -131,7 +130,7 @@ export const addMember = (queryClient: QueryClient) => {
 export const removeMember = (queryClient: QueryClient) => {
   return {
     mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) =>
-      API.removeMember(groupId, userId),
+      client.api.removeMember(groupId, userId),
     onSuccess: async (updatedGroup: Group) =>
       invalidateGroup(queryClient, updatedGroup.id),
   };
diff --git a/site/src/api/queries/insights.ts b/site/src/api/queries/insights.ts
index 7d60565e83bb0..7e2f5bd5b9c0a 100644
--- a/site/src/api/queries/insights.ts
+++ b/site/src/api/queries/insights.ts
@@ -1,22 +1,26 @@
-import * as API from "api/api";
+import {
+  type InsightsParams,
+  type InsightsTemplateParams,
+  client,
+} from "api/api";
 
-export const insightsTemplate = (params: API.InsightsTemplateParams) => {
+export const insightsTemplate = (params: InsightsTemplateParams) => {
   return {
     queryKey: ["insights", "templates", params.template_ids, params],
-    queryFn: () => API.getInsightsTemplate(params),
+    queryFn: () => client.api.getInsightsTemplate(params),
   };
 };
 
-export const insightsUserLatency = (params: API.InsightsParams) => {
+export const insightsUserLatency = (params: InsightsParams) => {
   return {
     queryKey: ["insights", "userLatency", params.template_ids, params],
-    queryFn: () => API.getInsightsUserLatency(params),
+    queryFn: () => client.api.getInsightsUserLatency(params),
   };
 };
 
-export const insightsUserActivity = (params: API.InsightsParams) => {
+export const insightsUserActivity = (params: InsightsParams) => {
   return {
     queryKey: ["insights", "userActivity", params.template_ids, params],
-    queryFn: () => API.getInsightsUserActivity(params),
+    queryFn: () => client.api.getInsightsUserActivity(params),
   };
 };
diff --git a/site/src/api/queries/integrations.ts b/site/src/api/queries/integrations.ts
index de43a4c8f4cac..0e26194b80b22 100644
--- a/site/src/api/queries/integrations.ts
+++ b/site/src/api/queries/integrations.ts
@@ -1,9 +1,9 @@
 import type { GetJFrogXRayScanParams } from "api/api";
-import * as API from "api/api";
+import { client } from "api/api";
 
 export const xrayScan = (params: GetJFrogXRayScanParams) => {
   return {
     queryKey: ["xray", params],
-    queryFn: () => API.getJFrogXRayScan(params),
+    queryFn: () => client.api.getJFrogXRayScan(params),
   };
 };
diff --git a/site/src/api/queries/oauth2.ts b/site/src/api/queries/oauth2.ts
index 78b31762b2aa5..06f8f9d9b4097 100644
--- a/site/src/api/queries/oauth2.ts
+++ b/site/src/api/queries/oauth2.ts
@@ -1,5 +1,5 @@
 import type { QueryClient } from "react-query";
-import * as API from "api/api";
+import { client } from "api/api";
 import type * as TypesGen from "api/typesGenerated";
 
 const appsKey = ["oauth2-provider", "apps"];
@@ -10,20 +10,20 @@ const appSecretsKey = (appId: string) => appKey(appId).concat("secrets");
 export const getApps = (userId?: string) => {
   return {
     queryKey: userId ? appsKey.concat(userId) : appsKey,
-    queryFn: () => API.getOAuth2ProviderApps({ user_id: userId }),
+    queryFn: () => client.api.getOAuth2ProviderApps({ user_id: userId }),
   };
 };
 
 export const getApp = (id: string) => {
   return {
     queryKey: appKey(id),
-    queryFn: () => API.getOAuth2ProviderApp(id),
+    queryFn: () => client.api.getOAuth2ProviderApp(id),
   };
 };
 
 export const postApp = (queryClient: QueryClient) => {
   return {
-    mutationFn: API.postOAuth2ProviderApp,
+    mutationFn: client.api.postOAuth2ProviderApp,
     onSuccess: async () => {
       await queryClient.invalidateQueries({
         queryKey: appsKey,
@@ -40,7 +40,7 @@ export const putApp = (queryClient: QueryClient) => {
     }: {
       id: string;
       req: TypesGen.PutOAuth2ProviderAppRequest;
-    }) => API.putOAuth2ProviderApp(id, req),
+    }) => client.api.putOAuth2ProviderApp(id, req),
     onSuccess: async (app: TypesGen.OAuth2ProviderApp) => {
       await queryClient.invalidateQueries({
         queryKey: appKey(app.id),
@@ -51,7 +51,7 @@ export const putApp = (queryClient: QueryClient) => {
 
 export const deleteApp = (queryClient: QueryClient) => {
   return {
-    mutationFn: API.deleteOAuth2ProviderApp,
+    mutationFn: client.api.deleteOAuth2ProviderApp,
     onSuccess: async () => {
       await queryClient.invalidateQueries({
         queryKey: appsKey,
@@ -63,13 +63,13 @@ export const deleteApp = (queryClient: QueryClient) => {
 export const getAppSecrets = (id: string) => {
   return {
     queryKey: appSecretsKey(id),
-    queryFn: () => API.getOAuth2ProviderAppSecrets(id),
+    queryFn: () => client.api.getOAuth2ProviderAppSecrets(id),
   };
 };
 
 export const postAppSecret = (queryClient: QueryClient) => {
   return {
-    mutationFn: API.postOAuth2ProviderAppSecret,
+    mutationFn: client.api.postOAuth2ProviderAppSecret,
     onSuccess: async (
       _: TypesGen.OAuth2ProviderAppSecretFull,
       appId: string,
@@ -84,7 +84,7 @@ export const postAppSecret = (queryClient: QueryClient) => {
 export const deleteAppSecret = (queryClient: QueryClient) => {
   return {
     mutationFn: ({ appId, secretId }: { appId: string; secretId: string }) =>
-      API.deleteOAuth2ProviderAppSecret(appId, secretId),
+      client.api.deleteOAuth2ProviderAppSecret(appId, secretId),
     onSuccess: async (_: void, { appId }: { appId: string }) => {
       await queryClient.invalidateQueries({
         queryKey: appSecretsKey(appId),
@@ -95,7 +95,7 @@ export const deleteAppSecret = (queryClient: QueryClient) => {
 
 export const revokeApp = (queryClient: QueryClient, userId: string) => {
   return {
-    mutationFn: API.revokeOAuth2ProviderApp,
+    mutationFn: client.api.revokeOAuth2ProviderApp,
     onSuccess: async () => {
       await queryClient.invalidateQueries({
         queryKey: userAppsKey(userId),
diff --git a/site/src/api/queries/roles.ts b/site/src/api/queries/roles.ts
index 37b2af49f3e74..7bfe89162316b 100644
--- a/site/src/api/queries/roles.ts
+++ b/site/src/api/queries/roles.ts
@@ -1,8 +1,8 @@
-import * as API from "api/api";
+import { client } from "api/api";
 
 export const roles = () => {
   return {
     queryKey: ["roles"],
-    queryFn: API.getRoles,
+    queryFn: client.api.getRoles,
   };
 };
diff --git a/site/src/api/queries/settings.ts b/site/src/api/queries/settings.ts
index 4a086cf18532c..bd4ef7f944dce 100644
--- a/site/src/api/queries/settings.ts
+++ b/site/src/api/queries/settings.ts
@@ -1,5 +1,5 @@
 import type { QueryClient, QueryOptions } from "react-query";
-import * as API from "api/api";
+import { client } from "api/api";
 import type {
   UpdateUserQuietHoursScheduleRequest,
   UserQuietHoursScheduleResponse,
@@ -16,7 +16,7 @@ export const userQuietHoursSchedule = (
 ): QueryOptions<UserQuietHoursScheduleResponse> => {
   return {
     queryKey: userQuietHoursScheduleKey(userId),
-    queryFn: () => API.getUserQuietHoursSchedule(userId),
+    queryFn: () => client.api.getUserQuietHoursSchedule(userId),
   };
 };
 
@@ -26,7 +26,7 @@ export const updateUserQuietHoursSchedule = (
 ) => {
   return {
     mutationFn: (request: UpdateUserQuietHoursScheduleRequest) =>
-      API.updateUserQuietHoursSchedule(userId, request),
+      client.api.updateUserQuietHoursSchedule(userId, request),
     onSuccess: async () => {
       await queryClient.invalidateQueries(userQuietHoursScheduleKey(userId));
     },
diff --git a/site/src/api/queries/sshKeys.ts b/site/src/api/queries/sshKeys.ts
index 6fc3593c318c7..878a80523863e 100644
--- a/site/src/api/queries/sshKeys.ts
+++ b/site/src/api/queries/sshKeys.ts
@@ -1,5 +1,5 @@
 import type { QueryClient } from "react-query";
-import * as API from "api/api";
+import { client } from "api/api";
 import type { GitSSHKey } from "api/typesGenerated";
 
 const getUserSSHKeyQueryKey = (userId: string) => [userId, "sshKey"];
@@ -7,7 +7,7 @@ const getUserSSHKeyQueryKey = (userId: string) => [userId, "sshKey"];
 export const userSSHKey = (userId: string) => {
   return {
     queryKey: getUserSSHKeyQueryKey(userId),
-    queryFn: () => API.getUserSSHKey(userId),
+    queryFn: () => client.api.getUserSSHKey(userId),
   };
 };
 
@@ -16,7 +16,7 @@ export const regenerateUserSSHKey = (
   queryClient: QueryClient,
 ) => {
   return {
-    mutationFn: () => API.regenerateUserSSHKey(userId),
+    mutationFn: () => client.api.regenerateUserSSHKey(userId),
     onSuccess: (newKey: GitSSHKey) => {
       queryClient.setQueryData(getUserSSHKeyQueryKey(userId), newKey);
     },
diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts
index 83879415bacf6..7e485a0e039b4 100644
--- a/site/src/api/queries/templates.ts
+++ b/site/src/api/queries/templates.ts
@@ -1,5 +1,5 @@
 import type { MutationOptions, QueryClient, QueryOptions } from "react-query";
-import * as API from "api/api";
+import { client } from "api/api";
 import type {
   CreateTemplateRequest,
   CreateTemplateVersionRequest,
@@ -26,7 +26,7 @@ export const templateByName = (
 ): QueryOptions<Template> => {
   return {
     queryKey: templateByNameKey(organizationId, name),
-    queryFn: async () => API.getTemplateByName(organizationId, name),
+    queryFn: async () => client.api.getTemplateByName(organizationId, name),
   };
 };
 
@@ -39,27 +39,27 @@ const getTemplatesQueryKey = (organizationId: string, deprecated?: boolean) => [
 export const templates = (organizationId: string, deprecated?: boolean) => {
   return {
     queryKey: getTemplatesQueryKey(organizationId, deprecated),
-    queryFn: () => API.getTemplates(organizationId, { deprecated }),
+    queryFn: () => client.api.getTemplates(organizationId, { deprecated }),
   };
 };
 
 export const templateACL = (templateId: string) => {
   return {
     queryKey: ["templateAcl", templateId],
-    queryFn: () => API.getTemplateACL(templateId),
+    queryFn: () => client.api.getTemplateACL(templateId),
   };
 };
 
 export const setUserRole = (
   queryClient: QueryClient,
 ): MutationOptions<
-  Awaited<ReturnType<typeof API.updateTemplateACL>>,
+  Awaited<ReturnType<typeof client.api.updateTemplateACL>>,
   unknown,
   { templateId: string; userId: string; role: TemplateRole }
 > => {
   return {
     mutationFn: ({ templateId, userId, role }) =>
-      API.updateTemplateACL(templateId, {
+      client.api.updateTemplateACL(templateId, {
         user_perms: {
           [userId]: role,
         },
@@ -73,13 +73,13 @@ export const setUserRole = (
 export const setGroupRole = (
   queryClient: QueryClient,
 ): MutationOptions<
-  Awaited<ReturnType<typeof API.updateTemplateACL>>,
+  Awaited<ReturnType<typeof client.api.updateTemplateACL>>,
   unknown,
   { templateId: string; groupId: string; role: TemplateRole }
 > => {
   return {
     mutationFn: ({ templateId, groupId, role }) =>
-      API.updateTemplateACL(templateId, {
+      client.api.updateTemplateACL(templateId, {
         group_perms: {
           [groupId]: role,
         },
@@ -93,14 +93,14 @@ export const setGroupRole = (
 export const templateExamples = (organizationId: string) => {
   return {
     queryKey: [...getTemplatesQueryKey(organizationId), "examples"],
-    queryFn: () => API.getTemplateExamples(organizationId),
+    queryFn: () => client.api.getTemplateExamples(organizationId),
   };
 };
 
 export const templateVersion = (versionId: string) => {
   return {
     queryKey: ["templateVersion", versionId],
-    queryFn: () => API.getTemplateVersion(versionId),
+    queryFn: () => client.api.getTemplateVersion(versionId),
   };
 };
 
@@ -112,14 +112,18 @@ export const templateVersionByName = (
   return {
     queryKey: ["templateVersion", organizationId, templateName, versionName],
     queryFn: () =>
-      API.getTemplateVersionByName(organizationId, templateName, versionName),
+      client.api.getTemplateVersionByName(
+        organizationId,
+        templateName,
+        versionName,
+      ),
   };
 };
 
 export const templateVersions = (templateId: string) => {
   return {
     queryKey: ["templateVersions", templateId],
-    queryFn: () => API.getTemplateVersions(templateId),
+    queryFn: () => client.api.getTemplateVersions(templateId),
   };
 };
 
@@ -132,14 +136,14 @@ export const templateVersionVariablesKey = (versionId: string) => [
 export const templateVersionVariables = (versionId: string) => {
   return {
     queryKey: templateVersionVariablesKey(versionId),
-    queryFn: () => API.getTemplateVersionVariables(versionId),
+    queryFn: () => client.api.getTemplateVersionVariables(versionId),
   };
 };
 
 export const createTemplateVersion = (organizationId: string) => {
   return {
     mutationFn: async (request: CreateTemplateVersionRequest) => {
-      const newVersion = await API.createTemplateVersion(
+      const newVersion = await client.api.createTemplateVersion(
         organizationId,
         request,
       );
@@ -151,7 +155,7 @@ export const createTemplateVersion = (organizationId: string) => {
 export const createAndBuildTemplateVersion = (organizationId: string) => {
   return {
     mutationFn: async (request: CreateTemplateVersionRequest) => {
-      const newVersion = await API.createTemplateVersion(
+      const newVersion = await client.api.createTemplateVersion(
         organizationId,
         request,
       );
@@ -167,7 +171,7 @@ export const updateActiveTemplateVersion = (
 ) => {
   return {
     mutationFn: (versionId: string) =>
-      API.updateActiveTemplateVersion(template.id, {
+      client.api.updateActiveTemplateVersion(template.id, {
         id: versionId,
       }),
     onSuccess: async () => {
@@ -185,7 +189,7 @@ export const templaceACLAvailable = (
 ) => {
   return {
     queryKey: ["template", templateId, "aclAvailable", options],
-    queryFn: () => API.getTemplateACLAvailable(templateId, options),
+    queryFn: () => client.api.getTemplateACLAvailable(templateId, options),
   };
 };
 
@@ -198,7 +202,7 @@ export const templateVersionExternalAuthKey = (versionId: string) => [
 export const templateVersionExternalAuth = (versionId: string) => {
   return {
     queryKey: templateVersionExternalAuthKey(versionId),
-    queryFn: () => API.getTemplateVersionExternalAuth(versionId),
+    queryFn: () => client.api.getTemplateVersionExternalAuth(versionId),
   };
 };
 
@@ -217,13 +221,13 @@ export type CreateTemplateOptions = {
 };
 
 const createTemplateFn = async (options: CreateTemplateOptions) => {
-  const version = await API.createTemplateVersion(
+  const version = await client.api.createTemplateVersion(
     options.organizationId,
     options.version,
   );
   options.onCreateVersion?.(version);
   await waitBuildToBeFinished(version, options.onTemplateVersionChanges);
-  return API.createTemplate(options.organizationId, {
+  return client.api.createTemplate(options.organizationId, {
     ...options.template,
     template_version_id: version.id,
   });
@@ -232,21 +236,21 @@ const createTemplateFn = async (options: CreateTemplateOptions) => {
 export const templateVersionLogs = (versionId: string) => {
   return {
     queryKey: ["templateVersion", versionId, "logs"],
-    queryFn: () => API.getTemplateVersionLogs(versionId),
+    queryFn: () => client.api.getTemplateVersionLogs(versionId),
   };
 };
 
 export const richParameters = (versionId: string) => {
   return {
     queryKey: ["templateVersion", versionId, "richParameters"],
-    queryFn: () => API.getTemplateVersionRichParameters(versionId),
+    queryFn: () => client.api.getTemplateVersionRichParameters(versionId),
   };
 };
 
 export const resources = (versionId: string) => {
   return {
     queryKey: ["templateVersion", versionId, "resources"],
-    queryFn: () => API.getTemplateVersionResources(versionId),
+    queryFn: () => client.api.getTemplateVersionResources(versionId),
   };
 };
 
@@ -254,7 +258,7 @@ export const templateFiles = (fileId: string) => {
   return {
     queryKey: ["templateFiles", fileId],
     queryFn: async () => {
-      const tarFile = await API.getFile(fileId);
+      const tarFile = await client.api.getFile(fileId);
       return getTemplateVersionFiles(tarFile);
     },
   };
@@ -274,7 +278,7 @@ export const previousTemplateVersion = (
       "previous",
     ],
     queryFn: async () => {
-      const result = await API.getPreviousTemplateVersionByName(
+      const result = await client.api.getPreviousTemplateVersionByName(
         organizationId,
         templateName,
         versionName,
@@ -294,7 +298,7 @@ const waitBuildToBeFinished = async (
   do {
     // When pending we want to poll more frequently
     await delay(jobStatus === "pending" ? 250 : 1000);
-    data = await API.getTemplateVersion(version.id);
+    data = await client.api.getTemplateVersion(version.id);
     onRequest?.(data);
     jobStatus = data.job.status;
 
diff --git a/site/src/api/queries/updateCheck.ts b/site/src/api/queries/updateCheck.ts
index 40fcc6a3cfdde..6c7a2add42ef4 100644
--- a/site/src/api/queries/updateCheck.ts
+++ b/site/src/api/queries/updateCheck.ts
@@ -1,8 +1,8 @@
-import * as API from "api/api";
+import { client } from "api/api";
 
 export const updateCheck = () => {
   return {
     queryKey: ["updateCheck"],
-    queryFn: () => API.getUpdateCheck(),
+    queryFn: () => client.api.getUpdateCheck(),
   };
 };
diff --git a/site/src/api/queries/users.ts b/site/src/api/queries/users.ts
index 3a1806276146b..bee3cae979aeb 100644
--- a/site/src/api/queries/users.ts
+++ b/site/src/api/queries/users.ts
@@ -4,7 +4,7 @@ import type {
   UseMutationOptions,
   UseQueryOptions,
 } from "react-query";
-import * as API from "api/api";
+import { client } from "api/api";
 import type {
   AuthorizationRequest,
   GetUsersResponse,
@@ -39,14 +39,14 @@ export function paginatedUsers(
     },
 
     queryKey: ({ payload }) => usersKey(payload),
-    queryFn: ({ payload, signal }) => API.getUsers(payload, signal),
+    queryFn: ({ payload, signal }) => client.api.getUsers(payload, signal),
   };
 }
 
 export const users = (req: UsersRequest): UseQueryOptions<GetUsersResponse> => {
   return {
     queryKey: usersKey(req),
-    queryFn: ({ signal }) => API.getUsers(req, signal),
+    queryFn: ({ signal }) => client.api.getUsers(req, signal),
     cacheTime: 5 * 1000 * 60,
   };
 };
@@ -57,13 +57,13 @@ export const updatePassword = () => {
       userId,
       ...request
     }: UpdateUserPasswordRequest & { userId: string }) =>
-      API.updateUserPassword(userId, request),
+      client.api.updateUserPassword(userId, request),
   };
 };
 
 export const createUser = (queryClient: QueryClient) => {
   return {
-    mutationFn: API.createUser,
+    mutationFn: client.api.createUser,
     onSuccess: async () => {
       await queryClient.invalidateQueries(["users"]);
     },
@@ -72,13 +72,13 @@ export const createUser = (queryClient: QueryClient) => {
 
 export const createFirstUser = () => {
   return {
-    mutationFn: API.createFirstUser,
+    mutationFn: client.api.createFirstUser,
   };
 };
 
 export const suspendUser = (queryClient: QueryClient) => {
   return {
-    mutationFn: API.suspendUser,
+    mutationFn: client.api.suspendUser,
     onSuccess: async () => {
       await queryClient.invalidateQueries(["users"]);
     },
@@ -87,7 +87,7 @@ export const suspendUser = (queryClient: QueryClient) => {
 
 export const activateUser = (queryClient: QueryClient) => {
   return {
-    mutationFn: API.activateUser,
+    mutationFn: client.api.activateUser,
     onSuccess: async () => {
       await queryClient.invalidateQueries(["users"]);
     },
@@ -96,7 +96,7 @@ export const activateUser = (queryClient: QueryClient) => {
 
 export const deleteUser = (queryClient: QueryClient) => {
   return {
-    mutationFn: API.deleteUser,
+    mutationFn: client.api.deleteUser,
     onSuccess: async () => {
       await queryClient.invalidateQueries(["users"]);
     },
@@ -106,7 +106,7 @@ export const deleteUser = (queryClient: QueryClient) => {
 export const updateRoles = (queryClient: QueryClient) => {
   return {
     mutationFn: ({ userId, roles }: { userId: string; roles: string[] }) =>
-      API.updateUserRoles(roles, userId),
+      client.api.updateUserRoles(roles, userId),
     onSuccess: async () => {
       await queryClient.invalidateQueries(["users"]);
     },
@@ -120,7 +120,7 @@ export const authMethods = () => {
     // Even the endpoint being /users/authmethods we don't want to revalidate it
     // when users change so its better add a unique query key
     queryKey: ["authMethods"],
-    queryFn: API.getAuthMethods,
+    queryFn: client.api.getAuthMethods,
   };
 };
 
@@ -132,14 +132,14 @@ export const me = (): UseQueryOptions<User> & {
   return cachedQuery({
     initialData: initialUserData,
     queryKey: meKey,
-    queryFn: API.getAuthenticatedUser,
+    queryFn: client.api.getAuthenticatedUser,
   });
 };
 
 export function apiKey(): UseQueryOptions<GenerateAPIKeyResponse> {
   return {
     queryKey: [...meKey, "apiKey"],
-    queryFn: () => API.getApiKey(),
+    queryFn: () => client.api.getApiKey(),
   };
 }
 
@@ -148,7 +148,7 @@ export const hasFirstUser = (): UseQueryOptions<boolean> => {
     // This cannot be false otherwise it will not fetch!
     initialData: Boolean(initialUserData) || undefined,
     queryKey: ["hasFirstUser"],
-    queryFn: API.hasFirstUser,
+    queryFn: client.api.hasFirstUser,
   });
 };
 
@@ -178,10 +178,10 @@ const loginFn = async ({
   password: string;
   authorization: AuthorizationRequest;
 }) => {
-  await API.login(email, password);
+  await client.api.login(email, password);
   const [user, permissions] = await Promise.all([
-    API.getAuthenticatedUser(),
-    API.checkAuthorization(authorization),
+    client.api.getAuthenticatedUser(),
+    client.api.checkAuthorization(authorization),
   ]);
   return {
     user,
@@ -191,7 +191,7 @@ const loginFn = async ({
 
 export const logout = (queryClient: QueryClient) => {
   return {
-    mutationFn: API.logout,
+    mutationFn: client.api.logout,
     onSuccess: () => {
       queryClient.removeQueries();
     },
@@ -201,7 +201,7 @@ export const logout = (queryClient: QueryClient) => {
 export const updateProfile = (userId: string) => {
   return {
     mutationFn: (req: UpdateUserProfileRequest) =>
-      API.updateProfile(userId, req),
+      client.api.updateProfile(userId, req),
   };
 };
 
@@ -215,7 +215,7 @@ export const updateAppearanceSettings = (
   unknown
 > => {
   return {
-    mutationFn: (req) => API.updateAppearanceSettings(userId, req),
+    mutationFn: (req) => client.api.updateAppearanceSettings(userId, req),
     onMutate: async (patch) => {
       // Mutate the `queryClient` optimistically to make the theme switcher
       // more responsive.
diff --git a/site/src/api/queries/workspaceBuilds.ts b/site/src/api/queries/workspaceBuilds.ts
index 8960068b6169c..d4cc127c6814e 100644
--- a/site/src/api/queries/workspaceBuilds.ts
+++ b/site/src/api/queries/workspaceBuilds.ts
@@ -1,5 +1,5 @@
 import type { QueryOptions, UseInfiniteQueryOptions } from "react-query";
-import * as API from "api/api";
+import { client } from "api/api";
 import type {
   WorkspaceBuild,
   WorkspaceBuildParameter,
@@ -13,7 +13,7 @@ export function workspaceBuildParametersKey(workspaceBuildId: string) {
 export function workspaceBuildParameters(workspaceBuildId: string) {
   return {
     queryKey: workspaceBuildParametersKey(workspaceBuildId),
-    queryFn: () => API.getWorkspaceBuildParameters(workspaceBuildId),
+    queryFn: () => client.api.getWorkspaceBuildParameters(workspaceBuildId),
   } as const satisfies QueryOptions<WorkspaceBuildParameter[]>;
 }
 
@@ -25,7 +25,11 @@ export const workspaceBuildByNumber = (
   return {
     queryKey: ["workspaceBuild", username, workspaceName, buildNumber],
     queryFn: () =>
-      API.getWorkspaceBuildByNumber(username, workspaceName, buildNumber),
+      client.api.getWorkspaceBuildByNumber(
+        username,
+        workspaceName,
+        buildNumber,
+      ),
   };
 };
 
@@ -49,7 +53,7 @@ export const infiniteWorkspaceBuilds = (
       return pages.length + 1;
     },
     queryFn: ({ pageParam = 0 }) => {
-      return API.getWorkspaceBuilds(workspaceId, {
+      return client.api.getWorkspaceBuilds(workspaceId, {
         limit,
         offset: pageParam <= 0 ? 0 : (pageParam - 1) * limit,
       });
diff --git a/site/src/api/queries/workspaceQuota.ts b/site/src/api/queries/workspaceQuota.ts
index f43adf616688e..ec27415b7ce2e 100644
--- a/site/src/api/queries/workspaceQuota.ts
+++ b/site/src/api/queries/workspaceQuota.ts
@@ -1,4 +1,4 @@
-import * as API from "api/api";
+import { client } from "api/api";
 
 export const getWorkspaceQuotaQueryKey = (username: string) => [
   username,
@@ -8,7 +8,7 @@ export const getWorkspaceQuotaQueryKey = (username: string) => [
 export const workspaceQuota = (username: string) => {
   return {
     queryKey: getWorkspaceQuotaQueryKey(username),
-    queryFn: () => API.getWorkspaceQuota(username),
+    queryFn: () => client.api.getWorkspaceQuota(username),
   };
 };
 
@@ -20,6 +20,6 @@ export const getWorkspaceResolveAutostartQueryKey = (workspaceId: string) => [
 export const workspaceResolveAutostart = (workspaceId: string) => {
   return {
     queryKey: getWorkspaceResolveAutostartQueryKey(workspaceId),
-    queryFn: () => API.getWorkspaceResolveAutostart(workspaceId),
+    queryFn: () => client.api.getWorkspaceResolveAutostart(workspaceId),
   };
 };
diff --git a/site/src/api/queries/workspaceportsharing.ts b/site/src/api/queries/workspaceportsharing.ts
index 9e341d551a4f3..fd37af90fa926 100644
--- a/site/src/api/queries/workspaceportsharing.ts
+++ b/site/src/api/queries/workspaceportsharing.ts
@@ -1,8 +1,4 @@
-import {
-  deleteWorkspaceAgentSharedPort,
-  getWorkspaceAgentSharedPorts,
-  upsertWorkspaceAgentSharedPort,
-} from "api/api";
+import { client } from "api/api";
 import type {
   DeleteWorkspaceAgentPortShareRequest,
   UpsertWorkspaceAgentPortShareRequest,
@@ -11,14 +7,14 @@ import type {
 export const workspacePortShares = (workspaceId: string) => {
   return {
     queryKey: ["sharedPorts", workspaceId],
-    queryFn: () => getWorkspaceAgentSharedPorts(workspaceId),
+    queryFn: () => client.api.getWorkspaceAgentSharedPorts(workspaceId),
   };
 };
 
 export const upsertWorkspacePortShare = (workspaceId: string) => {
   return {
     mutationFn: async (options: UpsertWorkspaceAgentPortShareRequest) => {
-      await upsertWorkspaceAgentSharedPort(workspaceId, options);
+      await client.api.upsertWorkspaceAgentSharedPort(workspaceId, options);
     },
   };
 };
@@ -26,7 +22,7 @@ export const upsertWorkspacePortShare = (workspaceId: string) => {
 export const deleteWorkspacePortShare = (workspaceId: string) => {
   return {
     mutationFn: async (options: DeleteWorkspaceAgentPortShareRequest) => {
-      await deleteWorkspaceAgentSharedPort(workspaceId, options);
+      await client.api.deleteWorkspaceAgentSharedPort(workspaceId, options);
     },
   };
 };
diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts
index 816cc5613e99d..da68598cc6358 100644
--- a/site/src/api/queries/workspaces.ts
+++ b/site/src/api/queries/workspaces.ts
@@ -4,8 +4,7 @@ import type {
   QueryOptions,
   UseMutationOptions,
 } from "react-query";
-import * as API from "api/api";
-import { putWorkspaceExtension } from "api/api";
+import { type DeleteWorkspaceOptions, client } from "api/api";
 import type {
   CreateWorkspaceRequest,
   ProvisionerLogLevel,
@@ -28,7 +27,9 @@ export const workspaceByOwnerAndName = (owner: string, name: string) => {
   return {
     queryKey: workspaceByOwnerAndNameKey(owner, name),
     queryFn: () =>
-      API.getWorkspaceByOwnerAndName(owner, name, { include_deleted: true }),
+      client.api.getWorkspaceByOwnerAndName(owner, name, {
+        include_deleted: true,
+      }),
   };
 };
 
@@ -49,7 +50,7 @@ export const createWorkspace = (queryClient: QueryClient) => {
   return {
     mutationFn: async (variables: CreateWorkspaceMutationVariables) => {
       const { userId, organizationId, ...req } = variables;
-      return API.createWorkspace(organizationId, userId, req);
+      return client.api.createWorkspace(organizationId, userId, req);
     },
     onSuccess: async () => {
       await queryClient.invalidateQueries(["workspaces"]);
@@ -71,14 +72,14 @@ export const autoCreateWorkspace = (queryClient: QueryClient) => {
       if (versionId) {
         templateVersionParameters = { template_version_id: versionId };
       } else {
-        const template = await API.getTemplateByName(
+        const template = await client.api.getTemplateByName(
           organizationId,
           templateName,
         );
         templateVersionParameters = { template_id: template.id };
       }
 
-      return API.createWorkspace(organizationId, "me", {
+      return client.api.createWorkspace(organizationId, "me", {
         ...templateVersionParameters,
         name: defaultName,
         rich_parameter_values: defaultBuildParameters,
@@ -102,7 +103,7 @@ export function workspaces(config: WorkspacesRequest = {}) {
 
   return {
     queryKey: workspacesKey(config),
-    queryFn: () => API.getWorkspaces({ q, limit }),
+    queryFn: () => client.api.getWorkspaces({ q, limit }),
   } as const satisfies QueryOptions<WorkspacesResponse>;
 }
 
@@ -111,7 +112,7 @@ export const updateDeadline = (
 ): UseMutationOptions<void, unknown, Dayjs> => {
   return {
     mutationFn: (deadline: Dayjs) => {
-      return putWorkspaceExtension(workspace.id, deadline);
+      return client.api.putWorkspaceExtension(workspace.id, deadline);
     },
   };
 };
@@ -128,7 +129,11 @@ export const changeVersion = (
       versionId: string;
       buildParameters?: WorkspaceBuildParameter[];
     }) => {
-      return API.changeWorkspaceVersion(workspace, versionId, buildParameters);
+      return client.api.changeWorkspaceVersion(
+        workspace,
+        versionId,
+        buildParameters,
+      );
     },
     onSuccess: async (build: WorkspaceBuild) => {
       await updateWorkspaceBuild(build, queryClient);
@@ -142,7 +147,7 @@ export const updateWorkspace = (
 ) => {
   return {
     mutationFn: (buildParameters?: WorkspaceBuildParameter[]) => {
-      return API.updateWorkspace(workspace, buildParameters);
+      return client.api.updateWorkspace(workspace, buildParameters);
     },
     onSuccess: async (build: WorkspaceBuild) => {
       await updateWorkspaceBuild(build, queryClient);
@@ -155,8 +160,8 @@ export const deleteWorkspace = (
   queryClient: QueryClient,
 ) => {
   return {
-    mutationFn: (options: API.DeleteWorkspaceOptions) => {
-      return API.deleteWorkspace(workspace.id, options);
+    mutationFn: (options: DeleteWorkspaceOptions) => {
+      return client.api.deleteWorkspace(workspace.id, options);
     },
     onSuccess: async (build: WorkspaceBuild) => {
       await updateWorkspaceBuild(build, queryClient);
@@ -170,7 +175,7 @@ export const stopWorkspace = (
 ) => {
   return {
     mutationFn: ({ logLevel }: { logLevel?: ProvisionerLogLevel }) => {
-      return API.stopWorkspace(workspace.id, logLevel);
+      return client.api.stopWorkspace(workspace.id, logLevel);
     },
     onSuccess: async (build: WorkspaceBuild) => {
       await updateWorkspaceBuild(build, queryClient);
@@ -190,7 +195,7 @@ export const startWorkspace = (
       buildParameters?: WorkspaceBuildParameter[];
       logLevel?: ProvisionerLogLevel;
     }) => {
-      return API.startWorkspace(
+      return client.api.startWorkspace(
         workspace.id,
         workspace.latest_build.template_version_id,
         logLevel,
@@ -206,7 +211,7 @@ export const startWorkspace = (
 export const cancelBuild = (workspace: Workspace, queryClient: QueryClient) => {
   return {
     mutationFn: () => {
-      return API.cancelWorkspaceBuild(workspace.latest_build.id);
+      return client.api.cancelWorkspaceBuild(workspace.latest_build.id);
     },
     onSuccess: async () => {
       await queryClient.invalidateQueries({
@@ -219,7 +224,7 @@ export const cancelBuild = (workspace: Workspace, queryClient: QueryClient) => {
 export const activate = (workspace: Workspace, queryClient: QueryClient) => {
   return {
     mutationFn: () => {
-      return API.updateWorkspaceDormancy(workspace.id, false);
+      return client.api.updateWorkspaceDormancy(workspace.id, false);
     },
     onSuccess: (updatedWorkspace: Workspace) => {
       queryClient.setQueryData(
@@ -263,9 +268,9 @@ export const toggleFavorite = (
   return {
     mutationFn: () => {
       if (workspace.favorite) {
-        return API.deleteFavoriteWorkspace(workspace.id);
+        return client.api.deleteFavoriteWorkspace(workspace.id);
       } else {
-        return API.putFavoriteWorkspace(workspace.id);
+        return client.api.putFavoriteWorkspace(workspace.id);
       }
     },
     onSuccess: async () => {
diff --git a/site/src/components/Filter/UserFilter.tsx b/site/src/components/Filter/UserFilter.tsx
index 74bbf91376a12..fe44940880380 100644
--- a/site/src/components/Filter/UserFilter.tsx
+++ b/site/src/components/Filter/UserFilter.tsx
@@ -1,5 +1,5 @@
 import type { FC } from "react";
-import { getUsers } from "api/api";
+import { client } from "api/api";
 import { useAuthenticated } from "contexts/auth/RequireAuth";
 import { UserAvatar } from "../UserAvatar/UserAvatar";
 import { FilterSearchMenu, OptionItem } from "./filter";
@@ -42,7 +42,7 @@ export const useUserFilterMenu = ({
         };
       }
 
-      const usersRes = await getUsers({ q: value, limit: 1 });
+      const usersRes = await client.api.getUsers({ q: value, limit: 1 });
       const firstUser = usersRes.users.at(0);
       if (firstUser && firstUser.username === value) {
         return {
@@ -54,7 +54,7 @@ export const useUserFilterMenu = ({
       return null;
     },
     getOptions: async (query) => {
-      const usersRes = await getUsers({ q: query, limit: 25 });
+      const usersRes = await client.api.getUsers({ q: query, limit: 25 });
       let options: UserOption[] = usersRes.users.map((user) => ({
         label: user.username,
         value: user.username,
diff --git a/site/src/contexts/ProxyContext.tsx b/site/src/contexts/ProxyContext.tsx
index 07e1486dd12e6..fb1d3256e9fd2 100644
--- a/site/src/contexts/ProxyContext.tsx
+++ b/site/src/contexts/ProxyContext.tsx
@@ -8,7 +8,7 @@ import {
   useState,
 } from "react";
 import { useQuery } from "react-query";
-import { getWorkspaceProxies, getWorkspaceProxyRegions } from "api/api";
+import { client } from "api/api";
 import { cachedQuery } from "api/queries/util";
 import type { Region, WorkspaceProxy } from "api/typesGenerated";
 import { useAuthenticated } from "contexts/auth/RequireAuth";
@@ -120,8 +120,8 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => {
   const { permissions } = useAuthenticated();
   const query = async (): Promise<readonly Region[]> => {
     const endpoint = permissions.editWorkspaceProxies
-      ? getWorkspaceProxies
-      : getWorkspaceProxyRegions;
+      ? client.api.getWorkspaceProxies
+      : client.api.getWorkspaceProxyRegions;
     const resp = await endpoint();
     return resp.regions;
   };
diff --git a/site/src/contexts/auth/RequireAuth.tsx b/site/src/contexts/auth/RequireAuth.tsx
index d8db9071cc940..d820b267f55b5 100644
--- a/site/src/contexts/auth/RequireAuth.tsx
+++ b/site/src/contexts/auth/RequireAuth.tsx
@@ -1,6 +1,6 @@
 import { type FC, useEffect } from "react";
 import { Outlet, Navigate, useLocation } from "react-router-dom";
-import { axiosInstance } from "api/api";
+import { client } from "api/api";
 import { isApiError } from "api/errors";
 import { Loader } from "components/Loader/Loader";
 import { ProxyProvider } from "contexts/ProxyContext";
@@ -22,6 +22,7 @@ export const RequireAuth: FC = () => {
       return;
     }
 
+    const axiosInstance = client.getAxiosInstance();
     const interceptorHandle = axiosInstance.interceptors.response.use(
       (okResponse) => okResponse,
       (error: unknown) => {
diff --git a/site/src/contexts/useProxyLatency.ts b/site/src/contexts/useProxyLatency.ts
index 497cd457ede51..fb3d03d0269e4 100644
--- a/site/src/contexts/useProxyLatency.ts
+++ b/site/src/contexts/useProxyLatency.ts
@@ -1,6 +1,6 @@
 import PerformanceObserver from "@fastly/performance-observer-polyfill";
 import { useEffect, useReducer, useState } from "react";
-import { axiosInstance } from "api/api";
+import { client } from "api/api";
 import type { Region } from "api/typesGenerated";
 import { generateRandomString } from "utils/random";
 
@@ -197,6 +197,7 @@ export const useProxyLatency = (
     // The resource requests include xmlhttp requests.
     observer.observe({ entryTypes: ["resource"] });
 
+    const axiosInstance = client.getAxiosInstance();
     const proxyRequests = Object.keys(proxyChecks).map((latencyURL) => {
       return axiosInstance.get(latencyURL, {
         withCredentials: false,
diff --git a/site/src/modules/resources/AppLink/AppLink.tsx b/site/src/modules/resources/AppLink/AppLink.tsx
index 7243d2b1af5b6..bfbc03f4a9a91 100644
--- a/site/src/modules/resources/AppLink/AppLink.tsx
+++ b/site/src/modules/resources/AppLink/AppLink.tsx
@@ -4,7 +4,7 @@ import CircularProgress from "@mui/material/CircularProgress";
 import Link from "@mui/material/Link";
 import Tooltip from "@mui/material/Tooltip";
 import { type FC, useState } from "react";
-import { getApiKey } from "api/api";
+import { client } from "api/api";
 import type * as TypesGen from "api/typesGenerated";
 import { useProxy } from "contexts/ProxyContext";
 import { createAppLinkHref } from "utils/apps";
@@ -145,7 +145,7 @@ export const AppLink: FC<AppLinkProps> = ({ app, workspace, agent }) => {
             let url = href;
             if (hasMagicToken !== -1) {
               setFetchingSessionToken(true);
-              const key = await getApiKey();
+              const key = await client.api.getApiKey();
               url = href.replaceAll(magicTokenString, key.key);
               setFetchingSessionToken(false);
             }
diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx
index 1e4e2c2f59b82..8439a5c431c8b 100644
--- a/site/src/modules/resources/PortForwardButton.tsx
+++ b/site/src/modules/resources/PortForwardButton.tsx
@@ -19,7 +19,7 @@ import { type FormikContextType, useFormik } from "formik";
 import { useState, type FC } from "react";
 import { useQuery, useMutation } from "react-query";
 import * as Yup from "yup";
-import { getAgentListeningPorts } from "api/api";
+import { client } from "api/api";
 import {
   deleteWorkspacePortShare,
   upsertWorkspacePortShare,
@@ -70,7 +70,7 @@ export const PortForwardButton: FC<PortForwardButtonProps> = (props) => {
 
   const portsQuery = useQuery({
     queryKey: ["portForward", agent.id],
-    queryFn: () => getAgentListeningPorts(agent.id),
+    queryFn: () => client.api.getAgentListeningPorts(agent.id),
     enabled: agent.status === "connected",
     refetchInterval: 5_000,
   });
diff --git a/site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx b/site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx
index 73597dd22b6d3..36b6f5822f804 100644
--- a/site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx
+++ b/site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx
@@ -3,7 +3,7 @@ import ButtonGroup from "@mui/material/ButtonGroup";
 import Menu from "@mui/material/Menu";
 import MenuItem from "@mui/material/MenuItem";
 import { type FC, useState, useRef } from "react";
-import { getApiKey } from "api/api";
+import { client } from "api/api";
 import type { DisplayApp } from "api/typesGenerated";
 import { VSCodeIcon } from "components/Icons/VSCodeIcon";
 import { VSCodeInsidersIcon } from "components/Icons/VSCodeInsidersIcon";
@@ -119,7 +119,8 @@ const VSCodeButton: FC<VSCodeDesktopButtonProps> = ({
       disabled={loading}
       onClick={() => {
         setLoading(true);
-        getApiKey()
+        client.api
+          .getApiKey()
           .then(({ key }) => {
             const query = new URLSearchParams({
               owner: userName,
@@ -163,7 +164,8 @@ const VSCodeInsidersButton: FC<VSCodeDesktopButtonProps> = ({
       disabled={loading}
       onClick={() => {
         setLoading(true);
-        getApiKey()
+        client.api
+          .getApiKey()
           .then(({ key }) => {
             const query = new URLSearchParams({
               owner: userName,
diff --git a/site/src/pages/AuditPage/AuditPage.test.tsx b/site/src/pages/AuditPage/AuditPage.test.tsx
index 24b00fa082430..ea5e8b438606f 100644
--- a/site/src/pages/AuditPage/AuditPage.test.tsx
+++ b/site/src/pages/AuditPage/AuditPage.test.tsx
@@ -1,7 +1,7 @@
 import { screen, waitFor } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import { HttpResponse, http } from "msw";
-import * as API from "api/api";
+import { client } from "api/api";
 import { DEFAULT_RECORDS_PER_PAGE } from "components/PaginationWidget/utils";
 import {
   MockAuditLog,
@@ -61,10 +61,12 @@ describe("AuditPage", () => {
   it("renders page 5", async () => {
     // Given
     const page = 5;
-    const getAuditLogsSpy = jest.spyOn(API, "getAuditLogs").mockResolvedValue({
-      audit_logs: [MockAuditLog, MockAuditLog2],
-      count: 2,
-    });
+    const getAuditLogsSpy = jest
+      .spyOn(client.api, "getAuditLogs")
+      .mockResolvedValue({
+        audit_logs: [MockAuditLog, MockAuditLog2],
+        count: 2,
+      });
 
     // When
     await renderPage({ page: page });
@@ -82,7 +84,7 @@ describe("AuditPage", () => {
   describe("Filtering", () => {
     it("filters by URL", async () => {
       const getAuditLogsSpy = jest
-        .spyOn(API, "getAuditLogs")
+        .spyOn(client.api, "getAuditLogs")
         .mockResolvedValue({ audit_logs: [MockAuditLog], count: 1 });
 
       const query = "resource_type:workspace action:create";
@@ -98,7 +100,7 @@ describe("AuditPage", () => {
     it("resets page to 1 when filter is changed", async () => {
       await renderPage({ page: 2 });
 
-      const getAuditLogsSpy = jest.spyOn(API, "getAuditLogs");
+      const getAuditLogsSpy = jest.spyOn(client.api, "getAuditLogs");
       getAuditLogsSpy.mockClear();
 
       const filterField = screen.getByLabelText("Filter");
diff --git a/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx b/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx
index 38eacc38fd30e..1c28b4a1e3afb 100644
--- a/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx
+++ b/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx
@@ -1,6 +1,6 @@
 import { screen, waitFor, within } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import * as API from "api/api";
+import { client } from "api/api";
 import {
   MockTemplateExample,
   MockTemplateVersion,
@@ -35,14 +35,14 @@ test("Create template from starter template", async () => {
   const { router, container } = await renderPage(searchParams);
   const form = container.querySelector("form") as HTMLFormElement;
 
-  jest.spyOn(API, "createTemplateVersion").mockResolvedValueOnce({
+  jest.spyOn(client.api, "createTemplateVersion").mockResolvedValueOnce({
     ...MockTemplateVersion,
     job: {
       ...MockTemplateVersion.job,
       status: "pending",
     },
   });
-  jest.spyOn(API, "getTemplateVersion").mockResolvedValue({
+  jest.spyOn(client.api, "getTemplateVersion").mockResolvedValue({
     ...MockTemplateVersion,
     job: {
       ...MockTemplateVersion.job,
@@ -51,7 +51,7 @@ test("Create template from starter template", async () => {
     },
   });
   jest
-    .spyOn(API, "getTemplateVersionVariables")
+    .spyOn(client.api, "getTemplateVersionVariables")
     .mockResolvedValue([
       MockTemplateVersionVariable1,
       MockTemplateVersionVariable2,
@@ -85,35 +85,42 @@ test("Create template from starter template", async () => {
   // Setup the mock for the second template version creation before submit the form
   jest.clearAllMocks();
   jest
-    .spyOn(API, "createTemplateVersion")
+    .spyOn(client.api, "createTemplateVersion")
     .mockResolvedValue(MockTemplateVersion);
-  jest.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion);
-  jest.spyOn(API, "createTemplate").mockResolvedValue(MockTemplate);
+  jest
+    .spyOn(client.api, "getTemplateVersion")
+    .mockResolvedValue(MockTemplateVersion);
+  jest.spyOn(client.api, "createTemplate").mockResolvedValue(MockTemplate);
   await userEvent.click(
     within(form).getByRole("button", { name: /create template/i }),
   );
-  await waitFor(() => expect(API.createTemplate).toBeCalledTimes(1));
+  await waitFor(() => expect(client.api.createTemplate).toBeCalledTimes(1));
   expect(router.state.location.pathname).toEqual(
     `/templates/${MockTemplate.name}/files`,
   );
-  expect(API.createTemplateVersion).toHaveBeenCalledWith(MockOrganization.id, {
-    example_id: "aws-windows",
-    provisioner: "terraform",
-    storage_method: "file",
-    tags: {},
-    user_variable_values: [
-      { name: "first_variable", value: "First value" },
-      { name: "second_variable", value: "2" },
-      { name: "third_variable", value: "true" },
-    ],
-  });
+  expect(client.api.createTemplateVersion).toHaveBeenCalledWith(
+    MockOrganization.id,
+    {
+      example_id: "aws-windows",
+      provisioner: "terraform",
+      storage_method: "file",
+      tags: {},
+      user_variable_values: [
+        { name: "first_variable", value: "First value" },
+        { name: "second_variable", value: "2" },
+        { name: "third_variable", value: "true" },
+      ],
+    },
+  );
 });
 
 test("Create template from duplicating a template", async () => {
-  jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
-  jest.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion);
+  jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate);
   jest
-    .spyOn(API, "getTemplateVersionVariables")
+    .spyOn(client.api, "getTemplateVersion")
+    .mockResolvedValue(MockTemplateVersion);
+  jest
+    .spyOn(client.api, "getTemplateVersionVariables")
     .mockResolvedValue([MockTemplateVersionVariable1]);
 
   const searchParams = new URLSearchParams({
@@ -135,10 +142,12 @@ test("Create template from duplicating a template", async () => {
   ).toHaveValue(MockTemplateVersionVariable1.value);
   // Create template
   jest
-    .spyOn(API, "createTemplateVersion")
+    .spyOn(client.api, "createTemplateVersion")
+    .mockResolvedValue(MockTemplateVersion);
+  jest
+    .spyOn(client.api, "getTemplateVersion")
     .mockResolvedValue(MockTemplateVersion);
-  jest.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion);
-  jest.spyOn(API, "createTemplate").mockResolvedValue(MockTemplate);
+  jest.spyOn(client.api, "createTemplate").mockResolvedValue(MockTemplate);
   await userEvent.click(
     screen.getByRole("button", { name: /create template/i }),
   );
diff --git a/site/src/pages/CreateTokenPage/CreateTokenPage.test.tsx b/site/src/pages/CreateTokenPage/CreateTokenPage.test.tsx
index f58eed5d1e6b2..20f74d4c4dd79 100644
--- a/site/src/pages/CreateTokenPage/CreateTokenPage.test.tsx
+++ b/site/src/pages/CreateTokenPage/CreateTokenPage.test.tsx
@@ -1,6 +1,6 @@
 import { screen, within } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import * as API from "api/api";
+import { client } from "api/api";
 import {
   renderWithAuth,
   waitForLoaderToBeRemoved,
@@ -9,7 +9,7 @@ import { CreateTokenPage } from "./CreateTokenPage";
 
 describe("TokenPage", () => {
   it("shows the success modal", async () => {
-    jest.spyOn(API, "createToken").mockResolvedValueOnce({
+    jest.spyOn(client.api, "createToken").mockResolvedValueOnce({
       key: "abcd",
     });
 
diff --git a/site/src/pages/CreateTokenPage/CreateTokenPage.tsx b/site/src/pages/CreateTokenPage/CreateTokenPage.tsx
index 4ea1f98144671..a5883041b9299 100644
--- a/site/src/pages/CreateTokenPage/CreateTokenPage.tsx
+++ b/site/src/pages/CreateTokenPage/CreateTokenPage.tsx
@@ -3,7 +3,7 @@ import { type FC, useState } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation, useQuery } from "react-query";
 import { useNavigate } from "react-router-dom";
-import { createToken, getTokenConfig } from "api/api";
+import { client } from "api/api";
 import { ErrorAlert } from "components/Alert/ErrorAlert";
 import { CodeExample } from "components/CodeExample/CodeExample";
 import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
@@ -28,7 +28,7 @@ export const CreateTokenPage: FC = () => {
     isError: creationFailed,
     isSuccess: creationSuccessful,
     data: newToken,
-  } = useMutation(createToken);
+  } = useMutation(client.api.createToken);
   const {
     data: tokenConfig,
     isLoading: fetchingTokenConfig,
@@ -36,7 +36,7 @@ export const CreateTokenPage: FC = () => {
     error: tokenFetchError,
   } = useQuery({
     queryKey: ["tokenconfig"],
-    queryFn: getTokenConfig,
+    queryFn: client.api.getTokenConfig,
   });
 
   const [formError, setFormError] = useState<unknown>(undefined);
diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx
index 85ffe8ea45896..47c29e4ba91b7 100644
--- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx
+++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx
@@ -1,6 +1,6 @@
 import { fireEvent, screen, waitFor } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import * as API from "api/api";
+import { client } from "api/api";
 import {
   MockTemplate,
   MockUser,
@@ -36,14 +36,16 @@ const renderCreateWorkspacePage = () => {
 describe("CreateWorkspacePage", () => {
   it("succeeds with default owner", async () => {
     jest
-      .spyOn(API, "getUsers")
+      .spyOn(client.api, "getUsers")
       .mockResolvedValueOnce({ users: [MockUser], count: 1 });
     jest
-      .spyOn(API, "getWorkspaceQuota")
+      .spyOn(client.api, "getWorkspaceQuota")
       .mockResolvedValueOnce(MockWorkspaceQuota);
-    jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace);
     jest
-      .spyOn(API, "getTemplateVersionRichParameters")
+      .spyOn(client.api, "createWorkspace")
+      .mockResolvedValueOnce(MockWorkspace);
+    jest
+      .spyOn(client.api, "getTemplateVersionRichParameters")
       .mockResolvedValueOnce([MockTemplateVersionParameter1]);
 
     renderCreateWorkspacePage();
@@ -59,7 +61,7 @@ describe("CreateWorkspacePage", () => {
     await userEvent.click(submitButton);
 
     await waitFor(() =>
-      expect(API.createWorkspace).toBeCalledWith(
+      expect(client.api.createWorkspace).toBeCalledWith(
         MockUser.organization_ids[0],
         MockUser.id,
         expect.objectContaining({
@@ -73,7 +75,7 @@ describe("CreateWorkspacePage", () => {
     const param = "first_parameter";
     const paramValue = "It works!";
     jest
-      .spyOn(API, "getTemplateVersionRichParameters")
+      .spyOn(client.api, "getTemplateVersionRichParameters")
       .mockResolvedValueOnce([MockTemplateVersionParameter1]);
 
     renderWithAuth(<CreateWorkspacePage />, {
@@ -89,7 +91,7 @@ describe("CreateWorkspacePage", () => {
 
   it("rich parameter: number validation fails", async () => {
     jest
-      .spyOn(API, "getTemplateVersionRichParameters")
+      .spyOn(client.api, "getTemplateVersionRichParameters")
       .mockResolvedValueOnce([
         MockTemplateVersionParameter1,
         MockTemplateVersionParameter2,
@@ -124,7 +126,7 @@ describe("CreateWorkspacePage", () => {
 
   it("rich parameter: string validation fails", async () => {
     jest
-      .spyOn(API, "getTemplateVersionRichParameters")
+      .spyOn(client.api, "getTemplateVersionRichParameters")
       .mockResolvedValueOnce([
         MockTemplateVersionParameter1,
         MockTemplateVersionParameter3,
@@ -157,14 +159,16 @@ describe("CreateWorkspacePage", () => {
   });
 
   it("rich parameter: number validation fails with custom error", async () => {
-    jest.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce([
-      MockTemplateVersionParameter1,
-      {
-        ...MockTemplateVersionParameter2,
-        validation_error: "These are values: {min}, {max}, and {value}.",
-        validation_monotonic: undefined, // only needs min-max rules
-      },
-    ]);
+    jest
+      .spyOn(client.api, "getTemplateVersionRichParameters")
+      .mockResolvedValueOnce([
+        MockTemplateVersionParameter1,
+        {
+          ...MockTemplateVersionParameter2,
+          validation_error: "These are values: {min}, {max}, and {value}.",
+          validation_monotonic: undefined, // only needs min-max rules
+        },
+      ]);
 
     renderCreateWorkspacePage();
     await waitForLoaderToBeRemoved();
@@ -187,14 +191,16 @@ describe("CreateWorkspacePage", () => {
 
   it("external auth authenticates and succeeds", async () => {
     jest
-      .spyOn(API, "getWorkspaceQuota")
+      .spyOn(client.api, "getWorkspaceQuota")
       .mockResolvedValueOnce(MockWorkspaceQuota);
     jest
-      .spyOn(API, "getUsers")
+      .spyOn(client.api, "getUsers")
       .mockResolvedValueOnce({ users: [MockUser], count: 1 });
-    jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace);
     jest
-      .spyOn(API, "getTemplateVersionExternalAuth")
+      .spyOn(client.api, "createWorkspace")
+      .mockResolvedValueOnce(MockWorkspace);
+    jest
+      .spyOn(client.api, "getTemplateVersionExternalAuth")
       .mockResolvedValue([MockTemplateVersionExternalAuthGithub]);
 
     renderCreateWorkspacePage();
@@ -210,7 +216,7 @@ describe("CreateWorkspacePage", () => {
     await userEvent.click(githubButton);
 
     jest
-      .spyOn(API, "getTemplateVersionExternalAuth")
+      .spyOn(client.api, "getTemplateVersionExternalAuth")
       .mockResolvedValue([MockTemplateVersionExternalAuthGithubAuthenticated]);
 
     await screen.findByText(
@@ -223,7 +229,7 @@ describe("CreateWorkspacePage", () => {
     await userEvent.click(submitButton);
 
     await waitFor(() =>
-      expect(API.createWorkspace).toBeCalledWith(
+      expect(client.api.createWorkspace).toBeCalledWith(
         MockUser.organization_ids[0],
         MockUser.id,
         expect.objectContaining({
@@ -235,14 +241,16 @@ describe("CreateWorkspacePage", () => {
 
   it("optional external auth is optional", async () => {
     jest
-      .spyOn(API, "getWorkspaceQuota")
+      .spyOn(client.api, "getWorkspaceQuota")
       .mockResolvedValueOnce(MockWorkspaceQuota);
     jest
-      .spyOn(API, "getUsers")
+      .spyOn(client.api, "getUsers")
       .mockResolvedValueOnce({ users: [MockUser], count: 1 });
-    jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace);
     jest
-      .spyOn(API, "getTemplateVersionExternalAuth")
+      .spyOn(client.api, "createWorkspace")
+      .mockResolvedValueOnce(MockWorkspace);
+    jest
+      .spyOn(client.api, "getTemplateVersionExternalAuth")
       .mockResolvedValue([
         { ...MockTemplateVersionExternalAuthGithub, optional: true },
       ]);
@@ -263,7 +271,7 @@ describe("CreateWorkspacePage", () => {
     await userEvent.click(submitButton);
 
     await waitFor(() =>
-      expect(API.createWorkspace).toBeCalledWith(
+      expect(client.api.createWorkspace).toBeCalledWith(
         MockUser.organization_ids[0],
         MockUser.id,
         expect.objectContaining({
@@ -276,7 +284,7 @@ describe("CreateWorkspacePage", () => {
   it("auto create a workspace if uses mode=auto", async () => {
     const param = "first_parameter";
     const paramValue = "It works!";
-    const createWorkspaceSpy = jest.spyOn(API, "createWorkspace");
+    const createWorkspaceSpy = jest.spyOn(client.api, "createWorkspace");
 
     renderWithAuth(<CreateWorkspacePage />, {
       route:
@@ -307,10 +315,10 @@ describe("CreateWorkspacePage", () => {
   it("disables mode=auto if a required external auth provider is not connected", async () => {
     const param = "first_parameter";
     const paramValue = "It works!";
-    const createWorkspaceSpy = jest.spyOn(API, "createWorkspace");
+    const createWorkspaceSpy = jest.spyOn(client.api, "createWorkspace");
 
     const externalAuthSpy = jest
-      .spyOn(API, "getTemplateVersionExternalAuth")
+      .spyOn(client.api, "getTemplateVersionExternalAuth")
       .mockResolvedValue([MockTemplateVersionExternalAuthGithub]);
 
     renderWithAuth(<CreateWorkspacePage />, {
@@ -336,7 +344,7 @@ describe("CreateWorkspacePage", () => {
   it("auto create a workspace if uses mode=auto and version=version-id", async () => {
     const param = "first_parameter";
     const paramValue = "It works!";
-    const createWorkspaceSpy = jest.spyOn(API, "createWorkspace");
+    const createWorkspaceSpy = jest.spyOn(client.api, "createWorkspace");
 
     renderWithAuth(<CreateWorkspacePage />, {
       route:
diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx
index b885b11d32f6c..48dd90aa3232d 100644
--- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx
+++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx
@@ -2,7 +2,7 @@ import { type FC, useCallback, useEffect, useState, useRef } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation, useQuery, useQueryClient } from "react-query";
 import { useNavigate, useParams, useSearchParams } from "react-router-dom";
-import { getUserParameters } from "api/api";
+import { client } from "api/api";
 import type { ApiErrorResponse } from "api/errors";
 import { checkAuthorization } from "api/queries/authCheck";
 import {
@@ -99,7 +99,7 @@ const CreateWorkspacePage: FC = () => {
   const autofillEnabled = experiments.includes("auto-fill-parameters");
   const userParametersQuery = useQuery({
     queryKey: ["userParameters"],
-    queryFn: () => getUserParameters(templateQuery.data!.id),
+    queryFn: () => client.api.getUserParameters(templateQuery.data!.id),
     enabled: autofillEnabled && templateQuery.isSuccess,
   });
   const autofillParameters = getAutofillParameters(
diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx
index 98758f22f7b5b..f470446e4655e 100644
--- a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx
+++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx
@@ -2,7 +2,7 @@ import type { FC } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation } from "react-query";
 import { useNavigate } from "react-router-dom";
-import { createLicense } from "api/api";
+import { client } from "api/api";
 import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
 import { pageTitle } from "utils/page";
 import { AddNewLicensePageView } from "./AddNewLicensePageView";
@@ -14,7 +14,7 @@ const AddNewLicensePage: FC = () => {
     mutate: saveLicenseKeyApi,
     isLoading: isCreating,
     error: savingLicenseError,
-  } = useMutation(createLicense, {
+  } = useMutation(client.api.createLicense, {
     onSuccess: () => {
       displaySuccess("You have successfully added a license");
       navigate("/deployment/licenses?success=true");
diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx
index 8f172cf1155a1..238d861511a41 100644
--- a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx
+++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx
@@ -3,7 +3,7 @@ import { Helmet } from "react-helmet-async";
 import { useMutation, useQuery, useQueryClient } from "react-query";
 import { useSearchParams } from "react-router-dom";
 import useToggle from "react-use/lib/useToggle";
-import { getLicenses, removeLicense } from "api/api";
+import { client } from "api/api";
 import { getErrorMessage } from "api/errors";
 import { entitlements, refreshEntitlements } from "api/queries/entitlements";
 import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
@@ -32,7 +32,7 @@ const LicensesSettingsPage: FC = () => {
   }, [entitlementsQuery.error]);
 
   const { mutate: removeLicenseApi, isLoading: isRemovingLicense } =
-    useMutation(removeLicense, {
+    useMutation(client.api.removeLicense, {
       onSuccess: () => {
         displaySuccess("Successfully removed license");
         void queryClient.invalidateQueries(["licenses"]);
@@ -44,7 +44,7 @@ const LicensesSettingsPage: FC = () => {
 
   const { data: licenses, isLoading } = useQuery({
     queryKey: ["licenses"],
-    queryFn: () => getLicenses(),
+    queryFn: () => client.api.getLicenses(),
   });
 
   useEffect(() => {
diff --git a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.test.tsx b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.test.tsx
index 580df7f645c7c..e1afd45f64531 100644
--- a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.test.tsx
+++ b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.test.tsx
@@ -1,6 +1,6 @@
 import { screen } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import * as API from "api/api";
+import { client } from "api/api";
 import { TemplateLayout } from "pages/TemplatePage/TemplateLayout";
 import {
   MockTemplate,
@@ -15,7 +15,7 @@ import TemplateEmbedPage from "./TemplateEmbedPage";
 
 test("Users can fill the parameters and copy the open in coder url", async () => {
   jest
-    .spyOn(API, "getTemplateVersionRichParameters")
+    .spyOn(client.api, "getTemplateVersionRichParameters")
     .mockResolvedValue([parameter1, parameter2]);
 
   renderWithAuth(
diff --git a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx
index 57716e0b91fe5..6263126eda7b7 100644
--- a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx
+++ b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx
@@ -7,7 +7,7 @@ import RadioGroup from "@mui/material/RadioGroup";
 import { type FC, useEffect, useState } from "react";
 import { Helmet } from "react-helmet-async";
 import { useQuery } from "react-query";
-import { getTemplateVersionRichParameters } from "api/api";
+import { client } from "api/api";
 import type { Template, TemplateVersionParameter } from "api/typesGenerated";
 import { FormSection, VerticalForm } from "components/Form/Form";
 import { Loader } from "components/Loader/Loader";
@@ -24,7 +24,8 @@ const TemplateEmbedPage: FC = () => {
   const { template } = useTemplateLayoutContext();
   const { data: templateParameters } = useQuery({
     queryKey: ["template", template.id, "embed"],
-    queryFn: () => getTemplateVersionRichParameters(template.active_version_id),
+    queryFn: () =>
+      client.api.getTemplateVersionRichParameters(template.active_version_id),
   });
 
   return (
diff --git a/site/src/pages/TemplatePage/TemplateLayout.tsx b/site/src/pages/TemplatePage/TemplateLayout.tsx
index 43bf807c45df2..23c663e32ba1c 100644
--- a/site/src/pages/TemplatePage/TemplateLayout.tsx
+++ b/site/src/pages/TemplatePage/TemplateLayout.tsx
@@ -7,11 +7,7 @@ import {
 } from "react";
 import { useQuery } from "react-query";
 import { Outlet, useLocation, useNavigate, useParams } from "react-router-dom";
-import {
-  checkAuthorization,
-  getTemplateByName,
-  getTemplateVersion,
-} from "api/api";
+import { client } from "api/api";
 import type { AuthorizationRequest } from "api/typesGenerated";
 import { ErrorAlert } from "components/Alert/ErrorAlert";
 import { Loader } from "components/Loader/Loader";
@@ -39,10 +35,14 @@ const templatePermissions = (
 });
 
 const fetchTemplate = async (organizationId: string, templateName: string) => {
-  const template = await getTemplateByName(organizationId, templateName);
+  const template = await client.api.getTemplateByName(
+    organizationId,
+    templateName,
+  );
+
   const [activeVersion, permissions] = await Promise.all([
-    getTemplateVersion(template.active_version_id),
-    checkAuthorization({
+    client.api.getTemplateVersion(template.active_version_id),
+    client.api.checkAuthorization({
       checks: templatePermissions(template.id),
     }),
   ]);
diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx
index c0460c5b59d74..fbd1ff993c802 100644
--- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx
+++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx
@@ -1,7 +1,7 @@
 import type { FC } from "react";
 import { Helmet } from "react-helmet-async";
 import { useQuery } from "react-query";
-import { getTemplateVersionResources } from "api/api";
+import { client } from "api/api";
 import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout";
 import { getTemplatePageTitle } from "../utils";
 import { TemplateSummaryPageView } from "./TemplateSummaryPageView";
@@ -10,7 +10,7 @@ export const TemplateSummaryPage: FC = () => {
   const { template, activeVersion } = useTemplateLayoutContext();
   const { data: resources } = useQuery({
     queryKey: ["templates", template.id, "resources"],
-    queryFn: () => getTemplateVersionResources(activeVersion.id),
+    queryFn: () => client.api.getTemplateVersionResources(activeVersion.id),
   });
 
   return (
diff --git a/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx b/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx
index 5d50f110d00de..d08de7669e505 100644
--- a/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx
+++ b/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx
@@ -1,11 +1,7 @@
 import { useState } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation, useQuery } from "react-query";
-import {
-  archiveTemplateVersion,
-  getTemplateVersions,
-  updateActiveTemplateVersion,
-} from "api/api";
+import { client } from "api/api";
 import { getErrorMessage } from "api/errors";
 import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
 import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
@@ -17,7 +13,7 @@ const TemplateVersionsPage = () => {
   const { template, permissions } = useTemplateLayoutContext();
   const { data } = useQuery({
     queryKey: ["template", "versions", template.id],
-    queryFn: () => getTemplateVersions(template.id),
+    queryFn: () => client.api.getTemplateVersions(template.id),
   });
   // We use this to update the active version in the UI without having to refetch the template
   const [latestActiveVersion, setLatestActiveVersion] = useState(
@@ -25,7 +21,7 @@ const TemplateVersionsPage = () => {
   );
   const { mutate: promoteVersion, isLoading: isPromoting } = useMutation({
     mutationFn: (templateVersionId: string) => {
-      return updateActiveTemplateVersion(template.id, {
+      return client.api.updateActiveTemplateVersion(template.id, {
         id: templateVersionId,
       });
     },
@@ -41,7 +37,7 @@ const TemplateVersionsPage = () => {
 
   const { mutate: archiveVersion, isLoading: isArchiving } = useMutation({
     mutationFn: (templateVersionId: string) => {
-      return archiveTemplateVersion(templateVersionId);
+      return client.api.archiveTemplateVersion(templateVersionId);
     },
     onSuccess: async () => {
       // The reload is unfortunate. When a version is archived, we should hide
diff --git a/site/src/pages/TemplatePage/useDeletionDialogState.test.ts b/site/src/pages/TemplatePage/useDeletionDialogState.test.ts
index 63a53f3d1b682..5f8421f2d6584 100644
--- a/site/src/pages/TemplatePage/useDeletionDialogState.test.ts
+++ b/site/src/pages/TemplatePage/useDeletionDialogState.test.ts
@@ -1,5 +1,5 @@
 import { act, renderHook, waitFor } from "@testing-library/react";
-import * as API from "api/api";
+import { client } from "api/api";
 import { MockTemplate } from "testHelpers/entities";
 import { useDeletionDialogState } from "./useDeletionDialogState";
 
@@ -23,9 +23,9 @@ test("confirm template deletion", async () => {
   expect(result.current.isDeleteDialogOpen).toBeTruthy();
 
   // Confirm delete
-  jest.spyOn(API, "deleteTemplate");
+  jest.spyOn(client.api, "deleteTemplate");
   await act(async () => result.current.confirmDelete());
-  await waitFor(() => expect(API.deleteTemplate).toBeCalledTimes(1));
+  await waitFor(() => expect(client.api.deleteTemplate).toBeCalledTimes(1));
   expect(onDeleteTemplate).toBeCalledTimes(1);
 });
 
diff --git a/site/src/pages/TemplatePage/useDeletionDialogState.ts b/site/src/pages/TemplatePage/useDeletionDialogState.ts
index 7b3b7bbfcac63..8185d128efa20 100644
--- a/site/src/pages/TemplatePage/useDeletionDialogState.ts
+++ b/site/src/pages/TemplatePage/useDeletionDialogState.ts
@@ -1,5 +1,5 @@
 import { useState } from "react";
-import { deleteTemplate } from "api/api";
+import { client } from "api/api";
 import { getErrorMessage } from "api/errors";
 import { displayError } from "components/GlobalSnackbar/utils";
 
@@ -27,7 +27,7 @@ export const useDeletionDialogState = (
   const confirmDelete = async () => {
     try {
       setState({ status: "deleting" });
-      await deleteTemplate(templateId);
+      await client.api.deleteTemplate(templateId);
       onDelete();
     } catch (e) {
       setState({ status: "confirming" });
diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx
index 2b9402bda94bd..4b0120a29c546 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx
@@ -1,7 +1,7 @@
 import { screen, waitFor } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import { http, HttpResponse } from "msw";
-import * as API from "api/api";
+import { client, withDefaultFeatures } from "api/api";
 import type { Template, UpdateTemplateMeta } from "api/typesGenerated";
 import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter";
 import { MockEntitlements, MockTemplate } from "testHelpers/entities";
@@ -104,12 +104,14 @@ const fillAndSubmitForm = async ({
 describe("TemplateSettingsPage", () => {
   it("succeeds", async () => {
     await renderTemplateSettingsPage();
-    jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({
+    jest.spyOn(client.api, "updateTemplateMeta").mockResolvedValueOnce({
       ...MockTemplate,
       ...validFormValues,
     });
     await fillAndSubmitForm(validFormValues);
-    await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1));
+    await waitFor(() =>
+      expect(client.api.updateTemplateMeta).toBeCalledTimes(1),
+    );
   });
 
   it("allows a description of 128 chars", () => {
@@ -138,13 +140,16 @@ describe("TemplateSettingsPage", () => {
         http.get("/api/v2/entitlements", () => {
           return HttpResponse.json({
             ...MockEntitlements,
-            features: API.withDefaultFeatures({
+            features: withDefaultFeatures({
               access_control: { enabled: true, entitlement: "entitled" },
             }),
           });
         }),
       );
-      const updateTemplateMetaSpy = jest.spyOn(API, "updateTemplateMeta");
+      const updateTemplateMetaSpy = jest.spyOn(
+        client.api,
+        "updateTemplateMeta",
+      );
       const deprecationMessage = "This template is deprecated";
 
       await renderTemplateSettingsPage();
@@ -163,13 +168,16 @@ describe("TemplateSettingsPage", () => {
         http.get("/api/v2/entitlements", () => {
           return HttpResponse.json({
             ...MockEntitlements,
-            features: API.withDefaultFeatures({
+            features: withDefaultFeatures({
               access_control: { enabled: false, entitlement: "not_entitled" },
             }),
           });
         }),
       );
-      const updateTemplateMetaSpy = jest.spyOn(API, "updateTemplateMeta");
+      const updateTemplateMetaSpy = jest.spyOn(
+        client.api,
+        "updateTemplateMeta",
+      );
 
       await renderTemplateSettingsPage();
       await deprecateTemplate(
diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx
index 15e4c8fb21206..3ea10d8308b85 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx
@@ -2,7 +2,7 @@ import type { FC } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation, useQueryClient } from "react-query";
 import { useNavigate, useParams } from "react-router-dom";
-import { updateTemplateMeta } from "api/api";
+import { client } from "api/api";
 import { templateByNameKey } from "api/queries/templates";
 import type { UpdateTemplateMeta } from "api/typesGenerated";
 import { displaySuccess } from "components/GlobalSnackbar/utils";
@@ -31,7 +31,9 @@ export const TemplateSettingsPage: FC = () => {
     isLoading: isSubmitting,
     error: submitError,
   } = useMutation(
-    (data: UpdateTemplateMeta) => updateTemplateMeta(template.id, data),
+    (data: UpdateTemplateMeta) => {
+      return client.api.updateTemplateMeta(template.id, data);
+    },
     {
       onSuccess: async (data) => {
         // This update has a chance to return a 304 which means nothing was updated.
diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx
index cb0505e99b800..61e474be7ee84 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx
@@ -1,6 +1,6 @@
 import { screen, waitFor } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import * as API from "api/api";
+import { client } from "api/api";
 import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter";
 import {
   MockEntitlementsWithScheduling,
@@ -127,38 +127,38 @@ function waitForWithCutoff(callback: () => void | Promise<void>) {
 describe("TemplateSchedulePage", () => {
   beforeEach(() => {
     jest
-      .spyOn(API, "getEntitlements")
+      .spyOn(client.api, "getEntitlements")
       .mockResolvedValue(MockEntitlementsWithScheduling);
   });
 
   it("Calls the API when user fills in and submits a form", async () => {
     await renderTemplateSchedulePage();
-    jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({
+    jest.spyOn(client.api, "updateTemplateMeta").mockResolvedValueOnce({
       ...MockTemplate,
       ...validFormValues,
     });
 
     await fillAndSubmitForm(validFormValues);
     await waitForWithCutoff(() =>
-      expect(API.updateTemplateMeta).toBeCalledTimes(1),
+      expect(client.api.updateTemplateMeta).toBeCalledTimes(1),
     );
   });
 
   test("default is converted to and from hours", async () => {
     await renderTemplateSchedulePage();
 
-    jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({
+    jest.spyOn(client.api, "updateTemplateMeta").mockResolvedValueOnce({
       ...MockTemplate,
       ...validFormValues,
     });
 
     await fillAndSubmitForm(validFormValues);
     await waitForWithCutoff(() =>
-      expect(API.updateTemplateMeta).toBeCalledTimes(1),
+      expect(client.api.updateTemplateMeta).toBeCalledTimes(1),
     );
 
     await waitForWithCutoff(() => {
-      expect(API.updateTemplateMeta).toBeCalledWith(
+      expect(client.api.updateTemplateMeta).toBeCalledWith(
         "test-template",
         expect.objectContaining({
           default_ttl_ms: (validFormValues.default_ttl_ms || 0) * 3600000,
@@ -170,18 +170,18 @@ describe("TemplateSchedulePage", () => {
   test("failure, dormancy, and dormancy auto-deletion converted to and from days", async () => {
     await renderTemplateSchedulePage();
 
-    jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({
+    jest.spyOn(client.api, "updateTemplateMeta").mockResolvedValueOnce({
       ...MockTemplate,
       ...validFormValues,
     });
 
     await fillAndSubmitForm(validFormValues);
     await waitForWithCutoff(() =>
-      expect(API.updateTemplateMeta).toBeCalledTimes(1),
+      expect(client.api.updateTemplateMeta).toBeCalledTimes(1),
     );
 
     await waitForWithCutoff(() => {
-      expect(API.updateTemplateMeta).toBeCalledWith(
+      expect(client.api.updateTemplateMeta).toBeCalledWith(
         "test-template",
         expect.objectContaining({
           failure_ttl_ms: (validFormValues.failure_ttl_ms || 0) * 86400000,
diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx
index de45cbd38652e..4c420bae5568e 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx
@@ -2,7 +2,7 @@ import type { FC } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation, useQueryClient } from "react-query";
 import { useNavigate, useParams } from "react-router-dom";
-import { updateTemplateMeta } from "api/api";
+import { client } from "api/api";
 import { templateByNameKey } from "api/queries/templates";
 import type { UpdateTemplateMeta } from "api/typesGenerated";
 import { displaySuccess } from "components/GlobalSnackbar/utils";
@@ -27,7 +27,8 @@ const TemplateSchedulePage: FC = () => {
     isLoading: isSubmitting,
     error: submitError,
   } = useMutation(
-    (data: UpdateTemplateMeta) => updateTemplateMeta(template.id, data),
+    (data: UpdateTemplateMeta) =>
+      client.api.updateTemplateMeta(template.id, data),
     {
       onSuccess: async () => {
         await queryClient.invalidateQueries(
diff --git a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx
index c123a317691e5..99047c540d352 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx
@@ -1,6 +1,6 @@
 import { screen } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import * as API from "api/api";
+import { client } from "api/api";
 import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter";
 import {
   MockTemplate,
@@ -32,12 +32,14 @@ const renderTemplateVariablesPage = async () => {
 
 describe("TemplateVariablesPage", () => {
   it("renders with variables", async () => {
-    jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate);
     jest
-      .spyOn(API, "getTemplateVersion")
+      .spyOn(client.api, "getTemplateByName")
+      .mockResolvedValueOnce(MockTemplate);
+    jest
+      .spyOn(client.api, "getTemplateVersion")
       .mockResolvedValueOnce(MockTemplateVersion);
     jest
-      .spyOn(API, "getTemplateVersionVariables")
+      .spyOn(client.api, "getTemplateVersionVariables")
       .mockResolvedValueOnce([
         MockTemplateVersionVariable1,
         MockTemplateVersionVariable2,
@@ -57,22 +59,26 @@ describe("TemplateVariablesPage", () => {
   });
 
   it("user submits the form successfully", async () => {
-    jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate);
     jest
-      .spyOn(API, "getTemplateVersion")
+      .spyOn(client.api, "getTemplateByName")
+      .mockResolvedValueOnce(MockTemplate);
+    jest
+      .spyOn(client.api, "getTemplateVersion")
       .mockResolvedValue(MockTemplateVersion);
     jest
-      .spyOn(API, "getTemplateVersionVariables")
+      .spyOn(client.api, "getTemplateVersionVariables")
       .mockResolvedValueOnce([
         MockTemplateVersionVariable1,
         MockTemplateVersionVariable2,
       ]);
     jest
-      .spyOn(API, "createTemplateVersion")
+      .spyOn(client.api, "createTemplateVersion")
       .mockResolvedValueOnce(MockTemplateVersion2);
-    jest.spyOn(API, "updateActiveTemplateVersion").mockResolvedValueOnce({
-      message: "done",
-    });
+    jest
+      .spyOn(client.api, "updateActiveTemplateVersion")
+      .mockResolvedValueOnce({
+        message: "done",
+      });
 
     await renderTemplateVariablesPage();
 
diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx
index 6f54255fbe23a..22b172c9cd9ed 100644
--- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx
+++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx
@@ -26,6 +26,8 @@ import type { MonacoEditorProps } from "./MonacoEditor";
 import { Language } from "./PublishTemplateVersionDialog";
 import TemplateVersionEditorPage from "./TemplateVersionEditorPage";
 
+const { client } = api;
+
 // For some reason this component in Jest is throwing a MUI style warning so,
 // since we don't need it for this test, we can mock it out
 jest.mock(
@@ -72,8 +74,8 @@ const buildTemplateVersion = async (
   user: UserEvent,
   topbar: HTMLElement,
 ) => {
-  jest.spyOn(api, "uploadFile").mockResolvedValueOnce({ hash: "hash" });
-  jest.spyOn(api, "createTemplateVersion").mockResolvedValue({
+  jest.spyOn(client.api, "uploadFile").mockResolvedValueOnce({ hash: "hash" });
+  jest.spyOn(client.api, "createTemplateVersion").mockResolvedValue({
     ...templateVersion,
     job: {
       ...templateVersion.job,
@@ -81,7 +83,7 @@ const buildTemplateVersion = async (
     },
   });
   jest
-    .spyOn(api, "getTemplateVersionByName")
+    .spyOn(client.api, "getTemplateVersionByName")
     .mockResolvedValue(templateVersion);
   jest
     .spyOn(api, "watchBuildLogsByTemplateVersionId")
@@ -116,10 +118,10 @@ test("Use custom name, message and set it as active when publishing", async () =
 
   // Publish
   const patchTemplateVersion = jest
-    .spyOn(api, "patchTemplateVersion")
+    .spyOn(client.api, "patchTemplateVersion")
     .mockResolvedValue(newTemplateVersion);
   const updateActiveTemplateVersion = jest
-    .spyOn(api, "updateActiveTemplateVersion")
+    .spyOn(client.api, "updateActiveTemplateVersion")
     .mockResolvedValue({ message: "" });
   const publishButton = within(topbar).getByRole("button", {
     name: "Publish",
@@ -162,10 +164,10 @@ test("Do not mark as active if promote is not checked", async () => {
 
   // Publish
   const patchTemplateVersion = jest
-    .spyOn(api, "patchTemplateVersion")
+    .spyOn(client.api, "patchTemplateVersion")
     .mockResolvedValue(newTemplateVersion);
   const updateActiveTemplateVersion = jest
-    .spyOn(api, "updateActiveTemplateVersion")
+    .spyOn(client.api, "updateActiveTemplateVersion")
     .mockResolvedValue({ message: "" });
   const publishButton = within(topbar).getByRole("button", {
     name: "Publish",
@@ -207,7 +209,7 @@ test("Patch request is not send when there are no changes", async () => {
 
   // Publish
   const patchTemplateVersion = jest
-    .spyOn(api, "patchTemplateVersion")
+    .spyOn(client.api, "patchTemplateVersion")
     .mockResolvedValue(newTemplateVersion);
   const publishButton = within(topbar).getByRole("button", {
     name: "Publish",
diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx
index 10412bd616a67..97e752c799459 100644
--- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx
+++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx
@@ -2,7 +2,7 @@ import { type FC, useEffect, useState } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation, useQuery, useQueryClient } from "react-query";
 import { useNavigate, useParams, useSearchParams } from "react-router-dom";
-import { patchTemplateVersion, updateActiveTemplateVersion } from "api/api";
+import { client } from "api/api";
 import { file, uploadFile } from "api/queries/files";
 import {
   createTemplateVersion,
@@ -323,12 +323,12 @@ const publishVersion = async (options: {
   const publishActions: Promise<unknown>[] = [];
 
   if (haveChanges) {
-    publishActions.push(patchTemplateVersion(version.id, data));
+    publishActions.push(client.api.patchTemplateVersion(version.id, data));
   }
 
   if (isActiveVersion) {
     publishActions.push(
-      updateActiveTemplateVersion(version.template_id!, {
+      client.api.updateActiveTemplateVersion(version.template_id!, {
         id: version.id,
       }),
     );
diff --git a/site/src/pages/TerminalPage/TerminalPage.test.tsx b/site/src/pages/TerminalPage/TerminalPage.test.tsx
index 8b910602474d0..538087ca1b281 100644
--- a/site/src/pages/TerminalPage/TerminalPage.test.tsx
+++ b/site/src/pages/TerminalPage/TerminalPage.test.tsx
@@ -2,7 +2,7 @@ import "jest-canvas-mock";
 import { waitFor } from "@testing-library/react";
 import WS from "jest-websocket-mock";
 import { HttpResponse, http } from "msw";
-import * as API from "api/api";
+import { client } from "api/api";
 import {
   MockUser,
   MockWorkspace,
@@ -56,7 +56,7 @@ describe("TerminalPage", () => {
 
   it("loads the right workspace data", async () => {
     jest
-      .spyOn(API, "getWorkspaceByOwnerAndName")
+      .spyOn(client.api, "getWorkspaceByOwnerAndName")
       .mockResolvedValue(MockWorkspace);
     new WS(
       `ws://localhost/api/v2/workspaceagents/${MockWorkspaceAgent.id}/pty`,
@@ -65,7 +65,7 @@ describe("TerminalPage", () => {
       `/${MockUser.username}/${MockWorkspace.name}/terminal`,
     );
     await waitFor(() => {
-      expect(API.getWorkspaceByOwnerAndName).toHaveBeenCalledWith(
+      expect(client.api.getWorkspaceByOwnerAndName).toHaveBeenCalledWith(
         MockUser.username,
         MockWorkspace.name,
         { include_deleted: true },
diff --git a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx
index 4bce60a5fe465..52a53b8a0803e 100644
--- a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx
+++ b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx
@@ -1,5 +1,5 @@
 import { fireEvent, screen, waitFor } from "@testing-library/react";
-import * as API from "api/api";
+import { client } from "api/api";
 import { mockApiError } from "testHelpers/entities";
 import { renderWithAuth } from "testHelpers/renderHelpers";
 import * as AccountForm from "./AccountForm";
@@ -25,34 +25,36 @@ const fillAndSubmitForm = async () => {
 describe("AccountPage", () => {
   describe("when it is a success", () => {
     it("shows the success message", async () => {
-      jest.spyOn(API, "updateProfile").mockImplementationOnce((userId, data) =>
-        Promise.resolve({
-          id: userId,
-          email: "user@coder.com",
-          created_at: new Date().toISOString(),
-          status: "active",
-          organization_ids: ["123"],
-          roles: [],
-          avatar_url: "",
-          last_seen_at: new Date().toISOString(),
-          login_type: "password",
-          theme_preference: "",
-          ...data,
-        }),
-      );
+      jest
+        .spyOn(client.api, "updateProfile")
+        .mockImplementationOnce((userId, data) =>
+          Promise.resolve({
+            id: userId,
+            email: "user@coder.com",
+            created_at: new Date().toISOString(),
+            status: "active",
+            organization_ids: ["123"],
+            roles: [],
+            avatar_url: "",
+            last_seen_at: new Date().toISOString(),
+            login_type: "password",
+            theme_preference: "",
+            ...data,
+          }),
+        );
       renderWithAuth(<AccountPage />);
       await fillAndSubmitForm();
 
       const successMessage = await screen.findByText("Updated settings.");
       expect(successMessage).toBeDefined();
-      expect(API.updateProfile).toBeCalledTimes(1);
-      expect(API.updateProfile).toBeCalledWith("me", newData);
+      expect(client.api.updateProfile).toBeCalledTimes(1);
+      expect(client.api.updateProfile).toBeCalledWith("me", newData);
     });
   });
 
   describe("when the username is already taken", () => {
     it("shows an error", async () => {
-      jest.spyOn(API, "updateProfile").mockRejectedValueOnce(
+      jest.spyOn(client.api, "updateProfile").mockRejectedValueOnce(
         mockApiError({
           message: "Invalid profile",
           validations: [
@@ -68,14 +70,14 @@ describe("AccountPage", () => {
         "Username is already in use",
       );
       expect(errorMessage).toBeDefined();
-      expect(API.updateProfile).toBeCalledTimes(1);
-      expect(API.updateProfile).toBeCalledWith("me", newData);
+      expect(client.api.updateProfile).toBeCalledTimes(1);
+      expect(client.api.updateProfile).toBeCalledWith("me", newData);
     });
   });
 
   describe("when it is an unknown error", () => {
     it("shows a generic error message", async () => {
-      jest.spyOn(API, "updateProfile").mockRejectedValueOnce({
+      jest.spyOn(client.api, "updateProfile").mockRejectedValueOnce({
         data: "unknown error",
       });
 
@@ -84,8 +86,8 @@ describe("AccountPage", () => {
 
       const errorMessage = await screen.findByText("Something went wrong.");
       expect(errorMessage).toBeDefined();
-      expect(API.updateProfile).toBeCalledTimes(1);
-      expect(API.updateProfile).toBeCalledWith("me", newData);
+      expect(client.api.updateProfile).toBeCalledTimes(1);
+      expect(client.api.updateProfile).toBeCalledWith("me", newData);
     });
   });
 });
diff --git a/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx b/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx
index 01c0ad3addfd0..8cca935805d8e 100644
--- a/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx
+++ b/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx
@@ -1,6 +1,6 @@
 import { screen } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import * as API from "api/api";
+import { client } from "api/api";
 import { MockUser } from "testHelpers/entities";
 import { renderWithAuth } from "testHelpers/renderHelpers";
 import { AppearancePage } from "./AppearancePage";
@@ -9,7 +9,7 @@ describe("appearance page", () => {
   it("does nothing when selecting current theme", async () => {
     renderWithAuth(<AppearancePage />);
 
-    jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({
+    jest.spyOn(client.api, "updateAppearanceSettings").mockResolvedValueOnce({
       ...MockUser,
       theme_preference: "dark",
     });
@@ -18,13 +18,13 @@ describe("appearance page", () => {
     await userEvent.click(dark);
 
     // Check if the API was called correctly
-    expect(API.updateAppearanceSettings).toBeCalledTimes(0);
+    expect(client.api.updateAppearanceSettings).toBeCalledTimes(0);
   });
 
   it("changes theme to dark blue", async () => {
     renderWithAuth(<AppearancePage />);
 
-    jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({
+    jest.spyOn(client.api, "updateAppearanceSettings").mockResolvedValueOnce({
       ...MockUser,
       theme_preference: "darkBlue",
     });
@@ -33,8 +33,8 @@ describe("appearance page", () => {
     await userEvent.click(darkBlue);
 
     // Check if the API was called correctly
-    expect(API.updateAppearanceSettings).toBeCalledTimes(1);
-    expect(API.updateAppearanceSettings).toHaveBeenCalledWith("me", {
+    expect(client.api.updateAppearanceSettings).toBeCalledTimes(1);
+    expect(client.api.updateAppearanceSettings).toHaveBeenCalledWith("me", {
       theme_preference: "darkBlue",
     });
   });
@@ -42,7 +42,7 @@ describe("appearance page", () => {
   it("changes theme to light", async () => {
     renderWithAuth(<AppearancePage />);
 
-    jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({
+    jest.spyOn(client.api, "updateAppearanceSettings").mockResolvedValueOnce({
       ...MockUser,
       theme_preference: "light",
     });
@@ -51,8 +51,8 @@ describe("appearance page", () => {
     await userEvent.click(light);
 
     // Check if the API was called correctly
-    expect(API.updateAppearanceSettings).toBeCalledTimes(1);
-    expect(API.updateAppearanceSettings).toHaveBeenCalledWith("me", {
+    expect(client.api.updateAppearanceSettings).toBeCalledTimes(1);
+    expect(client.api.updateAppearanceSettings).toHaveBeenCalledWith("me", {
       theme_preference: "light",
     });
   });
diff --git a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx
index daa03d50ea839..e7c46bc466adf 100644
--- a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx
+++ b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx
@@ -1,5 +1,5 @@
 import { fireEvent, screen, within } from "@testing-library/react";
-import * as API from "api/api";
+import { client } from "api/api";
 import { MockGitSSHKey, mockApiError } from "testHelpers/entities";
 import { renderWithAuth } from "testHelpers/renderHelpers";
 import { Language as SSHKeysPageLanguage, SSHKeysPage } from "./SSHKeysPage";
@@ -28,7 +28,7 @@ describe("SSH keys Page", () => {
 
         const newUserSSHKey =
           "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDSC/ouD/LqiT1Rd99vDv/MwUmqzJuinLTMTpk5kVy66";
-        jest.spyOn(API, "regenerateUserSSHKey").mockResolvedValueOnce({
+        jest.spyOn(client.api, "regenerateUserSSHKey").mockResolvedValueOnce({
           ...MockGitSSHKey,
           public_key: newUserSSHKey,
         });
@@ -43,7 +43,7 @@ describe("SSH keys Page", () => {
         await screen.findByText("SSH Key regenerated successfully.");
 
         // Check if the API was called correctly
-        expect(API.regenerateUserSSHKey).toBeCalledTimes(1);
+        expect(client.api.regenerateUserSSHKey).toBeCalledTimes(1);
 
         // Check if the SSH key is updated
         await screen.findByText(newUserSSHKey);
@@ -57,7 +57,7 @@ describe("SSH keys Page", () => {
         // Wait to the ssh be rendered on the screen
         await screen.findByText(MockGitSSHKey.public_key);
 
-        jest.spyOn(API, "regenerateUserSSHKey").mockRejectedValueOnce(
+        jest.spyOn(client.api, "regenerateUserSSHKey").mockRejectedValueOnce(
           mockApiError({
             message: SSHKeysPageLanguage.regenerationError,
           }),
@@ -82,7 +82,7 @@ describe("SSH keys Page", () => {
         expect(alert).toHaveTextContent(SSHKeysPageLanguage.regenerationError);
 
         // Check if the API was called correctly
-        expect(API.regenerateUserSSHKey).toBeCalledTimes(1);
+        expect(client.api.regenerateUserSSHKey).toBeCalledTimes(1);
       });
     });
   });
diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx
index f2f0c73c4d7c9..1665ece78b8b4 100644
--- a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx
+++ b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx
@@ -1,6 +1,6 @@
 import { fireEvent, screen, waitFor, within } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import * as API from "api/api";
+import { client } from "api/api";
 import type { OAuthConversionResponse } from "api/typesGenerated";
 import { MockAuthMethodsAll, mockApiError } from "testHelpers/entities";
 import {
@@ -37,29 +37,34 @@ const fillAndSubmitSecurityForm = () => {
 };
 
 beforeEach(() => {
-  jest.spyOn(API, "getAuthMethods").mockResolvedValue(MockAuthMethodsAll);
-  jest.spyOn(API, "getUserLoginType").mockResolvedValue({
+  jest
+    .spyOn(client.api, "getAuthMethods")
+    .mockResolvedValue(MockAuthMethodsAll);
+  jest.spyOn(client.api, "getUserLoginType").mockResolvedValue({
     login_type: "password",
   });
 });
 
 test("update password successfully", async () => {
   jest
-    .spyOn(API, "updateUserPassword")
+    .spyOn(client.api, "updateUserPassword")
     .mockImplementationOnce((_userId, _data) => Promise.resolve(undefined));
   const { user } = await renderPage();
   fillAndSubmitSecurityForm();
 
   const successMessage = await screen.findByText("Updated password.");
   expect(successMessage).toBeDefined();
-  expect(API.updateUserPassword).toBeCalledTimes(1);
-  expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues);
+  expect(client.api.updateUserPassword).toBeCalledTimes(1);
+  expect(client.api.updateUserPassword).toBeCalledWith(
+    user.id,
+    newSecurityFormValues,
+  );
 
   await waitFor(() => expect(window.location).toBeAt("/"));
 });
 
 test("update password with incorrect old password", async () => {
-  jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce(
+  jest.spyOn(client.api, "updateUserPassword").mockRejectedValueOnce(
     mockApiError({
       message: "Incorrect password.",
       validations: [{ detail: "Incorrect password.", field: "old_password" }],
@@ -72,12 +77,15 @@ test("update password with incorrect old password", async () => {
   const errorMessage = await screen.findAllByText("Incorrect password.");
   expect(errorMessage).toBeDefined();
   expect(errorMessage).toHaveLength(2);
-  expect(API.updateUserPassword).toBeCalledTimes(1);
-  expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues);
+  expect(client.api.updateUserPassword).toBeCalledTimes(1);
+  expect(client.api.updateUserPassword).toBeCalledWith(
+    user.id,
+    newSecurityFormValues,
+  );
 });
 
 test("update password with invalid password", async () => {
-  jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce(
+  jest.spyOn(client.api, "updateUserPassword").mockRejectedValueOnce(
     mockApiError({
       message: "Invalid password.",
       validations: [{ detail: "Invalid password.", field: "password" }],
@@ -90,12 +98,15 @@ test("update password with invalid password", async () => {
   const errorMessage = await screen.findAllByText("Invalid password.");
   expect(errorMessage).toBeDefined();
   expect(errorMessage).toHaveLength(2);
-  expect(API.updateUserPassword).toBeCalledTimes(1);
-  expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues);
+  expect(client.api.updateUserPassword).toBeCalledTimes(1);
+  expect(client.api.updateUserPassword).toBeCalledWith(
+    user.id,
+    newSecurityFormValues,
+  );
 });
 
 test("update password when submit returns an unknown error", async () => {
-  jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce({
+  jest.spyOn(client.api, "updateUserPassword").mockRejectedValueOnce({
     data: "unknown error",
   });
 
@@ -104,15 +115,18 @@ test("update password when submit returns an unknown error", async () => {
 
   const errorMessage = await screen.findByText("Something went wrong.");
   expect(errorMessage).toBeDefined();
-  expect(API.updateUserPassword).toBeCalledTimes(1);
-  expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues);
+  expect(client.api.updateUserPassword).toBeCalledTimes(1);
+  expect(client.api.updateUserPassword).toBeCalledWith(
+    user.id,
+    newSecurityFormValues,
+  );
 });
 
 test("change login type to OIDC", async () => {
   const user = userEvent.setup();
   const { user: userData } = await renderPage();
   const convertToOAUTHSpy = jest
-    .spyOn(API, "convertToOAUTH")
+    .spyOn(client.api, "convertToOAUTH")
     .mockResolvedValue({
       state_string: "some-state-string",
       expires_at: "2021-01-01T00:00:00Z",
diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx
index f0191310656c5..afd0a66498d9f 100644
--- a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx
+++ b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx
@@ -1,6 +1,6 @@
 import type { ComponentProps, FC } from "react";
 import { useMutation, useQuery } from "react-query";
-import { getUserLoginType } from "api/api";
+import { client } from "api/api";
 import { authMethods, updatePassword } from "api/queries/users";
 import { displaySuccess } from "components/GlobalSnackbar/utils";
 import { Loader } from "components/Loader/Loader";
@@ -19,7 +19,7 @@ export const SecurityPage: FC = () => {
   const authMethodsQuery = useQuery(authMethods());
   const { data: userLoginType } = useQuery({
     queryKey: ["loginType"],
-    queryFn: getUserLoginType,
+    queryFn: client.api.getUserLoginType,
   });
   const singleSignOnSection = useSingleSignOnSection();
 
diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx
index f2c14dcd45762..4b8bb9c9b11fd 100644
--- a/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx
+++ b/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx
@@ -7,7 +7,7 @@ import Link from "@mui/material/Link";
 import TextField from "@mui/material/TextField";
 import { type FC, useState } from "react";
 import { useMutation } from "react-query";
-import { convertToOAUTH } from "api/api";
+import { client } from "api/api";
 import { getErrorMessage } from "api/errors";
 import type {
   AuthMethods,
@@ -52,7 +52,7 @@ export const useSingleSignOnSection = () => {
   const [loginTypeConfirmation, setLoginTypeConfirmation] =
     useState<LoginTypeConfirmation>({ open: false, selectedType: undefined });
 
-  const mutation = useMutation(convertToOAUTH, {
+  const mutation = useMutation(client.api.convertToOAUTH, {
     onSuccess: (data) => {
       const loginTypeMsg =
         data.to_type === "github" ? "Github" : "OpenID Connect";
diff --git a/site/src/pages/UserSettingsPage/TokensPage/hooks.ts b/site/src/pages/UserSettingsPage/TokensPage/hooks.ts
index a92252ecc8b8a..3e923a89cd5bf 100644
--- a/site/src/pages/UserSettingsPage/TokensPage/hooks.ts
+++ b/site/src/pages/UserSettingsPage/TokensPage/hooks.ts
@@ -4,7 +4,7 @@ import {
   useQuery,
   useQueryClient,
 } from "react-query";
-import { getTokens, deleteToken } from "api/api";
+import { client } from "api/api";
 import type { TokensFilter } from "api/typesGenerated";
 
 // Load all tokens
@@ -13,7 +13,7 @@ export const useTokensData = ({ include_all }: TokensFilter) => {
   const result = useQuery({
     queryKey,
     queryFn: () =>
-      getTokens({
+      client.api.getTokens({
         include_all,
       }),
   });
@@ -29,7 +29,7 @@ export const useDeleteToken = (queryKey: QueryKey) => {
   const queryClient = useQueryClient();
 
   return useMutation({
-    mutationFn: deleteToken,
+    mutationFn: client.api.deleteToken,
     onSuccess: () => {
       // Invalidate and refetch
       void queryClient.invalidateQueries(queryKey);
diff --git a/site/src/pages/UsersPage/UsersPage.test.tsx b/site/src/pages/UsersPage/UsersPage.test.tsx
index 7ed98850dc401..815b36a721b02 100644
--- a/site/src/pages/UsersPage/UsersPage.test.tsx
+++ b/site/src/pages/UsersPage/UsersPage.test.tsx
@@ -1,7 +1,7 @@
 import { fireEvent, screen, within } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import { HttpResponse, http } from "msw";
-import * as API from "api/api";
+import { client } from "api/api";
 import type { Role } from "api/typesGenerated";
 import {
   MockUser,
@@ -261,7 +261,7 @@ describe("UsersPage", () => {
 
         await resetUserPassword(() => {
           jest
-            .spyOn(API, "updateUserPassword")
+            .spyOn(client.api, "updateUserPassword")
             .mockResolvedValueOnce(undefined);
         });
 
@@ -269,8 +269,8 @@ describe("UsersPage", () => {
         await screen.findByText("Successfully updated the user password.");
 
         // Check if the API was called correctly
-        expect(API.updateUserPassword).toBeCalledTimes(1);
-        expect(API.updateUserPassword).toBeCalledWith(MockUser.id, {
+        expect(client.api.updateUserPassword).toBeCalledTimes(1);
+        expect(client.api.updateUserPassword).toBeCalledWith(MockUser.id, {
           password: expect.any(String),
           old_password: "",
         });
@@ -281,15 +281,17 @@ describe("UsersPage", () => {
         renderPage();
 
         await resetUserPassword(() => {
-          jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce({});
+          jest
+            .spyOn(client.api, "updateUserPassword")
+            .mockRejectedValueOnce({});
         });
 
         // Check if the error message is displayed
         await screen.findByText("Error on resetting the user password.");
 
         // Check if the API was called correctly
-        expect(API.updateUserPassword).toBeCalledTimes(1);
-        expect(API.updateUserPassword).toBeCalledWith(MockUser.id, {
+        expect(client.api.updateUserPassword).toBeCalledTimes(1);
+        expect(client.api.updateUserPassword).toBeCalledWith(MockUser.id, {
           password: expect.any(String),
           old_password: "",
         });
diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx
index e358c50954d03..523ef6c808df6 100644
--- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx
+++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx
@@ -1,6 +1,6 @@
 import { screen, waitFor } from "@testing-library/react";
 import WS from "jest-websocket-mock";
-import * as API from "api/api";
+import { client } from "api/api";
 import {
   MockWorkspace,
   MockWorkspaceAgent,
@@ -18,7 +18,7 @@ afterEach(() => {
 describe("WorkspaceBuildPage", () => {
   test("gets the right workspace build", async () => {
     const getWorkspaceBuildSpy = jest
-      .spyOn(API, "getWorkspaceBuildByNumber")
+      .spyOn(client.api, "getWorkspaceBuildByNumber")
       .mockResolvedValue(MockWorkspaceBuild);
     renderWithAuth(<WorkspaceBuildPage />, {
       route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}/builds/${MockWorkspace.latest_build.build_number}`,
diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx
index 13ec3028248bb..0345137b691c0 100644
--- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx
+++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx
@@ -3,7 +3,7 @@ import type { FC } from "react";
 import { Helmet } from "react-helmet-async";
 import { useQuery } from "react-query";
 import { useParams } from "react-router-dom";
-import { getWorkspaceBuilds } from "api/api";
+import { client } from "api/api";
 import { workspaceBuildByNumber } from "api/queries/workspaceBuilds";
 import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs";
 import { pageTitle } from "utils/page";
@@ -26,7 +26,7 @@ export const WorkspaceBuildPage: FC = () => {
   const buildsQuery = useQuery({
     queryKey: ["builds", username, build?.workspace_id],
     queryFn: () => {
-      return getWorkspaceBuilds(build?.workspace_id ?? "", {
+      return client.api.getWorkspaceBuilds(build?.workspace_id ?? "", {
         since: dayjs().add(-30, "day").toISOString(),
       });
     },
diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx
index e203ccd671366..3299e25b1a522 100644
--- a/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx
@@ -5,7 +5,7 @@ import visuallyHidden from "@mui/utils/visuallyHidden";
 import { useFormik } from "formik";
 import type { FC } from "react";
 import { useQuery } from "react-query";
-import { getWorkspaceParameters } from "api/api";
+import { client } from "api/api";
 import type {
   TemplateVersionParameter,
   Workspace,
@@ -49,7 +49,7 @@ export const BuildParametersPopover: FC<BuildParametersPopoverProps> = ({
 }) => {
   const { data: parameters } = useQuery({
     queryKey: ["workspace", workspace.id, "parameters"],
-    queryFn: () => getWorkspaceParameters(workspace),
+    queryFn: () => client.api.getWorkspaceParameters(workspace),
   });
   const ephemeralParameters = parameters
     ? parameters.templateVersionRichParameters.filter((p) => p.ephemeral)
diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx
index 0a69834992638..2ac588f5ce373 100644
--- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx
+++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx
@@ -1,7 +1,7 @@
 import { screen, waitFor, within } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import { HttpResponse, http } from "msw";
-import * as api from "api/api";
+import * as API from "api/api";
 import type { TemplateVersionParameter, Workspace } from "api/typesGenerated";
 import EventSourceMock from "eventsourcemock";
 import {
@@ -22,16 +22,22 @@ import { renderWithAuth } from "testHelpers/renderHelpers";
 import { server } from "testHelpers/server";
 import { WorkspacePage } from "./WorkspacePage";
 
+const { client } = API;
+
 // Renders the workspace page and waits for it be loaded
 const renderWorkspacePage = async (workspace: Workspace) => {
-  jest.spyOn(api, "getWorkspaceByOwnerAndName").mockResolvedValue(workspace);
-  jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate);
-  jest.spyOn(api, "getTemplateVersionRichParameters").mockResolvedValueOnce([]);
   jest
-    .spyOn(api, "getDeploymentConfig")
+    .spyOn(client.api, "getWorkspaceByOwnerAndName")
+    .mockResolvedValue(workspace);
+  jest.spyOn(client.api, "getTemplate").mockResolvedValueOnce(MockTemplate);
+  jest
+    .spyOn(client.api, "getTemplateVersionRichParameters")
+    .mockResolvedValueOnce([]);
+  jest
+    .spyOn(client.api, "getDeploymentConfig")
     .mockResolvedValueOnce(MockDeploymentConfig);
   jest
-    .spyOn(api, "watchWorkspaceAgentLogs")
+    .spyOn(API, "watchWorkspaceAgentLogs")
     .mockImplementation((_, options) => {
       options.onDone?.();
       return new WebSocket("");
@@ -87,7 +93,7 @@ describe("WorkspacePage", () => {
   it("requests a delete job when the user presses Delete and confirms", async () => {
     const user = userEvent.setup({ delay: 0 });
     const deleteWorkspaceMock = jest
-      .spyOn(api, "deleteWorkspace")
+      .spyOn(client.api, "deleteWorkspace")
       .mockResolvedValueOnce(MockWorkspaceBuild);
     await renderWorkspacePage(MockWorkspace);
 
@@ -127,7 +133,7 @@ describe("WorkspacePage", () => {
     );
 
     const deleteWorkspaceMock = jest
-      .spyOn(api, "deleteWorkspace")
+      .spyOn(client.api, "deleteWorkspace")
       .mockResolvedValueOnce(MockWorkspaceBuildDelete);
     await renderWorkspacePage(MockFailedWorkspace);
 
@@ -173,7 +179,7 @@ describe("WorkspacePage", () => {
     );
 
     const startWorkspaceMock = jest
-      .spyOn(api, "startWorkspace")
+      .spyOn(client.api, "startWorkspace")
       .mockImplementation(() => Promise.resolve(MockWorkspaceBuild));
 
     await testButton(MockStoppedWorkspace, "Start", startWorkspaceMock);
@@ -181,7 +187,7 @@ describe("WorkspacePage", () => {
 
   it("requests a stop job when the user presses Stop", async () => {
     const stopWorkspaceMock = jest
-      .spyOn(api, "stopWorkspace")
+      .spyOn(client.api, "stopWorkspace")
       .mockResolvedValueOnce(MockWorkspaceBuild);
 
     await testButton(MockWorkspace, "Stop", stopWorkspaceMock);
@@ -189,7 +195,7 @@ describe("WorkspacePage", () => {
 
   it("requests a stop when the user presses Restart", async () => {
     const stopWorkspaceMock = jest
-      .spyOn(api, "stopWorkspace")
+      .spyOn(client.api, "stopWorkspace")
       .mockResolvedValueOnce(MockWorkspaceBuild);
 
     // Render
@@ -215,7 +221,7 @@ describe("WorkspacePage", () => {
     );
 
     const cancelWorkspaceMock = jest
-      .spyOn(api, "cancelWorkspaceBuild")
+      .spyOn(client.api, "cancelWorkspaceBuild")
       .mockImplementation(() => Promise.resolve({ message: "job canceled" }));
 
     await testButton(MockStartingWorkspace, "Cancel", cancelWorkspaceMock);
@@ -224,11 +230,11 @@ describe("WorkspacePage", () => {
   it("requests an update when the user presses Update", async () => {
     // Mocks
     jest
-      .spyOn(api, "getWorkspaceByOwnerAndName")
+      .spyOn(client.api, "getWorkspaceByOwnerAndName")
       .mockResolvedValueOnce(MockOutdatedWorkspace);
 
     const updateWorkspaceMock = jest
-      .spyOn(api, "updateWorkspace")
+      .spyOn(client.api, "updateWorkspace")
       .mockResolvedValueOnce(MockWorkspaceBuild);
 
     // Render
@@ -249,12 +255,12 @@ describe("WorkspacePage", () => {
   it("updates the parameters when they are missing during update", async () => {
     // Mocks
     jest
-      .spyOn(api, "getWorkspaceByOwnerAndName")
+      .spyOn(client.api, "getWorkspaceByOwnerAndName")
       .mockResolvedValueOnce(MockOutdatedWorkspace);
     const updateWorkspaceSpy = jest
-      .spyOn(api, "updateWorkspace")
+      .spyOn(client.api, "updateWorkspace")
       .mockRejectedValueOnce(
-        new api.MissingBuildParameters(
+        new API.MissingBuildParameters(
           [MockTemplateVersionParameter1, MockTemplateVersionParameter2],
           MockOutdatedWorkspace.template_active_version_id,
         ),
@@ -271,7 +277,7 @@ describe("WorkspacePage", () => {
 
     // The update was called
     await waitFor(() => {
-      expect(api.updateWorkspace).toBeCalled();
+      expect(client.api.updateWorkspace).toBeCalled();
       updateWorkspaceSpy.mockClear();
     });
 
@@ -294,7 +300,7 @@ describe("WorkspacePage", () => {
 
     // Check if the update was called using the values from the form
     await waitFor(() => {
-      expect(api.updateWorkspace).toBeCalledWith(MockOutdatedWorkspace, [
+      expect(client.api.updateWorkspace).toBeCalledWith(MockOutdatedWorkspace, [
         {
           name: MockTemplateVersionParameter1.name,
           value: "some-value",
@@ -309,7 +315,7 @@ describe("WorkspacePage", () => {
 
   it("restart the workspace with one time parameters when having the confirmation dialog", async () => {
     localStorage.removeItem(`${MockUser.id}_ignoredWarnings`);
-    jest.spyOn(api, "getWorkspaceParameters").mockResolvedValue({
+    jest.spyOn(client.api, "getWorkspaceParameters").mockResolvedValue({
       templateVersionRichParameters: [
         {
           ...MockTemplateVersionParameter1,
@@ -321,7 +327,7 @@ describe("WorkspacePage", () => {
       ],
       buildParameters: [{ name: "rebuild", value: "false" }],
     });
-    const restartWorkspaceSpy = jest.spyOn(api, "restartWorkspace");
+    const restartWorkspaceSpy = jest.spyOn(client.api, "restartWorkspace");
     const user = userEvent.setup();
     await renderWorkspacePage(MockWorkspace);
     await user.click(screen.getByTestId("build-parameters-button"));
@@ -351,7 +357,7 @@ describe("WorkspacePage", () => {
     const retryDebugButtonRe = /^Debug$/i;
 
     describe("Retries a failed 'Start' transition", () => {
-      const mockStart = jest.spyOn(api, "startWorkspace");
+      const mockStart = jest.spyOn(client.api, "startWorkspace");
       const failedStart: Workspace = {
         ...MockFailedWorkspace,
         latest_build: {
@@ -384,7 +390,7 @@ describe("WorkspacePage", () => {
     });
 
     describe("Retries a failed 'Stop' transition", () => {
-      const mockStop = jest.spyOn(api, "stopWorkspace");
+      const mockStop = jest.spyOn(client.api, "stopWorkspace");
       const failedStop: Workspace = {
         ...MockFailedWorkspace,
         latest_build: {
@@ -405,7 +411,7 @@ describe("WorkspacePage", () => {
     });
 
     describe("Retries a failed 'Delete' transition", () => {
-      const mockDelete = jest.spyOn(api, "deleteWorkspace");
+      const mockDelete = jest.spyOn(client.api, "deleteWorkspace");
       const failedDelete: Workspace = {
         ...MockFailedWorkspace,
         latest_build: {
@@ -450,7 +456,7 @@ describe("WorkspacePage", () => {
         return HttpResponse.json([parameter]);
       }),
     );
-    const startWorkspaceSpy = jest.spyOn(api, "startWorkspace");
+    const startWorkspaceSpy = jest.spyOn(client.api, "startWorkspace");
 
     await renderWorkspacePage(workspace);
     const retryWithBuildParametersButton = await screen.findByRole("button", {
@@ -496,7 +502,7 @@ describe("WorkspacePage", () => {
         return HttpResponse.json([parameter]);
       }),
     );
-    const startWorkspaceSpy = jest.spyOn(api, "startWorkspace");
+    const startWorkspaceSpy = jest.spyOn(client.api, "startWorkspace");
 
     await renderWorkspacePage(workspace);
     const retryWithBuildParametersButton = await screen.findByRole("button", {
diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx
index 5df3e18bc6fe6..13ed63059fbfc 100644
--- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx
@@ -3,7 +3,7 @@ import { type FC, useEffect, useState } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation, useQuery, useQueryClient } from "react-query";
 import { useNavigate } from "react-router-dom";
-import { MissingBuildParameters, restartWorkspace } from "api/api";
+import { MissingBuildParameters, client } from "api/api";
 import { getErrorMessage } from "api/errors";
 import { buildInfo } from "api/queries/buildInfo";
 import { deploymentConfig, deploymentSSHConfig } from "api/queries/deployment";
@@ -80,7 +80,7 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
   }>({ open: false });
   const { mutate: mutateRestartWorkspace, isLoading: isRestarting } =
     useMutation({
-      mutationFn: restartWorkspace,
+      mutationFn: client.api.restartWorkspace,
     });
 
   // SSH Prefix
diff --git a/site/src/pages/WorkspacePage/WorkspaceScheduleControls.test.tsx b/site/src/pages/WorkspacePage/WorkspaceScheduleControls.test.tsx
index 71e4174499305..19a0ef19fe34a 100644
--- a/site/src/pages/WorkspacePage/WorkspaceScheduleControls.test.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceScheduleControls.test.tsx
@@ -5,7 +5,7 @@ import { HttpResponse, http } from "msw";
 import type { FC } from "react";
 import { QueryClient, QueryClientProvider, useQuery } from "react-query";
 import { RouterProvider, createMemoryRouter } from "react-router-dom";
-import * as API from "api/api";
+import { client } from "api/api";
 import { workspaceByOwnerAndName } from "api/queries/workspaces";
 import { GlobalSnackbar } from "components/GlobalSnackbar/GlobalSnackbar";
 import { ThemeProvider } from "contexts/ThemeProvider";
@@ -62,7 +62,7 @@ const renderScheduleControls = async () => {
 test("add 3 hours to deadline", async () => {
   const user = userEvent.setup();
   const updateDeadlineSpy = jest
-    .spyOn(API, "putWorkspaceExtension")
+    .spyOn(client.api, "putWorkspaceExtension")
     .mockResolvedValue();
 
   await renderScheduleControls();
@@ -91,7 +91,7 @@ test("add 3 hours to deadline", async () => {
 test("remove 2 hours to deadline", async () => {
   const user = userEvent.setup();
   const updateDeadlineSpy = jest
-    .spyOn(API, "putWorkspaceExtension")
+    .spyOn(client.api, "putWorkspaceExtension")
     .mockResolvedValue();
 
   await renderScheduleControls();
@@ -119,7 +119,7 @@ test("remove 2 hours to deadline", async () => {
 test("rollback to previous deadline on error", async () => {
   const user = userEvent.setup();
   const initialScheduleMessage = "Stop in 3 hours";
-  jest.spyOn(API, "putWorkspaceExtension").mockRejectedValue({});
+  jest.spyOn(client.api, "putWorkspaceExtension").mockRejectedValue({});
 
   await renderScheduleControls();
 
@@ -139,7 +139,7 @@ test("rollback to previous deadline on error", async () => {
 test("request is only sent once when clicking multiple times", async () => {
   const user = userEvent.setup();
   const updateDeadlineSpy = jest
-    .spyOn(API, "putWorkspaceExtension")
+    .spyOn(client.api, "putWorkspaceExtension")
     .mockResolvedValue();
 
   await renderScheduleControls();
diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx
index 99b4f692d1db2..1879a28c01564 100644
--- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx
+++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx
@@ -1,6 +1,6 @@
 import { screen, waitFor, within } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import * as api from "api/api";
+import { client } from "api/api";
 import {
   MockWorkspace,
   MockTemplateVersionParameter1,
@@ -20,15 +20,17 @@ import WorkspaceParametersPage from "./WorkspaceParametersPage";
 test("Submit the workspace settings page successfully", async () => {
   // Mock the API calls that loads data
   jest
-    .spyOn(api, "getWorkspaceByOwnerAndName")
+    .spyOn(client.api, "getWorkspaceByOwnerAndName")
     .mockResolvedValueOnce(MockWorkspace);
-  jest.spyOn(api, "getTemplateVersionRichParameters").mockResolvedValueOnce([
-    MockTemplateVersionParameter1,
-    MockTemplateVersionParameter2,
-    // Immutable parameters
-    MockTemplateVersionParameter4,
-  ]);
-  jest.spyOn(api, "getWorkspaceBuildParameters").mockResolvedValueOnce([
+  jest
+    .spyOn(client.api, "getTemplateVersionRichParameters")
+    .mockResolvedValueOnce([
+      MockTemplateVersionParameter1,
+      MockTemplateVersionParameter2,
+      // Immutable parameters
+      MockTemplateVersionParameter4,
+    ]);
+  jest.spyOn(client.api, "getWorkspaceBuildParameters").mockResolvedValueOnce([
     MockWorkspaceBuildParameter1,
     MockWorkspaceBuildParameter2,
     // Immutable value
@@ -36,7 +38,7 @@ test("Submit the workspace settings page successfully", async () => {
   ]);
   // Mock the API calls that submit data
   const postWorkspaceBuildSpy = jest
-    .spyOn(api, "postWorkspaceBuild")
+    .spyOn(client.api, "postWorkspaceBuild")
     .mockResolvedValue(MockWorkspaceBuild);
   // Setup event and rendering
   const user = userEvent.setup();
diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx
index c10accb30e9a0..72dc73d672185 100644
--- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx
+++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx
@@ -4,7 +4,7 @@ import type { FC } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation, useQuery } from "react-query";
 import { useNavigate } from "react-router-dom";
-import { getWorkspaceParameters, postWorkspaceBuild } from "api/api";
+import { client } from "api/api";
 import { isApiValidationError } from "api/errors";
 import { checkAuthorization } from "api/queries/authCheck";
 import { templateByName } from "api/queries/templates";
@@ -29,12 +29,12 @@ const WorkspaceParametersPage: FC = () => {
   const workspace = useWorkspaceSettings();
   const parameters = useQuery({
     queryKey: ["workspace", workspace.id, "parameters"],
-    queryFn: () => getWorkspaceParameters(workspace),
+    queryFn: () => client.api.getWorkspaceParameters(workspace),
   });
   const navigate = useNavigate();
   const updateParameters = useMutation({
     mutationFn: (buildParameters: WorkspaceBuildParameter[]) =>
-      postWorkspaceBuild(workspace.id, {
+      client.api.postWorkspaceBuild(workspace.id, {
         transition: "start",
         rich_parameter_values: buildParameters,
       }),
@@ -93,7 +93,9 @@ const WorkspaceParametersPage: FC = () => {
 export type WorkspaceParametersPageViewProps = {
   workspace: Workspace;
   canChangeVersions: boolean;
-  data: Awaited<ReturnType<typeof getWorkspaceParameters>> | undefined;
+  data:
+    | Awaited<ReturnType<typeof client.api.getWorkspaceParameters>>
+    | undefined;
   submitError: unknown;
   isSubmitting: boolean;
   onSubmit: (formValues: WorkspaceParametersFormValues) => void;
diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.test.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.test.tsx
index 22d6eaa4df141..920c9e4b2e352 100644
--- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.test.tsx
+++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.test.tsx
@@ -1,5 +1,5 @@
 import { screen } from "@testing-library/react";
-import * as API from "api/api";
+import { client } from "api/api";
 import { defaultSchedule } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule";
 import { MockTemplate } from "testHelpers/entities";
 import { render } from "testHelpers/renderHelpers";
@@ -271,7 +271,7 @@ const defaultFormProps: WorkspaceScheduleFormProps = {
 
 describe("templateInheritance", () => {
   it("disables the entire autostart feature appropriately", async () => {
-    jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
+    jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate);
     const props = {
       ...defaultFormProps,
       template: {
@@ -299,7 +299,7 @@ describe("templateInheritance", () => {
   it("disables the autostart days of the week appropriately", async () => {
     const enabledDayLabels = ["Sat", "Sun"];
 
-    jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
+    jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate);
     const props = {
       ...defaultFormProps,
       template: {
@@ -343,7 +343,7 @@ describe("templateInheritance", () => {
         allow_user_autostop: false,
       },
     };
-    jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
+    jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate);
     render(<WorkspaceScheduleForm {...props} />);
 
     const autoStopToggle = await screen.findByLabelText("Enable Autostop");
@@ -355,7 +355,7 @@ describe("templateInheritance", () => {
     expect(ttlInput).toBeDisabled();
   });
   it("disables secondary autostart fields if main feature switch is toggled off", async () => {
-    jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
+    jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate);
     render(
       <WorkspaceScheduleForm
         {...defaultFormProps}
@@ -379,7 +379,7 @@ describe("templateInheritance", () => {
     });
   });
   it("disables secondary autostop fields if main feature switch is toggled off", async () => {
-    jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
+    jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate);
     render(
       <WorkspaceScheduleForm
         {...defaultFormProps}
@@ -398,7 +398,7 @@ describe("templateInheritance", () => {
 });
 
 test("form should be enabled when both auto stop and auto start features are disabled, given that the template permits these actions", async () => {
-  jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
+  jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate);
   render(
     <WorkspaceScheduleForm
       {...defaultFormProps}
@@ -423,7 +423,7 @@ test("form should be disabled when both auto stop and auto start features are di
       allow_user_autostop: false,
     },
   };
-  jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
+  jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate);
   render(<WorkspaceScheduleForm {...props} />);
 
   const submitButton = await screen.findByRole("button", { name: "Submit" });
diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx
index 7830a161c879e..cb5870b3c9db8 100644
--- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx
+++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx
@@ -3,11 +3,7 @@ import { type FC, useState } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation, useQuery, useQueryClient } from "react-query";
 import { useNavigate, useParams } from "react-router-dom";
-import {
-  putWorkspaceAutostart,
-  putWorkspaceAutostop,
-  startWorkspace,
-} from "api/api";
+import { client } from "api/api";
 import { checkAuthorization } from "api/queries/authCheck";
 import { templateByName } from "api/queries/templates";
 import { workspaceByOwnerAndNameKey } from "api/queries/workspaces";
@@ -72,7 +68,10 @@ export const WorkspaceSchedulePage: FC = () => {
   const [isConfirmingApply, setIsConfirmingApply] = useState(false);
   const { mutate: updateWorkspace } = useMutation({
     mutationFn: () =>
-      startWorkspace(workspace.id, workspace.template_active_version_id),
+      client.api.startWorkspace(
+        workspace.id,
+        workspace.template_active_version_id,
+      ),
   });
 
   return (
@@ -167,11 +166,11 @@ const submitSchedule = async (data: SubmitScheduleData) => {
   const actions: Promise<void>[] = [];
 
   if (autostartChanged) {
-    actions.push(putWorkspaceAutostart(workspace.id, autostart));
+    actions.push(client.api.putWorkspaceAutostart(workspace.id, autostart));
   }
 
   if (autostopChanged) {
-    actions.push(putWorkspaceAutostop(workspace.id, ttl));
+    actions.push(client.api.putWorkspaceAutostop(workspace.id, ttl));
   }
 
   return Promise.all(actions);
diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.test.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.test.tsx
index 4fa1bc8a4d536..e2b5acf67e2d8 100644
--- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.test.tsx
+++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.test.tsx
@@ -1,6 +1,6 @@
 import { screen, waitFor, within } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import * as api from "api/api";
+import { client } from "api/api";
 import { MockWorkspace } from "testHelpers/entities";
 import {
   renderWithWorkspaceSettingsLayout,
@@ -11,11 +11,11 @@ import WorkspaceSettingsPage from "./WorkspaceSettingsPage";
 test("Submit the workspace settings page successfully", async () => {
   // Mock the API calls that loads data
   jest
-    .spyOn(api, "getWorkspaceByOwnerAndName")
+    .spyOn(client.api, "getWorkspaceByOwnerAndName")
     .mockResolvedValueOnce({ ...MockWorkspace });
   // Mock the API calls that submit data
   const patchWorkspaceSpy = jest
-    .spyOn(api, "patchWorkspace")
+    .spyOn(client.api, "patchWorkspace")
     .mockResolvedValue();
   // Setup event and rendering
   const user = userEvent.setup();
@@ -43,7 +43,7 @@ test("Submit the workspace settings page successfully", async () => {
 test("Name field is disabled if renames are disabled", async () => {
   // Mock the API calls that loads data
   jest
-    .spyOn(api, "getWorkspaceByOwnerAndName")
+    .spyOn(client.api, "getWorkspaceByOwnerAndName")
     .mockResolvedValueOnce({ ...MockWorkspace, allow_renames: false });
   renderWithWorkspaceSettingsLayout(<WorkspaceSettingsPage />, {
     route: "/@test-user/test-workspace/settings",
diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx
index e289a58c5ce59..e15d429178920 100644
--- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx
+++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx
@@ -2,7 +2,7 @@ import type { FC } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation } from "react-query";
 import { useNavigate, useParams } from "react-router-dom";
-import { patchWorkspace, updateWorkspaceAutomaticUpdates } from "api/api";
+import { client } from "api/api";
 import { displaySuccess } from "components/GlobalSnackbar/utils";
 import { pageTitle } from "utils/page";
 import type { WorkspaceSettingsFormValues } from "./WorkspaceSettingsForm";
@@ -22,8 +22,8 @@ const WorkspaceSettingsPage: FC = () => {
   const mutation = useMutation({
     mutationFn: async (formValues: WorkspaceSettingsFormValues) => {
       await Promise.all([
-        patchWorkspace(workspace.id, { name: formValues.name }),
-        updateWorkspaceAutomaticUpdates(
+        client.api.patchWorkspace(workspace.id, { name: formValues.name }),
+        client.api.updateWorkspaceAutomaticUpdates(
           workspace.id,
           formValues.automatic_updates,
         ),
diff --git a/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx b/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx
index de19212bcbefa..cc49cd091f22d 100644
--- a/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx
+++ b/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx
@@ -7,7 +7,7 @@ import dayjs from "dayjs";
 import relativeTime from "dayjs/plugin/relativeTime";
 import { type FC, type ReactNode, useMemo, useState, useEffect } from "react";
 import { useQueries } from "react-query";
-import { getTemplateVersion } from "api/api";
+import { client } from "api/api";
 import type { TemplateVersion, Workspace } from "api/typesGenerated";
 import { ErrorAlert } from "components/Alert/ErrorAlert";
 import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
@@ -129,7 +129,7 @@ export const BatchUpdateConfirmation: FC<BatchUpdateConfirmationProps> = ({
         // ...but the query _also_ doesn't have everything we need, like the
         // template display name!
         ...version,
-        ...(await getTemplateVersion(version.id)),
+        ...(await client.api.getTemplateVersion(version.id)),
       }),
     })),
   });
diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx
index 5b0dc1b2a959e..196ba291dd942 100644
--- a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx
+++ b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx
@@ -1,7 +1,7 @@
 import { screen, waitFor, within } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import { HttpResponse, http } from "msw";
-import * as API from "api/api";
+import { client } from "api/api";
 import type { Workspace } from "api/typesGenerated";
 import {
   MockStoppedWorkspace,
@@ -58,9 +58,9 @@ describe("WorkspacesPage", () => {
       { ...MockWorkspace, id: "3" },
     ];
     jest
-      .spyOn(API, "getWorkspaces")
+      .spyOn(client.api, "getWorkspaces")
       .mockResolvedValue({ workspaces, count: workspaces.length });
-    const deleteWorkspace = jest.spyOn(API, "deleteWorkspace");
+    const deleteWorkspace = jest.spyOn(client.api, "deleteWorkspace");
     const user = userEvent.setup();
     renderWithAuth(<WorkspacesPage />);
     await waitForLoaderToBeRemoved();
@@ -95,9 +95,9 @@ describe("WorkspacesPage", () => {
         { ...MockOutdatedWorkspace, id: "4" },
       ];
       jest
-        .spyOn(API, "getWorkspaces")
+        .spyOn(client.api, "getWorkspaces")
         .mockResolvedValue({ workspaces, count: workspaces.length });
-      const updateWorkspace = jest.spyOn(API, "updateWorkspace");
+      const updateWorkspace = jest.spyOn(client.api, "updateWorkspace");
       const user = userEvent.setup();
       renderWithAuth(<WorkspacesPage />);
       await waitForLoaderToBeRemoved();
@@ -134,9 +134,9 @@ describe("WorkspacesPage", () => {
         { ...MockOutdatedWorkspace, id: "3" },
       ];
       jest
-        .spyOn(API, "getWorkspaces")
+        .spyOn(client.api, "getWorkspaces")
         .mockResolvedValue({ workspaces, count: workspaces.length });
-      const updateWorkspace = jest.spyOn(API, "updateWorkspace");
+      const updateWorkspace = jest.spyOn(client.api, "updateWorkspace");
       const user = userEvent.setup();
       renderWithAuth(<WorkspacesPage />);
       await waitForLoaderToBeRemoved();
@@ -172,9 +172,9 @@ describe("WorkspacesPage", () => {
         { ...MockOutdatedWorkspace, id: "3" },
       ];
       jest
-        .spyOn(API, "getWorkspaces")
+        .spyOn(client.api, "getWorkspaces")
         .mockResolvedValue({ workspaces, count: workspaces.length });
-      const updateWorkspace = jest.spyOn(API, "updateWorkspace");
+      const updateWorkspace = jest.spyOn(client.api, "updateWorkspace");
       const user = userEvent.setup();
       renderWithAuth(<WorkspacesPage />);
       await waitForLoaderToBeRemoved();
@@ -212,9 +212,9 @@ describe("WorkspacesPage", () => {
         { ...MockWorkspace, id: "5" },
       ];
       jest
-        .spyOn(API, "getWorkspaces")
+        .spyOn(client.api, "getWorkspaces")
         .mockResolvedValue({ workspaces, count: workspaces.length });
-      const updateWorkspace = jest.spyOn(API, "updateWorkspace");
+      const updateWorkspace = jest.spyOn(client.api, "updateWorkspace");
       const user = userEvent.setup();
       renderWithAuth(<WorkspacesPage />);
       await waitForLoaderToBeRemoved();
@@ -254,9 +254,9 @@ describe("WorkspacesPage", () => {
       { ...MockWorkspace, id: "3" },
     ];
     jest
-      .spyOn(API, "getWorkspaces")
+      .spyOn(client.api, "getWorkspaces")
       .mockResolvedValue({ workspaces, count: workspaces.length });
-    const stopWorkspace = jest.spyOn(API, "stopWorkspace");
+    const stopWorkspace = jest.spyOn(client.api, "stopWorkspace");
     const user = userEvent.setup();
     renderWithAuth(<WorkspacesPage />);
     await waitForLoaderToBeRemoved();
@@ -281,9 +281,9 @@ describe("WorkspacesPage", () => {
       { ...MockStoppedWorkspace, id: "3" },
     ];
     jest
-      .spyOn(API, "getWorkspaces")
+      .spyOn(client.api, "getWorkspaces")
       .mockResolvedValue({ workspaces, count: workspaces.length });
-    const startWorkspace = jest.spyOn(API, "startWorkspace");
+    const startWorkspace = jest.spyOn(client.api, "startWorkspace");
     const user = userEvent.setup();
     renderWithAuth(<WorkspacesPage />);
     await waitForLoaderToBeRemoved();
diff --git a/site/src/pages/WorkspacesPage/batchActions.tsx b/site/src/pages/WorkspacesPage/batchActions.tsx
index a9e3eb1cf4c7c..0698c7b2a9c5c 100644
--- a/site/src/pages/WorkspacesPage/batchActions.tsx
+++ b/site/src/pages/WorkspacesPage/batchActions.tsx
@@ -1,12 +1,5 @@
 import { useMutation } from "react-query";
-import {
-  deleteWorkspace,
-  deleteFavoriteWorkspace,
-  putFavoriteWorkspace,
-  startWorkspace,
-  stopWorkspace,
-  updateWorkspace,
-} from "api/api";
+import { client } from "api/api";
 import type { Workspace } from "api/typesGenerated";
 import { displayError } from "components/GlobalSnackbar/utils";
 
@@ -21,7 +14,7 @@ export function useBatchActions(options: UseBatchActionsProps) {
     mutationFn: (workspaces: readonly Workspace[]) => {
       return Promise.all(
         workspaces.map((w) =>
-          startWorkspace(w.id, w.latest_build.template_version_id),
+          client.api.startWorkspace(w.id, w.latest_build.template_version_id),
         ),
       );
     },
@@ -33,7 +26,7 @@ export function useBatchActions(options: UseBatchActionsProps) {
 
   const stopAllMutation = useMutation({
     mutationFn: (workspaces: readonly Workspace[]) => {
-      return Promise.all(workspaces.map((w) => stopWorkspace(w.id)));
+      return Promise.all(workspaces.map((w) => client.api.stopWorkspace(w.id)));
     },
     onSuccess,
     onError: () => {
@@ -43,7 +36,9 @@ export function useBatchActions(options: UseBatchActionsProps) {
 
   const deleteAllMutation = useMutation({
     mutationFn: (workspaces: readonly Workspace[]) => {
-      return Promise.all(workspaces.map((w) => deleteWorkspace(w.id)));
+      return Promise.all(
+        workspaces.map((w) => client.api.deleteWorkspace(w.id)),
+      );
     },
     onSuccess,
     onError: () => {
@@ -56,7 +51,7 @@ export function useBatchActions(options: UseBatchActionsProps) {
       return Promise.all(
         workspaces
           .filter((w) => w.outdated && !w.dormant_at)
-          .map((w) => updateWorkspace(w)),
+          .map((w) => client.api.updateWorkspace(w)),
       );
     },
     onSuccess,
@@ -70,7 +65,7 @@ export function useBatchActions(options: UseBatchActionsProps) {
       return Promise.all(
         workspaces
           .filter((w) => !w.favorite)
-          .map((w) => putFavoriteWorkspace(w.id)),
+          .map((w) => client.api.putFavoriteWorkspace(w.id)),
       );
     },
     onSuccess,
@@ -84,7 +79,7 @@ export function useBatchActions(options: UseBatchActionsProps) {
       return Promise.all(
         workspaces
           .filter((w) => w.favorite)
-          .map((w) => deleteFavoriteWorkspace(w.id)),
+          .map((w) => client.api.deleteFavoriteWorkspace(w.id)),
       );
     },
     onSuccess,
diff --git a/site/src/pages/WorkspacesPage/data.ts b/site/src/pages/WorkspacesPage/data.ts
index a785d00d03122..d6840a35c31fa 100644
--- a/site/src/pages/WorkspacesPage/data.ts
+++ b/site/src/pages/WorkspacesPage/data.ts
@@ -5,7 +5,7 @@ import {
   useQuery,
   useQueryClient,
 } from "react-query";
-import { getWorkspaces, updateWorkspaceVersion } from "api/api";
+import { client } from "api/api";
 import { getErrorMessage } from "api/errors";
 import type {
   Workspace,
@@ -30,7 +30,7 @@ export const useWorkspacesData = ({
   const result = useQuery({
     queryKey,
     queryFn: () =>
-      getWorkspaces({
+      client.api.getWorkspaces({
         q: query,
         limit: limit,
         offset: page <= 0 ? 0 : (page - 1) * limit,
@@ -54,7 +54,7 @@ export const useWorkspaceUpdate = (queryKey: QueryKey) => {
   const queryClient = useQueryClient();
 
   return useMutation({
-    mutationFn: updateWorkspaceVersion,
+    mutationFn: client.api.updateWorkspaceVersion,
     onMutate: async (workspace) => {
       await queryClient.cancelQueries({ queryKey });
       queryClient.setQueryData<WorkspacesResponse>(queryKey, (oldResponse) => {
diff --git a/site/src/pages/WorkspacesPage/filter/menus.ts b/site/src/pages/WorkspacesPage/filter/menus.ts
index a8db56dd5a226..48789d789997c 100644
--- a/site/src/pages/WorkspacesPage/filter/menus.ts
+++ b/site/src/pages/WorkspacesPage/filter/menus.ts
@@ -1,4 +1,4 @@
-import { getTemplates } from "api/api";
+import { client } from "api/api";
 import type { WorkspaceStatus } from "api/typesGenerated";
 import {
   useFilterMenu,
@@ -21,7 +21,7 @@ export const useTemplateFilterMenu = ({
     id: "template",
     getSelectedOption: async () => {
       // Show all templates including deprecated
-      const templates = await getTemplates(organizationId);
+      const templates = await client.api.getTemplates(organizationId);
       const template = templates.find((template) => template.name === value);
       if (template) {
         return {
@@ -37,7 +37,7 @@ export const useTemplateFilterMenu = ({
     },
     getOptions: async (query) => {
       // Show all templates including deprecated
-      const templates = await getTemplates(organizationId);
+      const templates = await client.api.getTemplates(organizationId);
       const filteredTemplates = templates.filter(
         (template) =>
           template.name.toLowerCase().includes(query.toLowerCase()) ||
diff --git a/site/src/utils/terminal.ts b/site/src/utils/terminal.ts
index d27a6efce379c..643847cdcdb26 100644
--- a/site/src/utils/terminal.ts
+++ b/site/src/utils/terminal.ts
@@ -1,4 +1,4 @@
-import * as API from "api/api";
+import { client } from "api/api";
 
 export const terminalWebsocketUrl = async (
   baseUrl: string | undefined,
@@ -30,7 +30,7 @@ export const terminalWebsocketUrl = async (
   }
 
   // Do ticket issuance and set the query parameter.
-  const tokenRes = await API.issueReconnectingPTYSignedToken({
+  const tokenRes = await client.api.issueReconnectingPTYSignedToken({
     url: url.toString(),
     agentID: agentId,
   });

From 654df181eefc251e629c397527900d624755433c Mon Sep 17 00:00:00 2001
From: Parkreiner <throwawayclover@gmail.com>
Date: Tue, 7 May 2024 13:47:55 +0000
Subject: [PATCH 5/7] fix: remove temp comments

---
 site/src/api/api.ts | 8 --------
 1 file changed, 8 deletions(-)

diff --git a/site/src/api/api.ts b/site/src/api/api.ts
index 5a8e4bfb1374f..b2252512366fe 100644
--- a/site/src/api/api.ts
+++ b/site/src/api/api.ts
@@ -25,10 +25,6 @@ import userAgentParser from "ua-parser-js";
 import { delay } from "../utils/delay";
 import * as TypesGen from "./typesGenerated";
 
-////////////////////////////////////////////////////////////////////////////////
-// START OF API FILE
-////////////////////////////////////////////////////////////////////////////////
-
 const getMissingParameters = (
   oldBuildParameters: TypesGen.WorkspaceBuildParameter[],
   newBuildParameters: TypesGen.WorkspaceBuildParameter[],
@@ -1847,10 +1843,6 @@ export class Api {
   };
 }
 
-////////////////////////////////////////////////////////////////////////////////
-// START OF CLIENT FILE
-////////////////////////////////////////////////////////////////////////////////
-
 // This is a hard coded CSRF token/cookie pair for local development. In prod,
 // the GoLang webserver generates a random cookie with a new token for each
 // document request. For local development, we don't use the Go webserver for

From 2e2497374b2b58dbdaac84a376223543e8d2fe60 Mon Sep 17 00:00:00 2001
From: Parkreiner <throwawayclover@gmail.com>
Date: Thu, 9 May 2024 16:23:17 +0000
Subject: [PATCH 6/7] refactor: smoooooooosh the API

---
 site/e2e/api.ts                               |  12 +--
 site/e2e/global.setup.ts                      |   4 +-
 site/e2e/helpers.ts                           |   4 +-
 site/e2e/reporter.ts                          |   4 +-
 site/e2e/tests/deployment/general.spec.ts     |   4 +-
 site/e2e/tests/deployment/network.spec.ts     |   4 +-
 .../tests/deployment/observability.spec.ts    |   4 +-
 site/e2e/tests/deployment/security.spec.ts    |   7 +-
 site/e2e/tests/deployment/userAuth.spec.ts    |   4 +-
 .../tests/deployment/workspaceProxies.spec.ts |   4 +-
 site/e2e/tests/groups/removeMember.spec.ts    |   4 +-
 .../templates/updateTemplateSchedule.spec.ts  |   8 +-
 site/src/api/api.test.ts                      |  70 ++++++------
 site/src/api/api.ts                           | 102 ++++++++++--------
 site/src/api/queries/appearance.ts            |   6 +-
 site/src/api/queries/audits.ts                |   4 +-
 site/src/api/queries/authCheck.ts             |   4 +-
 site/src/api/queries/buildInfo.ts             |   4 +-
 site/src/api/queries/debug.ts                 |  10 +-
 site/src/api/queries/deployment.ts            |  10 +-
 site/src/api/queries/entitlements.ts          |   6 +-
 site/src/api/queries/experiments.ts           |   6 +-
 site/src/api/queries/externalAuth.ts          |  14 +--
 site/src/api/queries/files.ts                 |   6 +-
 site/src/api/queries/groups.ts                |  18 ++--
 site/src/api/queries/insights.ts              |  12 +--
 site/src/api/queries/integrations.ts          |   4 +-
 site/src/api/queries/oauth2.ts                |  20 ++--
 site/src/api/queries/roles.ts                 |   4 +-
 site/src/api/queries/settings.ts              |   6 +-
 site/src/api/queries/sshKeys.ts               |   6 +-
 site/src/api/queries/templates.ts             |  56 +++++-----
 site/src/api/queries/updateCheck.ts           |   4 +-
 site/src/api/queries/users.ts                 |  40 +++----
 site/src/api/queries/workspaceBuilds.ts       |  12 +--
 site/src/api/queries/workspaceQuota.ts        |   6 +-
 site/src/api/queries/workspaceportsharing.ts  |   8 +-
 site/src/api/queries/workspaces.ts            |  36 +++----
 site/src/components/Filter/UserFilter.tsx     |   6 +-
 site/src/contexts/ProxyContext.tsx            |   6 +-
 site/src/contexts/auth/RequireAuth.tsx        |   4 +-
 site/src/contexts/useProxyLatency.ts          |   4 +-
 .../src/modules/resources/AppLink/AppLink.tsx |   4 +-
 .../modules/resources/PortForwardButton.tsx   |   4 +-
 .../VSCodeDesktopButton.tsx                   |   8 +-
 site/src/pages/AuditPage/AuditPage.test.tsx   |  16 ++-
 .../CreateTemplatePage.test.tsx               |  59 +++++-----
 .../CreateTokenPage/CreateTokenPage.test.tsx  |   4 +-
 .../pages/CreateTokenPage/CreateTokenPage.tsx |   6 +-
 .../CreateWorkspacePage.test.tsx              |  72 ++++++-------
 .../CreateWorkspacePage.tsx                   |   4 +-
 .../AddNewLicensePage.tsx                     |   4 +-
 .../LicensesSettingsPage.tsx                  |   6 +-
 .../TemplateEmbedPage.test.tsx                |   4 +-
 .../TemplateEmbedPage/TemplateEmbedPage.tsx   |   4 +-
 .../src/pages/TemplatePage/TemplateLayout.tsx |  11 +-
 .../TemplateSummaryPage.tsx                   |   4 +-
 .../TemplateVersionsPage.tsx                  |   8 +-
 .../useDeletionDialogState.test.ts            |   6 +-
 .../TemplatePage/useDeletionDialogState.ts    |   4 +-
 .../TemplateSettingsPage.test.tsx             |  18 +---
 .../TemplateSettingsPage.tsx                  |   4 +-
 .../TemplateSchedulePage.test.tsx             |  20 ++--
 .../TemplateSchedulePage.tsx                  |   5 +-
 .../TemplateVariablesPage.test.tsx            |  28 ++---
 .../TemplateVersionEditorPage.test.tsx        |  18 ++--
 .../TemplateVersionEditorPage.tsx             |   6 +-
 .../pages/TerminalPage/TerminalPage.test.tsx  |   6 +-
 .../AccountPage/AccountPage.test.tsx          |  50 +++++----
 .../AppearancePage/AppearancePage.test.tsx    |  18 ++--
 .../SSHKeysPage/SSHKeysPage.test.tsx          |  10 +-
 .../SecurityPage/SecurityPage.test.tsx        |  46 +++-----
 .../SecurityPage/SecurityPage.tsx             |   4 +-
 .../SecurityPage/SingleSignOnSection.tsx      |   4 +-
 .../UserSettingsPage/TokensPage/hooks.ts      |   9 +-
 site/src/pages/UsersPage/UsersPage.test.tsx   |  16 ++-
 .../WorkspaceBuildPage.test.tsx               |   4 +-
 .../WorkspaceBuildPage/WorkspaceBuildPage.tsx |   4 +-
 .../BuildParametersPopover.tsx                |   4 +-
 .../WorkspacePage/WorkspacePage.test.tsx      |  58 +++++-----
 .../WorkspacePage/WorkspaceReadyPage.tsx      |   4 +-
 .../WorkspaceScheduleControls.test.tsx        |  10 +-
 .../WorkspaceParametersPage.test.tsx          |  22 ++--
 .../WorkspaceParametersPage.tsx               |  10 +-
 .../WorkspaceScheduleForm.test.tsx            |  16 +--
 .../WorkspaceSchedulePage.tsx                 |  11 +-
 .../WorkspaceSettingsPage.test.tsx            |   8 +-
 .../WorkspaceSettingsPage.tsx                 |   6 +-
 .../BatchUpdateConfirmation.tsx               |   4 +-
 .../WorkspacesPage/WorkspacesPage.test.tsx    |  30 +++---
 .../src/pages/WorkspacesPage/batchActions.tsx |  16 ++-
 site/src/pages/WorkspacesPage/data.ts         |   6 +-
 site/src/pages/WorkspacesPage/filter/menus.ts |   6 +-
 site/src/utils/terminal.ts                    |   4 +-
 94 files changed, 593 insertions(+), 677 deletions(-)

diff --git a/site/e2e/api.ts b/site/e2e/api.ts
index fb442ca7f5bd7..08a25543b0fb6 100644
--- a/site/e2e/api.ts
+++ b/site/e2e/api.ts
@@ -1,7 +1,7 @@
 import type { Page } from "@playwright/test";
 import { expect } from "@playwright/test";
 import { formatDuration, intervalToDuration } from "date-fns";
-import { type DeploymentConfig, client } from "api/api";
+import { type DeploymentConfig, API } from "api/api";
 import type { SerpentOption } from "api/typesGenerated";
 import { coderPort } from "./constants";
 import { findSessionToken, randomName } from "./helpers";
@@ -11,26 +11,26 @@ let currentOrgId: string;
 export const setupApiCalls = async (page: Page) => {
   try {
     const token = await findSessionToken(page);
-    client.setSessionToken(token);
+    API.setSessionToken(token);
   } catch {
     // If this fails, we have an unauthenticated client.
   }
 
-  client.setHost(`http://127.0.0.1:${coderPort}`);
+  API.setHost(`http://127.0.0.1:${coderPort}`);
 };
 
 export const getCurrentOrgId = async (): Promise<string> => {
   if (currentOrgId) {
     return currentOrgId;
   }
-  const currentUser = await client.api.getAuthenticatedUser();
+  const currentUser = await API.getAuthenticatedUser();
   currentOrgId = currentUser.organization_ids[0];
   return currentOrgId;
 };
 
 export const createUser = async (orgId: string) => {
   const name = randomName();
-  const user = await client.api.createUser({
+  const user = await API.createUser({
     email: `${name}@coder.com`,
     username: name,
     password: "s3cure&password!",
@@ -43,7 +43,7 @@ export const createUser = async (orgId: string) => {
 
 export const createGroup = async (orgId: string) => {
   const name = randomName();
-  const group = await client.api.createGroup(orgId, {
+  const group = await API.createGroup(orgId, {
     name,
     display_name: `Display ${name}`,
     avatar_url: "/emojis/1f60d.png",
diff --git a/site/e2e/global.setup.ts b/site/e2e/global.setup.ts
index e577e048ddb27..b23f6bbaa1cd3 100644
--- a/site/e2e/global.setup.ts
+++ b/site/e2e/global.setup.ts
@@ -1,5 +1,5 @@
 import { expect, test } from "@playwright/test";
-import { client } from "api/api";
+import { API } from "api/api";
 import { Language } from "pages/CreateUserPage/CreateUserForm";
 import { setupApiCalls } from "./api";
 import * as constants from "./constants";
@@ -9,7 +9,7 @@ import { storageState } from "./playwright.config";
 test("setup deployment", async ({ page }) => {
   await page.goto("/", { waitUntil: "domcontentloaded" });
   await setupApiCalls(page);
-  const exists = await client.api.hasFirstUser();
+  const exists = await API.hasFirstUser();
   // First user already exists, abort early. All tests execute this as a dependency,
   // if you run multiple tests in the UI, this will fail unless we check this.
   if (exists) {
diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts
index 474b378904371..95c38cf5bf533 100644
--- a/site/e2e/helpers.ts
+++ b/site/e2e/helpers.ts
@@ -6,7 +6,7 @@ import capitalize from "lodash/capitalize";
 import path from "path";
 import * as ssh from "ssh2";
 import { Duplex } from "stream";
-import { client } from "api/api";
+import { API } from "api/api";
 import type {
   WorkspaceBuildParameter,
   UpdateTemplateMeta,
@@ -396,7 +396,7 @@ export const waitUntilUrlIsNotResponding = async (url: string) => {
   const retryIntervalMs = 1000;
   let retries = 0;
 
-  const axiosInstance = client.getAxiosInstance();
+  const axiosInstance = API.getAxiosInstance();
   while (retries < maxRetries) {
     try {
       await axiosInstance.get(url);
diff --git a/site/e2e/reporter.ts b/site/e2e/reporter.ts
index f2410a55859a8..8c9a0d163acc0 100644
--- a/site/e2e/reporter.ts
+++ b/site/e2e/reporter.ts
@@ -10,7 +10,7 @@ import type {
 } from "@playwright/test/reporter";
 import * as fs from "fs/promises";
 import type { Writable } from "stream";
-import { client } from "api/api";
+import { API } from "api/api";
 import { coderdPProfPort, enterpriseLicense } from "./constants";
 
 class CoderReporter implements Reporter {
@@ -143,7 +143,7 @@ const logLines = (chunk: string | Buffer): string[] => {
 };
 
 const exportDebugPprof = async (outputFile: string) => {
-  const axiosInstance = client.getAxiosInstance();
+  const axiosInstance = API.getAxiosInstance();
   const response = await axiosInstance.get(
     `http://127.0.0.1:${coderdPProfPort}/debug/pprof/goroutine?debug=1`,
   );
diff --git a/site/e2e/tests/deployment/general.spec.ts b/site/e2e/tests/deployment/general.spec.ts
index c52ec09b641f0..47e9a22e1a67f 100644
--- a/site/e2e/tests/deployment/general.spec.ts
+++ b/site/e2e/tests/deployment/general.spec.ts
@@ -1,5 +1,5 @@
 import { expect, test } from "@playwright/test";
-import { client } from "api/api";
+import { API } from "api/api";
 import { setupApiCalls } from "../../api";
 import { e2eFakeExperiment1, e2eFakeExperiment2 } from "../../constants";
 
@@ -7,7 +7,7 @@ test("experiments", async ({ page }) => {
   await setupApiCalls(page);
 
   // Load experiments from backend API
-  const availableExperiments = await client.api.getAvailableExperiments();
+  const availableExperiments = await API.getAvailableExperiments();
 
   // Verify if the site lists the same experiments
   await page.goto("/deployment/general", { waitUntil: "networkidle" });
diff --git a/site/e2e/tests/deployment/network.spec.ts b/site/e2e/tests/deployment/network.spec.ts
index 36ea46d7f45ac..d125a100d30bb 100644
--- a/site/e2e/tests/deployment/network.spec.ts
+++ b/site/e2e/tests/deployment/network.spec.ts
@@ -1,5 +1,5 @@
 import { test } from "@playwright/test";
-import { client } from "api/api";
+import { API } from "api/api";
 import {
   setupApiCalls,
   verifyConfigFlagArray,
@@ -11,7 +11,7 @@ import {
 
 test("enabled network settings", async ({ page }) => {
   await setupApiCalls(page);
-  const config = await client.api.getDeploymentConfig();
+  const config = await API.getDeploymentConfig();
 
   await page.goto("/deployment/network", { waitUntil: "domcontentloaded" });
 
diff --git a/site/e2e/tests/deployment/observability.spec.ts b/site/e2e/tests/deployment/observability.spec.ts
index 0268c8baa3ab1..7030ea35081a3 100644
--- a/site/e2e/tests/deployment/observability.spec.ts
+++ b/site/e2e/tests/deployment/observability.spec.ts
@@ -1,5 +1,5 @@
 import { test } from "@playwright/test";
-import { client } from "api/api";
+import { API } from "api/api";
 import {
   setupApiCalls,
   verifyConfigFlagArray,
@@ -11,7 +11,7 @@ import {
 
 test("enabled observability settings", async ({ page }) => {
   await setupApiCalls(page);
-  const config = await client.api.getDeploymentConfig();
+  const config = await API.getDeploymentConfig();
 
   await page.goto("/deployment/observability", {
     waitUntil: "domcontentloaded",
diff --git a/site/e2e/tests/deployment/security.spec.ts b/site/e2e/tests/deployment/security.spec.ts
index e8a79776a8ebc..45675089852e1 100644
--- a/site/e2e/tests/deployment/security.spec.ts
+++ b/site/e2e/tests/deployment/security.spec.ts
@@ -1,7 +1,6 @@
 import type { Page } from "@playwright/test";
 import { expect, test } from "@playwright/test";
-import type * as API from "api/api";
-import { client } from "api/api";
+import { type DeploymentConfig, API } from "api/api";
 import {
   findConfigOption,
   setupApiCalls,
@@ -12,7 +11,7 @@ import {
 
 test("enabled security settings", async ({ page }) => {
   await setupApiCalls(page);
-  const config = await client.api.getDeploymentConfig();
+  const config = await API.getDeploymentConfig();
 
   await page.goto("/deployment/security", { waitUntil: "domcontentloaded" });
 
@@ -31,7 +30,7 @@ test("enabled security settings", async ({ page }) => {
 
 async function verifyStrictTransportSecurity(
   page: Page,
-  config: API.DeploymentConfig,
+  config: DeploymentConfig,
 ) {
   const flag = "strict-transport-security";
   const opt = findConfigOption(config, flag);
diff --git a/site/e2e/tests/deployment/userAuth.spec.ts b/site/e2e/tests/deployment/userAuth.spec.ts
index 8083e4f66537d..8dd8a3af49af7 100644
--- a/site/e2e/tests/deployment/userAuth.spec.ts
+++ b/site/e2e/tests/deployment/userAuth.spec.ts
@@ -1,5 +1,5 @@
 import { test } from "@playwright/test";
-import { client } from "api/api";
+import { API } from "api/api";
 import {
   setupApiCalls,
   verifyConfigFlagArray,
@@ -10,7 +10,7 @@ import {
 
 test("login with OIDC", async ({ page }) => {
   await setupApiCalls(page);
-  const config = await client.api.getDeploymentConfig();
+  const config = await API.getDeploymentConfig();
 
   await page.goto("/deployment/userauth", { waitUntil: "domcontentloaded" });
 
diff --git a/site/e2e/tests/deployment/workspaceProxies.spec.ts b/site/e2e/tests/deployment/workspaceProxies.spec.ts
index ff02853f01b83..47f8d48895466 100644
--- a/site/e2e/tests/deployment/workspaceProxies.spec.ts
+++ b/site/e2e/tests/deployment/workspaceProxies.spec.ts
@@ -1,5 +1,5 @@
 import { test, expect, type Page } from "@playwright/test";
-import { client } from "api/api";
+import { API } from "api/api";
 import { setupApiCalls } from "../../api";
 import { coderPort, workspaceProxyPort } from "../../constants";
 import { randomName, requiresEnterpriseLicense } from "../../helpers";
@@ -34,7 +34,7 @@ test("custom proxy is online", async ({ page }) => {
   const proxyName = randomName();
 
   // Register workspace proxy
-  const proxyResponse = await client.api.createWorkspaceProxy({
+  const proxyResponse = await API.createWorkspaceProxy({
     name: proxyName,
     display_name: "",
     icon: "/emojis/1f1e7-1f1f7.png",
diff --git a/site/e2e/tests/groups/removeMember.spec.ts b/site/e2e/tests/groups/removeMember.spec.ts
index 3d2fb6534e846..468d9d4851441 100644
--- a/site/e2e/tests/groups/removeMember.spec.ts
+++ b/site/e2e/tests/groups/removeMember.spec.ts
@@ -1,5 +1,5 @@
 import { test, expect } from "@playwright/test";
-import { client } from "api/api";
+import { API } from "api/api";
 import {
   createGroup,
   createUser,
@@ -19,7 +19,7 @@ test("remove member", async ({ page, baseURL }) => {
     createGroup(orgId),
     createUser(orgId),
   ]);
-  await client.api.addMember(group.id, member.id);
+  await API.addMember(group.id, member.id);
 
   await page.goto(`${baseURL}/groups/${group.id}`, {
     waitUntil: "domcontentloaded",
diff --git a/site/e2e/tests/templates/updateTemplateSchedule.spec.ts b/site/e2e/tests/templates/updateTemplateSchedule.spec.ts
index 768d41518d7a3..5678f015c917c 100644
--- a/site/e2e/tests/templates/updateTemplateSchedule.spec.ts
+++ b/site/e2e/tests/templates/updateTemplateSchedule.spec.ts
@@ -1,5 +1,5 @@
 import { expect, test } from "@playwright/test";
-import { client } from "api/api";
+import { API } from "api/api";
 import { getCurrentOrgId, setupApiCalls } from "../../api";
 import { beforeCoderTest } from "../../hooks";
 
@@ -11,14 +11,14 @@ test("update template schedule settings without override other settings", async
 }) => {
   await setupApiCalls(page);
   const orgId = await getCurrentOrgId();
-  const templateVersion = await client.api.createTemplateVersion(orgId, {
+  const templateVersion = await API.createTemplateVersion(orgId, {
     storage_method: "file" as const,
     provisioner: "echo",
     user_variable_values: [],
     example_id: "docker",
     tags: {},
   });
-  const template = await client.api.createTemplate(orgId, {
+  const template = await API.createTemplate(orgId, {
     name: "test-template",
     display_name: "Test Template",
     template_version_id: templateVersion.id,
@@ -33,7 +33,7 @@ test("update template schedule settings without override other settings", async
   await page.getByRole("button", { name: "Submit" }).click();
   await expect(page.getByText("Template updated successfully")).toBeVisible();
 
-  const updatedTemplate = await client.api.getTemplate(template.id);
+  const updatedTemplate = await API.getTemplate(template.id);
   // Validate that the template data remains consistent, with the exception of
   // the 'default_ttl_ms' field (updated during the test) and the 'updated at'
   // field (automatically updated by the backend).
diff --git a/site/src/api/api.test.ts b/site/src/api/api.test.ts
index f2547a22cd80c..af5f5e22d61ba 100644
--- a/site/src/api/api.test.ts
+++ b/site/src/api/api.test.ts
@@ -6,10 +6,10 @@ import {
   MockWorkspaceBuild,
   MockWorkspaceBuildParameter1,
 } from "testHelpers/entities";
-import { client, getURLWithSearchParams, MissingBuildParameters } from "./api";
+import { API, getURLWithSearchParams, MissingBuildParameters } from "./api";
 import type * as TypesGen from "./typesGenerated";
 
-const axiosInstance = client.getAxiosInstance();
+const axiosInstance = API.getAxiosInstance();
 
 describe("api.ts", () => {
   describe("login", () => {
@@ -24,7 +24,7 @@ describe("api.ts", () => {
         .mockResolvedValueOnce({ data: loginResponse });
 
       // when
-      const result = await client.api.login("test", "123");
+      const result = await API.login("test", "123");
 
       // then
       expect(axiosInstance.post).toHaveBeenCalled();
@@ -45,7 +45,7 @@ describe("api.ts", () => {
       axiosInstance.post = axiosMockPost;
 
       try {
-        await client.api.login("test", "123");
+        await API.login("test", "123");
       } catch (error) {
         expect(error).toStrictEqual(expectedError);
       }
@@ -61,7 +61,7 @@ describe("api.ts", () => {
       axiosInstance.post = axiosMockPost;
 
       // when
-      await client.api.logout();
+      await API.logout();
 
       // then
       expect(axiosMockPost).toHaveBeenCalled();
@@ -81,7 +81,7 @@ describe("api.ts", () => {
       axiosInstance.post = axiosMockPost;
 
       try {
-        await client.api.logout();
+        await API.logout();
       } catch (error) {
         expect(error).toStrictEqual(expectedError);
       }
@@ -101,7 +101,7 @@ describe("api.ts", () => {
       axiosInstance.post = axiosMockPost;
 
       // when
-      const result = await client.api.getApiKey();
+      const result = await API.getApiKey();
 
       // then
       expect(axiosMockPost).toHaveBeenCalled();
@@ -122,7 +122,7 @@ describe("api.ts", () => {
       axiosInstance.post = axiosMockPost;
 
       try {
-        await client.api.getApiKey();
+        await API.getApiKey();
       } catch (error) {
         expect(error).toStrictEqual(expectedError);
       }
@@ -173,30 +173,25 @@ describe("api.ts", () => {
   describe("update", () => {
     it("creates a build with start and the latest template", async () => {
       jest
-        .spyOn(client.api, "postWorkspaceBuild")
+        .spyOn(API, "postWorkspaceBuild")
         .mockResolvedValueOnce(MockWorkspaceBuild);
-      jest.spyOn(client.api, "getTemplate").mockResolvedValueOnce(MockTemplate);
-      await client.api.updateWorkspace(MockWorkspace);
-      expect(client.api.postWorkspaceBuild).toHaveBeenCalledWith(
-        MockWorkspace.id,
-        {
-          transition: "start",
-          template_version_id: MockTemplate.active_version_id,
-          rich_parameter_values: [],
-        },
-      );
+      jest.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate);
+      await API.updateWorkspace(MockWorkspace);
+      expect(API.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, {
+        transition: "start",
+        template_version_id: MockTemplate.active_version_id,
+        rich_parameter_values: [],
+      });
     });
 
     it("fails when having missing parameters", async () => {
       jest
-        .spyOn(client.api, "postWorkspaceBuild")
+        .spyOn(API, "postWorkspaceBuild")
         .mockResolvedValue(MockWorkspaceBuild);
-      jest.spyOn(client.api, "getTemplate").mockResolvedValue(MockTemplate);
-      jest
-        .spyOn(client.api, "getWorkspaceBuildParameters")
-        .mockResolvedValue([]);
+      jest.spyOn(API, "getTemplate").mockResolvedValue(MockTemplate);
+      jest.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValue([]);
       jest
-        .spyOn(client.api, "getTemplateVersionRichParameters")
+        .spyOn(API, "getTemplateVersionRichParameters")
         .mockResolvedValue([
           MockTemplateVersionParameter1,
           { ...MockTemplateVersionParameter2, mutable: false },
@@ -204,7 +199,7 @@ describe("api.ts", () => {
 
       let error = new Error();
       try {
-        await client.api.updateWorkspace(MockWorkspace);
+        await API.updateWorkspace(MockWorkspace);
       } catch (e) {
         error = e as Error;
       }
@@ -219,26 +214,23 @@ describe("api.ts", () => {
 
     it("creates a build with the no parameters if it is already filled", async () => {
       jest
-        .spyOn(client.api, "postWorkspaceBuild")
+        .spyOn(API, "postWorkspaceBuild")
         .mockResolvedValueOnce(MockWorkspaceBuild);
-      jest.spyOn(client.api, "getTemplate").mockResolvedValueOnce(MockTemplate);
+      jest.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate);
       jest
-        .spyOn(client.api, "getWorkspaceBuildParameters")
+        .spyOn(API, "getWorkspaceBuildParameters")
         .mockResolvedValue([MockWorkspaceBuildParameter1]);
       jest
-        .spyOn(client.api, "getTemplateVersionRichParameters")
+        .spyOn(API, "getTemplateVersionRichParameters")
         .mockResolvedValue([
           { ...MockTemplateVersionParameter1, required: true, mutable: false },
         ]);
-      await client.api.updateWorkspace(MockWorkspace);
-      expect(client.api.postWorkspaceBuild).toHaveBeenCalledWith(
-        MockWorkspace.id,
-        {
-          transition: "start",
-          template_version_id: MockTemplate.active_version_id,
-          rich_parameter_values: [],
-        },
-      );
+      await API.updateWorkspace(MockWorkspace);
+      expect(API.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, {
+        transition: "start",
+        template_version_id: MockTemplate.active_version_id,
+        rich_parameter_values: [],
+      });
     });
   });
 });
diff --git a/site/src/api/api.ts b/site/src/api/api.ts
index b2252512366fe..9bc747f1c17f6 100644
--- a/site/src/api/api.ts
+++ b/site/src/api/api.ts
@@ -367,8 +367,21 @@ export class MissingBuildParameters extends Error {
   }
 }
 
-export class Api {
-  constructor(private readonly axios: AxiosInstance) {}
+/**
+ * This is the container for all API methods. It's split off to make it more
+ * clear where API methods should go, but it is eventually merged into the Api
+ * class with a more flat hierarchy
+ *
+ * All public methods should be defined as arrow functions to ensure that they
+ * can be passed around the React UI without losing their `this` context.
+ *
+ * This is one of the few cases where you have to worry about the difference
+ * between traditional methods and arrow function properties. Arrow functions
+ * disable JS's dynamic scope, and force all `this` references to resolve via
+ * lexical scope.
+ */
+class ApiMethods {
+  constructor(protected readonly axios: AxiosInstance) {}
 
   login = async (
     email: string,
@@ -1858,56 +1871,57 @@ const tokenMetadataElement =
     ? document.head.querySelector('meta[property="csrf-token"]')
     : null;
 
-interface ClientApi {
-  api: Api;
+function getConfiguredAxiosInstance(): AxiosInstance {
+  const instance = globalAxios.create();
+
+  // Adds 304 for the default axios validateStatus function
+  // https://github.com/axios/axios#handling-errors Check status here
+  // https://httpstatusdogs.com/
+  instance.defaults.validateStatus = (status) => {
+    return (status >= 200 && status < 300) || status === 304;
+  };
+
+  const metadataIsAvailable =
+    tokenMetadataElement !== null &&
+    tokenMetadataElement.getAttribute("content") !== null;
+
+  if (metadataIsAvailable) {
+    if (process.env.NODE_ENV === "development") {
+      // Development mode uses a hard-coded CSRF token
+      instance.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken;
+      instance.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken;
+      tokenMetadataElement.setAttribute("content", csrfToken);
+    } else {
+      instance.defaults.headers.common["X-CSRF-TOKEN"] =
+        tokenMetadataElement.getAttribute("content") ?? "";
+    }
+  } else {
+    // Do not write error logs if we are in a FE unit test.
+    if (process.env.JEST_WORKER_ID === undefined) {
+      console.error("CSRF token not found");
+    }
+  }
+
+  return instance;
+}
+
+// Other non-API methods defined here to make it a little easier to find them.
+interface ClientApi extends ApiMethods {
   getCsrfToken: () => string;
   setSessionToken: (token: string) => void;
   setHost: (host: string | undefined) => void;
   getAxiosInstance: () => AxiosInstance;
 }
 
-export class Client implements ClientApi {
-  private readonly axios: AxiosInstance;
-  readonly api: Api;
-
+export class Api extends ApiMethods implements ClientApi {
   constructor() {
-    this.axios = this.getConfiguredAxiosInstance();
-    this.api = new Api(this.axios);
+    const scopedAxiosInstance = getConfiguredAxiosInstance();
+    super(scopedAxiosInstance);
   }
 
-  private getConfiguredAxiosInstance(): AxiosInstance {
-    const axios = globalAxios.create();
-
-    // Adds 304 for the default axios validateStatus function
-    // https://github.com/axios/axios#handling-errors Check status here
-    // https://httpstatusdogs.com/
-    axios.defaults.validateStatus = (status) => {
-      return (status >= 200 && status < 300) || status === 304;
-    };
-
-    const metadataIsAvailable =
-      tokenMetadataElement !== null &&
-      tokenMetadataElement.getAttribute("content") !== null;
-
-    if (metadataIsAvailable) {
-      if (process.env.NODE_ENV === "development") {
-        // Development mode uses a hard-coded CSRF token
-        this.axios.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken;
-        axios.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken;
-        tokenMetadataElement.setAttribute("content", csrfToken);
-      } else {
-        axios.defaults.headers.common["X-CSRF-TOKEN"] =
-          tokenMetadataElement.getAttribute("content") ?? "";
-      }
-    } else {
-      // Do not write error logs if we are in a FE unit test.
-      if (process.env.JEST_WORKER_ID === undefined) {
-        console.error("CSRF token not found");
-      }
-    }
-
-    return axios;
-  }
+  // As with ApiMethods, all public methods should be defined with arrow
+  // function syntax to ensure they can be passed around the React UI without
+  // losing/detaching their `this` context!
 
   getCsrfToken = (): string => {
     return csrfToken;
@@ -1926,4 +1940,4 @@ export class Client implements ClientApi {
   };
 }
 
-export const client = new Client();
+export const API = new Api();
diff --git a/site/src/api/queries/appearance.ts b/site/src/api/queries/appearance.ts
index de867f72cb09e..8819d45fdfff9 100644
--- a/site/src/api/queries/appearance.ts
+++ b/site/src/api/queries/appearance.ts
@@ -1,5 +1,5 @@
 import type { QueryClient } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import type { AppearanceConfig } from "api/typesGenerated";
 import type { MetadataState } from "hooks/useEmbeddedMetadata";
 import { cachedQuery } from "./util";
@@ -10,13 +10,13 @@ export const appearance = (metadata: MetadataState<AppearanceConfig>) => {
   return cachedQuery({
     metadata,
     queryKey: ["appearance"],
-    queryFn: () => client.api.getAppearance(),
+    queryFn: () => API.getAppearance(),
   });
 };
 
 export const updateAppearance = (queryClient: QueryClient) => {
   return {
-    mutationFn: client.api.updateAppearance,
+    mutationFn: API.updateAppearance,
     onSuccess: (newConfig: AppearanceConfig) => {
       queryClient.setQueryData(appearanceConfigKey, newConfig);
     },
diff --git a/site/src/api/queries/audits.ts b/site/src/api/queries/audits.ts
index 22326c1c8d4a8..1dce9a29eaab8 100644
--- a/site/src/api/queries/audits.ts
+++ b/site/src/api/queries/audits.ts
@@ -1,4 +1,4 @@
-import { client } from "api/api";
+import { API } from "api/api";
 import type { AuditLogResponse } from "api/typesGenerated";
 import { useFilterParamsKey } from "components/Filter/filter";
 import type { UsePaginatedQueryOptions } from "hooks/usePaginatedQuery";
@@ -13,7 +13,7 @@ export function paginatedAudits(
       return ["auditLogs", payload, pageNumber] as const;
     },
     queryFn: ({ payload, limit, offset }) => {
-      return client.api.getAuditLogs({
+      return API.getAuditLogs({
         offset,
         limit,
         q: payload,
diff --git a/site/src/api/queries/authCheck.ts b/site/src/api/queries/authCheck.ts
index 470685f354789..3248f35357f25 100644
--- a/site/src/api/queries/authCheck.ts
+++ b/site/src/api/queries/authCheck.ts
@@ -1,4 +1,4 @@
-import { client } from "api/api";
+import { API } from "api/api";
 import type { AuthorizationRequest } from "api/typesGenerated";
 
 export const AUTHORIZATION_KEY = "authorization";
@@ -9,6 +9,6 @@ export const getAuthorizationKey = (req: AuthorizationRequest) =>
 export const checkAuthorization = (req: AuthorizationRequest) => {
   return {
     queryKey: getAuthorizationKey(req),
-    queryFn: () => client.api.checkAuthorization(req),
+    queryFn: () => API.checkAuthorization(req),
   };
 };
diff --git a/site/src/api/queries/buildInfo.ts b/site/src/api/queries/buildInfo.ts
index b4152904d7e14..43dac7d20334f 100644
--- a/site/src/api/queries/buildInfo.ts
+++ b/site/src/api/queries/buildInfo.ts
@@ -1,4 +1,4 @@
-import { client } from "api/api";
+import { API } from "api/api";
 import type { BuildInfoResponse } from "api/typesGenerated";
 import type { MetadataState } from "hooks/useEmbeddedMetadata";
 import { cachedQuery } from "./util";
@@ -10,6 +10,6 @@ export const buildInfo = (metadata: MetadataState<BuildInfoResponse>) => {
   return cachedQuery({
     metadata,
     queryKey: buildInfoKey,
-    queryFn: () => client.api.getBuildInfo(),
+    queryFn: () => API.getBuildInfo(),
   });
 };
diff --git a/site/src/api/queries/debug.ts b/site/src/api/queries/debug.ts
index 5cd4f40c7b635..b84fdf1b7c2fb 100644
--- a/site/src/api/queries/debug.ts
+++ b/site/src/api/queries/debug.ts
@@ -1,5 +1,5 @@
 import type { QueryClient, UseMutationOptions } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import type { HealthSettings, UpdateHealthSettings } from "api/typesGenerated";
 
 export const HEALTH_QUERY_KEY = ["health"];
@@ -7,14 +7,14 @@ export const HEALTH_QUERY_SETTINGS_KEY = ["health", "settings"];
 
 export const health = () => ({
   queryKey: HEALTH_QUERY_KEY,
-  queryFn: async () => client.api.getHealth(),
+  queryFn: async () => API.getHealth(),
 });
 
 export const refreshHealth = (queryClient: QueryClient) => {
   return {
     mutationFn: async () => {
       await queryClient.cancelQueries(HEALTH_QUERY_KEY);
-      const newHealthData = await client.api.getHealth(true);
+      const newHealthData = await API.getHealth(true);
       queryClient.setQueryData(HEALTH_QUERY_KEY, newHealthData);
     },
   };
@@ -23,7 +23,7 @@ export const refreshHealth = (queryClient: QueryClient) => {
 export const healthSettings = () => {
   return {
     queryKey: HEALTH_QUERY_SETTINGS_KEY,
-    queryFn: client.api.getHealthSettings,
+    queryFn: API.getHealthSettings,
   };
 };
 
@@ -36,7 +36,7 @@ export const updateHealthSettings = (
   unknown
 > => {
   return {
-    mutationFn: client.api.updateHealthSettings,
+    mutationFn: API.updateHealthSettings,
     onSuccess: async (_, newSettings) => {
       await queryClient.invalidateQueries(HEALTH_QUERY_KEY);
       queryClient.setQueryData(HEALTH_QUERY_SETTINGS_KEY, newSettings);
diff --git a/site/src/api/queries/deployment.ts b/site/src/api/queries/deployment.ts
index e4ef76c623b34..fa4d37967af18 100644
--- a/site/src/api/queries/deployment.ts
+++ b/site/src/api/queries/deployment.ts
@@ -1,29 +1,29 @@
-import { client } from "api/api";
+import { API } from "api/api";
 
 export const deploymentConfig = () => {
   return {
     queryKey: ["deployment", "config"],
-    queryFn: client.api.getDeploymentConfig,
+    queryFn: API.getDeploymentConfig,
   };
 };
 
 export const deploymentDAUs = () => {
   return {
     queryKey: ["deployment", "daus"],
-    queryFn: () => client.api.getDeploymentDAUs(),
+    queryFn: () => API.getDeploymentDAUs(),
   };
 };
 
 export const deploymentStats = () => {
   return {
     queryKey: ["deployment", "stats"],
-    queryFn: client.api.getDeploymentStats,
+    queryFn: API.getDeploymentStats,
   };
 };
 
 export const deploymentSSHConfig = () => {
   return {
     queryKey: ["deployment", "sshConfig"],
-    queryFn: client.api.getDeploymentSSHConfig,
+    queryFn: API.getDeploymentSSHConfig,
   };
 };
diff --git a/site/src/api/queries/entitlements.ts b/site/src/api/queries/entitlements.ts
index a3b3d2201c28c..542aa6f0cf591 100644
--- a/site/src/api/queries/entitlements.ts
+++ b/site/src/api/queries/entitlements.ts
@@ -1,5 +1,5 @@
 import type { QueryClient } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import type { Entitlements } from "api/typesGenerated";
 import type { MetadataState } from "hooks/useEmbeddedMetadata";
 import { cachedQuery } from "./util";
@@ -10,13 +10,13 @@ export const entitlements = (metadata: MetadataState<Entitlements>) => {
   return cachedQuery({
     metadata,
     queryKey: entitlementsQueryKey,
-    queryFn: () => client.api.getEntitlements(),
+    queryFn: () => API.getEntitlements(),
   });
 };
 
 export const refreshEntitlements = (queryClient: QueryClient) => {
   return {
-    mutationFn: client.api.refreshEntitlements,
+    mutationFn: API.refreshEntitlements,
     onSuccess: async () => {
       await queryClient.invalidateQueries({
         queryKey: entitlementsQueryKey,
diff --git a/site/src/api/queries/experiments.ts b/site/src/api/queries/experiments.ts
index fd81196a906b6..86fd9096ae9f2 100644
--- a/site/src/api/queries/experiments.ts
+++ b/site/src/api/queries/experiments.ts
@@ -1,4 +1,4 @@
-import { client } from "api/api";
+import { API } from "api/api";
 import type { Experiments } from "api/typesGenerated";
 import type { MetadataState } from "hooks/useEmbeddedMetadata";
 import { cachedQuery } from "./util";
@@ -9,13 +9,13 @@ export const experiments = (metadata: MetadataState<Experiments>) => {
   return cachedQuery({
     metadata,
     queryKey: experimentsKey,
-    queryFn: () => client.api.getExperiments(),
+    queryFn: () => API.getExperiments(),
   });
 };
 
 export const availableExperiments = () => {
   return {
     queryKey: ["availableExperiments"],
-    queryFn: async () => client.api.getAvailableExperiments(),
+    queryFn: async () => API.getAvailableExperiments(),
   };
 };
diff --git a/site/src/api/queries/externalAuth.ts b/site/src/api/queries/externalAuth.ts
index d3d905d9844d3..eda68713aa5fc 100644
--- a/site/src/api/queries/externalAuth.ts
+++ b/site/src/api/queries/externalAuth.ts
@@ -1,25 +1,25 @@
 import type { QueryClient, UseMutationOptions } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import type { ExternalAuth } from "api/typesGenerated";
 
 // Returns all configured external auths for a given user.
 export const externalAuths = () => {
   return {
     queryKey: ["external-auth"],
-    queryFn: () => client.api.getUserExternalAuthProviders(),
+    queryFn: () => API.getUserExternalAuthProviders(),
   };
 };
 
 export const externalAuthProvider = (providerId: string) => {
   return {
     queryKey: ["external-auth", providerId],
-    queryFn: () => client.api.getExternalAuthProvider(providerId),
+    queryFn: () => API.getExternalAuthProvider(providerId),
   };
 };
 
 export const externalAuthDevice = (providerId: string) => {
   return {
-    queryFn: () => client.api.getExternalAuthDevice(providerId),
+    queryFn: () => API.getExternalAuthDevice(providerId),
     queryKey: ["external-auth", providerId, "device"],
   };
 };
@@ -31,7 +31,7 @@ export const exchangeExternalAuthDevice = (
 ) => {
   return {
     queryFn: () =>
-      client.api.exchangeExternalAuthDevice(providerId, {
+      API.exchangeExternalAuthDevice(providerId, {
         device_code: deviceCode,
       }),
     queryKey: ["external-auth", providerId, "device", deviceCode],
@@ -46,7 +46,7 @@ export const validateExternalAuth = (
   queryClient: QueryClient,
 ): UseMutationOptions<ExternalAuth, unknown, string> => {
   return {
-    mutationFn: client.api.getExternalAuthProvider,
+    mutationFn: API.getExternalAuthProvider,
     onSuccess: (data, providerId) => {
       queryClient.setQueryData(["external-auth", providerId], data);
     },
@@ -55,7 +55,7 @@ export const validateExternalAuth = (
 
 export const unlinkExternalAuths = (queryClient: QueryClient) => {
   return {
-    mutationFn: client.api.unlinkExternalAuthProvider,
+    mutationFn: API.unlinkExternalAuthProvider,
     onSuccess: async () => {
       await queryClient.invalidateQueries(["external-auth"]);
     },
diff --git a/site/src/api/queries/files.ts b/site/src/api/queries/files.ts
index c8e30e3b9bd0d..a363e03f94473 100644
--- a/site/src/api/queries/files.ts
+++ b/site/src/api/queries/files.ts
@@ -1,14 +1,14 @@
-import { client } from "api/api";
+import { API } from "api/api";
 
 export const uploadFile = () => {
   return {
-    mutationFn: client.api.uploadFile,
+    mutationFn: API.uploadFile,
   };
 };
 
 export const file = (fileId: string) => {
   return {
     queryKey: ["files", fileId],
-    queryFn: () => client.api.getFile(fileId),
+    queryFn: () => API.getFile(fileId),
   };
 };
diff --git a/site/src/api/queries/groups.ts b/site/src/api/queries/groups.ts
index 1bd4d1554ca1b..5c34758df069f 100644
--- a/site/src/api/queries/groups.ts
+++ b/site/src/api/queries/groups.ts
@@ -1,5 +1,5 @@
 import type { QueryClient, UseQueryOptions } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import type {
   CreateGroupRequest,
   Group,
@@ -14,14 +14,14 @@ const getGroupQueryKey = (groupId: string) => ["group", groupId];
 export const groups = (organizationId: string) => {
   return {
     queryKey: GROUPS_QUERY_KEY,
-    queryFn: () => client.api.getGroups(organizationId),
+    queryFn: () => API.getGroups(organizationId),
   } satisfies UseQueryOptions<Group[]>;
 };
 
 export const group = (groupId: string) => {
   return {
     queryKey: getGroupQueryKey(groupId),
-    queryFn: () => client.api.getGroup(groupId),
+    queryFn: () => API.getGroup(groupId),
   };
 };
 
@@ -71,7 +71,7 @@ export const groupPermissions = (groupId: string) => {
   return {
     queryKey: [...getGroupQueryKey(groupId), "permissions"],
     queryFn: () =>
-      client.api.checkAuthorization({
+      API.checkAuthorization({
         checks: {
           canUpdateGroup: {
             object: {
@@ -91,7 +91,7 @@ export const createGroup = (queryClient: QueryClient) => {
       organizationId,
       ...request
     }: CreateGroupRequest & { organizationId: string }) =>
-      client.api.createGroup(organizationId, request),
+      API.createGroup(organizationId, request),
     onSuccess: async () => {
       await queryClient.invalidateQueries(GROUPS_QUERY_KEY);
     },
@@ -104,7 +104,7 @@ export const patchGroup = (queryClient: QueryClient) => {
       groupId,
       ...request
     }: PatchGroupRequest & { groupId: string }) =>
-      client.api.patchGroup(groupId, request),
+      API.patchGroup(groupId, request),
     onSuccess: async (updatedGroup: Group) =>
       invalidateGroup(queryClient, updatedGroup.id),
   };
@@ -112,7 +112,7 @@ export const patchGroup = (queryClient: QueryClient) => {
 
 export const deleteGroup = (queryClient: QueryClient) => {
   return {
-    mutationFn: client.api.deleteGroup,
+    mutationFn: API.deleteGroup,
     onSuccess: async (_: void, groupId: string) =>
       invalidateGroup(queryClient, groupId),
   };
@@ -121,7 +121,7 @@ export const deleteGroup = (queryClient: QueryClient) => {
 export const addMember = (queryClient: QueryClient) => {
   return {
     mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) =>
-      client.api.addMember(groupId, userId),
+      API.addMember(groupId, userId),
     onSuccess: async (updatedGroup: Group) =>
       invalidateGroup(queryClient, updatedGroup.id),
   };
@@ -130,7 +130,7 @@ export const addMember = (queryClient: QueryClient) => {
 export const removeMember = (queryClient: QueryClient) => {
   return {
     mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) =>
-      client.api.removeMember(groupId, userId),
+      API.removeMember(groupId, userId),
     onSuccess: async (updatedGroup: Group) =>
       invalidateGroup(queryClient, updatedGroup.id),
   };
diff --git a/site/src/api/queries/insights.ts b/site/src/api/queries/insights.ts
index 7e2f5bd5b9c0a..4b6dad8cd2fc8 100644
--- a/site/src/api/queries/insights.ts
+++ b/site/src/api/queries/insights.ts
@@ -1,26 +1,22 @@
-import {
-  type InsightsParams,
-  type InsightsTemplateParams,
-  client,
-} from "api/api";
+import { type InsightsParams, type InsightsTemplateParams, API } from "api/api";
 
 export const insightsTemplate = (params: InsightsTemplateParams) => {
   return {
     queryKey: ["insights", "templates", params.template_ids, params],
-    queryFn: () => client.api.getInsightsTemplate(params),
+    queryFn: () => API.getInsightsTemplate(params),
   };
 };
 
 export const insightsUserLatency = (params: InsightsParams) => {
   return {
     queryKey: ["insights", "userLatency", params.template_ids, params],
-    queryFn: () => client.api.getInsightsUserLatency(params),
+    queryFn: () => API.getInsightsUserLatency(params),
   };
 };
 
 export const insightsUserActivity = (params: InsightsParams) => {
   return {
     queryKey: ["insights", "userActivity", params.template_ids, params],
-    queryFn: () => client.api.getInsightsUserActivity(params),
+    queryFn: () => API.getInsightsUserActivity(params),
   };
 };
diff --git a/site/src/api/queries/integrations.ts b/site/src/api/queries/integrations.ts
index 0e26194b80b22..c0e7f6f28ce9d 100644
--- a/site/src/api/queries/integrations.ts
+++ b/site/src/api/queries/integrations.ts
@@ -1,9 +1,9 @@
 import type { GetJFrogXRayScanParams } from "api/api";
-import { client } from "api/api";
+import { API } from "api/api";
 
 export const xrayScan = (params: GetJFrogXRayScanParams) => {
   return {
     queryKey: ["xray", params],
-    queryFn: () => client.api.getJFrogXRayScan(params),
+    queryFn: () => API.getJFrogXRayScan(params),
   };
 };
diff --git a/site/src/api/queries/oauth2.ts b/site/src/api/queries/oauth2.ts
index 06f8f9d9b4097..26334955c4a86 100644
--- a/site/src/api/queries/oauth2.ts
+++ b/site/src/api/queries/oauth2.ts
@@ -1,5 +1,5 @@
 import type { QueryClient } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import type * as TypesGen from "api/typesGenerated";
 
 const appsKey = ["oauth2-provider", "apps"];
@@ -10,20 +10,20 @@ const appSecretsKey = (appId: string) => appKey(appId).concat("secrets");
 export const getApps = (userId?: string) => {
   return {
     queryKey: userId ? appsKey.concat(userId) : appsKey,
-    queryFn: () => client.api.getOAuth2ProviderApps({ user_id: userId }),
+    queryFn: () => API.getOAuth2ProviderApps({ user_id: userId }),
   };
 };
 
 export const getApp = (id: string) => {
   return {
     queryKey: appKey(id),
-    queryFn: () => client.api.getOAuth2ProviderApp(id),
+    queryFn: () => API.getOAuth2ProviderApp(id),
   };
 };
 
 export const postApp = (queryClient: QueryClient) => {
   return {
-    mutationFn: client.api.postOAuth2ProviderApp,
+    mutationFn: API.postOAuth2ProviderApp,
     onSuccess: async () => {
       await queryClient.invalidateQueries({
         queryKey: appsKey,
@@ -40,7 +40,7 @@ export const putApp = (queryClient: QueryClient) => {
     }: {
       id: string;
       req: TypesGen.PutOAuth2ProviderAppRequest;
-    }) => client.api.putOAuth2ProviderApp(id, req),
+    }) => API.putOAuth2ProviderApp(id, req),
     onSuccess: async (app: TypesGen.OAuth2ProviderApp) => {
       await queryClient.invalidateQueries({
         queryKey: appKey(app.id),
@@ -51,7 +51,7 @@ export const putApp = (queryClient: QueryClient) => {
 
 export const deleteApp = (queryClient: QueryClient) => {
   return {
-    mutationFn: client.api.deleteOAuth2ProviderApp,
+    mutationFn: API.deleteOAuth2ProviderApp,
     onSuccess: async () => {
       await queryClient.invalidateQueries({
         queryKey: appsKey,
@@ -63,13 +63,13 @@ export const deleteApp = (queryClient: QueryClient) => {
 export const getAppSecrets = (id: string) => {
   return {
     queryKey: appSecretsKey(id),
-    queryFn: () => client.api.getOAuth2ProviderAppSecrets(id),
+    queryFn: () => API.getOAuth2ProviderAppSecrets(id),
   };
 };
 
 export const postAppSecret = (queryClient: QueryClient) => {
   return {
-    mutationFn: client.api.postOAuth2ProviderAppSecret,
+    mutationFn: API.postOAuth2ProviderAppSecret,
     onSuccess: async (
       _: TypesGen.OAuth2ProviderAppSecretFull,
       appId: string,
@@ -84,7 +84,7 @@ export const postAppSecret = (queryClient: QueryClient) => {
 export const deleteAppSecret = (queryClient: QueryClient) => {
   return {
     mutationFn: ({ appId, secretId }: { appId: string; secretId: string }) =>
-      client.api.deleteOAuth2ProviderAppSecret(appId, secretId),
+      API.deleteOAuth2ProviderAppSecret(appId, secretId),
     onSuccess: async (_: void, { appId }: { appId: string }) => {
       await queryClient.invalidateQueries({
         queryKey: appSecretsKey(appId),
@@ -95,7 +95,7 @@ export const deleteAppSecret = (queryClient: QueryClient) => {
 
 export const revokeApp = (queryClient: QueryClient, userId: string) => {
   return {
-    mutationFn: client.api.revokeOAuth2ProviderApp,
+    mutationFn: API.revokeOAuth2ProviderApp,
     onSuccess: async () => {
       await queryClient.invalidateQueries({
         queryKey: userAppsKey(userId),
diff --git a/site/src/api/queries/roles.ts b/site/src/api/queries/roles.ts
index 7bfe89162316b..2a6c1700b53a7 100644
--- a/site/src/api/queries/roles.ts
+++ b/site/src/api/queries/roles.ts
@@ -1,8 +1,8 @@
-import { client } from "api/api";
+import { API } from "api/api";
 
 export const roles = () => {
   return {
     queryKey: ["roles"],
-    queryFn: client.api.getRoles,
+    queryFn: API.getRoles,
   };
 };
diff --git a/site/src/api/queries/settings.ts b/site/src/api/queries/settings.ts
index bd4ef7f944dce..eb3468b68d978 100644
--- a/site/src/api/queries/settings.ts
+++ b/site/src/api/queries/settings.ts
@@ -1,5 +1,5 @@
 import type { QueryClient, QueryOptions } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import type {
   UpdateUserQuietHoursScheduleRequest,
   UserQuietHoursScheduleResponse,
@@ -16,7 +16,7 @@ export const userQuietHoursSchedule = (
 ): QueryOptions<UserQuietHoursScheduleResponse> => {
   return {
     queryKey: userQuietHoursScheduleKey(userId),
-    queryFn: () => client.api.getUserQuietHoursSchedule(userId),
+    queryFn: () => API.getUserQuietHoursSchedule(userId),
   };
 };
 
@@ -26,7 +26,7 @@ export const updateUserQuietHoursSchedule = (
 ) => {
   return {
     mutationFn: (request: UpdateUserQuietHoursScheduleRequest) =>
-      client.api.updateUserQuietHoursSchedule(userId, request),
+      API.updateUserQuietHoursSchedule(userId, request),
     onSuccess: async () => {
       await queryClient.invalidateQueries(userQuietHoursScheduleKey(userId));
     },
diff --git a/site/src/api/queries/sshKeys.ts b/site/src/api/queries/sshKeys.ts
index 878a80523863e..43686ff1437b2 100644
--- a/site/src/api/queries/sshKeys.ts
+++ b/site/src/api/queries/sshKeys.ts
@@ -1,5 +1,5 @@
 import type { QueryClient } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import type { GitSSHKey } from "api/typesGenerated";
 
 const getUserSSHKeyQueryKey = (userId: string) => [userId, "sshKey"];
@@ -7,7 +7,7 @@ const getUserSSHKeyQueryKey = (userId: string) => [userId, "sshKey"];
 export const userSSHKey = (userId: string) => {
   return {
     queryKey: getUserSSHKeyQueryKey(userId),
-    queryFn: () => client.api.getUserSSHKey(userId),
+    queryFn: () => API.getUserSSHKey(userId),
   };
 };
 
@@ -16,7 +16,7 @@ export const regenerateUserSSHKey = (
   queryClient: QueryClient,
 ) => {
   return {
-    mutationFn: () => client.api.regenerateUserSSHKey(userId),
+    mutationFn: () => API.regenerateUserSSHKey(userId),
     onSuccess: (newKey: GitSSHKey) => {
       queryClient.setQueryData(getUserSSHKeyQueryKey(userId), newKey);
     },
diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts
index 7e485a0e039b4..2d0485b8f347b 100644
--- a/site/src/api/queries/templates.ts
+++ b/site/src/api/queries/templates.ts
@@ -1,5 +1,5 @@
 import type { MutationOptions, QueryClient, QueryOptions } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import type {
   CreateTemplateRequest,
   CreateTemplateVersionRequest,
@@ -26,7 +26,7 @@ export const templateByName = (
 ): QueryOptions<Template> => {
   return {
     queryKey: templateByNameKey(organizationId, name),
-    queryFn: async () => client.api.getTemplateByName(organizationId, name),
+    queryFn: async () => API.getTemplateByName(organizationId, name),
   };
 };
 
@@ -39,27 +39,27 @@ const getTemplatesQueryKey = (organizationId: string, deprecated?: boolean) => [
 export const templates = (organizationId: string, deprecated?: boolean) => {
   return {
     queryKey: getTemplatesQueryKey(organizationId, deprecated),
-    queryFn: () => client.api.getTemplates(organizationId, { deprecated }),
+    queryFn: () => API.getTemplates(organizationId, { deprecated }),
   };
 };
 
 export const templateACL = (templateId: string) => {
   return {
     queryKey: ["templateAcl", templateId],
-    queryFn: () => client.api.getTemplateACL(templateId),
+    queryFn: () => API.getTemplateACL(templateId),
   };
 };
 
 export const setUserRole = (
   queryClient: QueryClient,
 ): MutationOptions<
-  Awaited<ReturnType<typeof client.api.updateTemplateACL>>,
+  Awaited<ReturnType<typeof API.updateTemplateACL>>,
   unknown,
   { templateId: string; userId: string; role: TemplateRole }
 > => {
   return {
     mutationFn: ({ templateId, userId, role }) =>
-      client.api.updateTemplateACL(templateId, {
+      API.updateTemplateACL(templateId, {
         user_perms: {
           [userId]: role,
         },
@@ -73,13 +73,13 @@ export const setUserRole = (
 export const setGroupRole = (
   queryClient: QueryClient,
 ): MutationOptions<
-  Awaited<ReturnType<typeof client.api.updateTemplateACL>>,
+  Awaited<ReturnType<typeof API.updateTemplateACL>>,
   unknown,
   { templateId: string; groupId: string; role: TemplateRole }
 > => {
   return {
     mutationFn: ({ templateId, groupId, role }) =>
-      client.api.updateTemplateACL(templateId, {
+      API.updateTemplateACL(templateId, {
         group_perms: {
           [groupId]: role,
         },
@@ -93,14 +93,14 @@ export const setGroupRole = (
 export const templateExamples = (organizationId: string) => {
   return {
     queryKey: [...getTemplatesQueryKey(organizationId), "examples"],
-    queryFn: () => client.api.getTemplateExamples(organizationId),
+    queryFn: () => API.getTemplateExamples(organizationId),
   };
 };
 
 export const templateVersion = (versionId: string) => {
   return {
     queryKey: ["templateVersion", versionId],
-    queryFn: () => client.api.getTemplateVersion(versionId),
+    queryFn: () => API.getTemplateVersion(versionId),
   };
 };
 
@@ -112,18 +112,14 @@ export const templateVersionByName = (
   return {
     queryKey: ["templateVersion", organizationId, templateName, versionName],
     queryFn: () =>
-      client.api.getTemplateVersionByName(
-        organizationId,
-        templateName,
-        versionName,
-      ),
+      API.getTemplateVersionByName(organizationId, templateName, versionName),
   };
 };
 
 export const templateVersions = (templateId: string) => {
   return {
     queryKey: ["templateVersions", templateId],
-    queryFn: () => client.api.getTemplateVersions(templateId),
+    queryFn: () => API.getTemplateVersions(templateId),
   };
 };
 
@@ -136,14 +132,14 @@ export const templateVersionVariablesKey = (versionId: string) => [
 export const templateVersionVariables = (versionId: string) => {
   return {
     queryKey: templateVersionVariablesKey(versionId),
-    queryFn: () => client.api.getTemplateVersionVariables(versionId),
+    queryFn: () => API.getTemplateVersionVariables(versionId),
   };
 };
 
 export const createTemplateVersion = (organizationId: string) => {
   return {
     mutationFn: async (request: CreateTemplateVersionRequest) => {
-      const newVersion = await client.api.createTemplateVersion(
+      const newVersion = await API.createTemplateVersion(
         organizationId,
         request,
       );
@@ -155,7 +151,7 @@ export const createTemplateVersion = (organizationId: string) => {
 export const createAndBuildTemplateVersion = (organizationId: string) => {
   return {
     mutationFn: async (request: CreateTemplateVersionRequest) => {
-      const newVersion = await client.api.createTemplateVersion(
+      const newVersion = await API.createTemplateVersion(
         organizationId,
         request,
       );
@@ -171,7 +167,7 @@ export const updateActiveTemplateVersion = (
 ) => {
   return {
     mutationFn: (versionId: string) =>
-      client.api.updateActiveTemplateVersion(template.id, {
+      API.updateActiveTemplateVersion(template.id, {
         id: versionId,
       }),
     onSuccess: async () => {
@@ -189,7 +185,7 @@ export const templaceACLAvailable = (
 ) => {
   return {
     queryKey: ["template", templateId, "aclAvailable", options],
-    queryFn: () => client.api.getTemplateACLAvailable(templateId, options),
+    queryFn: () => API.getTemplateACLAvailable(templateId, options),
   };
 };
 
@@ -202,7 +198,7 @@ export const templateVersionExternalAuthKey = (versionId: string) => [
 export const templateVersionExternalAuth = (versionId: string) => {
   return {
     queryKey: templateVersionExternalAuthKey(versionId),
-    queryFn: () => client.api.getTemplateVersionExternalAuth(versionId),
+    queryFn: () => API.getTemplateVersionExternalAuth(versionId),
   };
 };
 
@@ -221,13 +217,13 @@ export type CreateTemplateOptions = {
 };
 
 const createTemplateFn = async (options: CreateTemplateOptions) => {
-  const version = await client.api.createTemplateVersion(
+  const version = await API.createTemplateVersion(
     options.organizationId,
     options.version,
   );
   options.onCreateVersion?.(version);
   await waitBuildToBeFinished(version, options.onTemplateVersionChanges);
-  return client.api.createTemplate(options.organizationId, {
+  return API.createTemplate(options.organizationId, {
     ...options.template,
     template_version_id: version.id,
   });
@@ -236,21 +232,21 @@ const createTemplateFn = async (options: CreateTemplateOptions) => {
 export const templateVersionLogs = (versionId: string) => {
   return {
     queryKey: ["templateVersion", versionId, "logs"],
-    queryFn: () => client.api.getTemplateVersionLogs(versionId),
+    queryFn: () => API.getTemplateVersionLogs(versionId),
   };
 };
 
 export const richParameters = (versionId: string) => {
   return {
     queryKey: ["templateVersion", versionId, "richParameters"],
-    queryFn: () => client.api.getTemplateVersionRichParameters(versionId),
+    queryFn: () => API.getTemplateVersionRichParameters(versionId),
   };
 };
 
 export const resources = (versionId: string) => {
   return {
     queryKey: ["templateVersion", versionId, "resources"],
-    queryFn: () => client.api.getTemplateVersionResources(versionId),
+    queryFn: () => API.getTemplateVersionResources(versionId),
   };
 };
 
@@ -258,7 +254,7 @@ export const templateFiles = (fileId: string) => {
   return {
     queryKey: ["templateFiles", fileId],
     queryFn: async () => {
-      const tarFile = await client.api.getFile(fileId);
+      const tarFile = await API.getFile(fileId);
       return getTemplateVersionFiles(tarFile);
     },
   };
@@ -278,7 +274,7 @@ export const previousTemplateVersion = (
       "previous",
     ],
     queryFn: async () => {
-      const result = await client.api.getPreviousTemplateVersionByName(
+      const result = await API.getPreviousTemplateVersionByName(
         organizationId,
         templateName,
         versionName,
@@ -298,7 +294,7 @@ const waitBuildToBeFinished = async (
   do {
     // When pending we want to poll more frequently
     await delay(jobStatus === "pending" ? 250 : 1000);
-    data = await client.api.getTemplateVersion(version.id);
+    data = await API.getTemplateVersion(version.id);
     onRequest?.(data);
     jobStatus = data.job.status;
 
diff --git a/site/src/api/queries/updateCheck.ts b/site/src/api/queries/updateCheck.ts
index 6c7a2add42ef4..e8dc1b2cc3e41 100644
--- a/site/src/api/queries/updateCheck.ts
+++ b/site/src/api/queries/updateCheck.ts
@@ -1,8 +1,8 @@
-import { client } from "api/api";
+import { API } from "api/api";
 
 export const updateCheck = () => {
   return {
     queryKey: ["updateCheck"],
-    queryFn: () => client.api.getUpdateCheck(),
+    queryFn: () => API.getUpdateCheck(),
   };
 };
diff --git a/site/src/api/queries/users.ts b/site/src/api/queries/users.ts
index 982c1ca78717c..7dcd157f7bc6c 100644
--- a/site/src/api/queries/users.ts
+++ b/site/src/api/queries/users.ts
@@ -3,7 +3,7 @@ import type {
   UseMutationOptions,
   UseQueryOptions,
 } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import type {
   AuthorizationRequest,
   GetUsersResponse,
@@ -41,14 +41,14 @@ export function paginatedUsers(
     },
 
     queryKey: ({ payload }) => usersKey(payload),
-    queryFn: ({ payload, signal }) => client.api.getUsers(payload, signal),
+    queryFn: ({ payload, signal }) => API.getUsers(payload, signal),
   };
 }
 
 export const users = (req: UsersRequest): UseQueryOptions<GetUsersResponse> => {
   return {
     queryKey: usersKey(req),
-    queryFn: ({ signal }) => client.api.getUsers(req, signal),
+    queryFn: ({ signal }) => API.getUsers(req, signal),
     cacheTime: 5 * 1000 * 60,
   };
 };
@@ -59,13 +59,13 @@ export const updatePassword = () => {
       userId,
       ...request
     }: UpdateUserPasswordRequest & { userId: string }) =>
-      client.api.updateUserPassword(userId, request),
+      API.updateUserPassword(userId, request),
   };
 };
 
 export const createUser = (queryClient: QueryClient) => {
   return {
-    mutationFn: client.api.createUser,
+    mutationFn: API.createUser,
     onSuccess: async () => {
       await queryClient.invalidateQueries(["users"]);
     },
@@ -74,13 +74,13 @@ export const createUser = (queryClient: QueryClient) => {
 
 export const createFirstUser = () => {
   return {
-    mutationFn: client.api.createFirstUser,
+    mutationFn: API.createFirstUser,
   };
 };
 
 export const suspendUser = (queryClient: QueryClient) => {
   return {
-    mutationFn: client.api.suspendUser,
+    mutationFn: API.suspendUser,
     onSuccess: async () => {
       await queryClient.invalidateQueries(["users"]);
     },
@@ -89,7 +89,7 @@ export const suspendUser = (queryClient: QueryClient) => {
 
 export const activateUser = (queryClient: QueryClient) => {
   return {
-    mutationFn: client.api.activateUser,
+    mutationFn: API.activateUser,
     onSuccess: async () => {
       await queryClient.invalidateQueries(["users"]);
     },
@@ -98,7 +98,7 @@ export const activateUser = (queryClient: QueryClient) => {
 
 export const deleteUser = (queryClient: QueryClient) => {
   return {
-    mutationFn: client.api.deleteUser,
+    mutationFn: API.deleteUser,
     onSuccess: async () => {
       await queryClient.invalidateQueries(["users"]);
     },
@@ -108,7 +108,7 @@ export const deleteUser = (queryClient: QueryClient) => {
 export const updateRoles = (queryClient: QueryClient) => {
   return {
     mutationFn: ({ userId, roles }: { userId: string; roles: string[] }) =>
-      client.api.updateUserRoles(roles, userId),
+      API.updateUserRoles(roles, userId),
     onSuccess: async () => {
       await queryClient.invalidateQueries(["users"]);
     },
@@ -120,7 +120,7 @@ export const authMethods = () => {
     // Even the endpoint being /users/authmethods we don't want to revalidate it
     // when users change so its better add a unique query key
     queryKey: ["authMethods"],
-    queryFn: client.api.getAuthMethods,
+    queryFn: API.getAuthMethods,
   };
 };
 
@@ -130,14 +130,14 @@ export const me = (metadata: MetadataState<User>) => {
   return cachedQuery({
     metadata,
     queryKey: meKey,
-    queryFn: client.api.getAuthenticatedUser,
+    queryFn: API.getAuthenticatedUser,
   });
 };
 
 export function apiKey(): UseQueryOptions<GenerateAPIKeyResponse> {
   return {
     queryKey: [...meKey, "apiKey"],
-    queryFn: () => client.api.getApiKey(),
+    queryFn: () => API.getApiKey(),
   };
 }
 
@@ -145,7 +145,7 @@ export const hasFirstUser = (userMetadata: MetadataState<User>) => {
   return cachedQuery({
     metadata: userMetadata,
     queryKey: ["hasFirstUser"],
-    queryFn: client.api.hasFirstUser,
+    queryFn: API.hasFirstUser,
   });
 };
 
@@ -175,10 +175,10 @@ const loginFn = async ({
   password: string;
   authorization: AuthorizationRequest;
 }) => {
-  await client.api.login(email, password);
+  await API.login(email, password);
   const [user, permissions] = await Promise.all([
-    client.api.getAuthenticatedUser(),
-    client.api.checkAuthorization(authorization),
+    API.getAuthenticatedUser(),
+    API.checkAuthorization(authorization),
   ]);
   return {
     user,
@@ -188,7 +188,7 @@ const loginFn = async ({
 
 export const logout = (queryClient: QueryClient) => {
   return {
-    mutationFn: client.api.logout,
+    mutationFn: API.logout,
     onSuccess: () => {
       /**
        * 2024-05-02 - If we persist any form of user data after the user logs
@@ -214,7 +214,7 @@ export const logout = (queryClient: QueryClient) => {
 export const updateProfile = (userId: string) => {
   return {
     mutationFn: (req: UpdateUserProfileRequest) =>
-      client.api.updateProfile(userId, req),
+      API.updateProfile(userId, req),
   };
 };
 
@@ -228,7 +228,7 @@ export const updateAppearanceSettings = (
   unknown
 > => {
   return {
-    mutationFn: (req) => client.api.updateAppearanceSettings(userId, req),
+    mutationFn: (req) => API.updateAppearanceSettings(userId, req),
     onMutate: async (patch) => {
       // Mutate the `queryClient` optimistically to make the theme switcher
       // more responsive.
diff --git a/site/src/api/queries/workspaceBuilds.ts b/site/src/api/queries/workspaceBuilds.ts
index d4cc127c6814e..a7c0aaf4fdabe 100644
--- a/site/src/api/queries/workspaceBuilds.ts
+++ b/site/src/api/queries/workspaceBuilds.ts
@@ -1,5 +1,5 @@
 import type { QueryOptions, UseInfiniteQueryOptions } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import type {
   WorkspaceBuild,
   WorkspaceBuildParameter,
@@ -13,7 +13,7 @@ export function workspaceBuildParametersKey(workspaceBuildId: string) {
 export function workspaceBuildParameters(workspaceBuildId: string) {
   return {
     queryKey: workspaceBuildParametersKey(workspaceBuildId),
-    queryFn: () => client.api.getWorkspaceBuildParameters(workspaceBuildId),
+    queryFn: () => API.getWorkspaceBuildParameters(workspaceBuildId),
   } as const satisfies QueryOptions<WorkspaceBuildParameter[]>;
 }
 
@@ -25,11 +25,7 @@ export const workspaceBuildByNumber = (
   return {
     queryKey: ["workspaceBuild", username, workspaceName, buildNumber],
     queryFn: () =>
-      client.api.getWorkspaceBuildByNumber(
-        username,
-        workspaceName,
-        buildNumber,
-      ),
+      API.getWorkspaceBuildByNumber(username, workspaceName, buildNumber),
   };
 };
 
@@ -53,7 +49,7 @@ export const infiniteWorkspaceBuilds = (
       return pages.length + 1;
     },
     queryFn: ({ pageParam = 0 }) => {
-      return client.api.getWorkspaceBuilds(workspaceId, {
+      return API.getWorkspaceBuilds(workspaceId, {
         limit,
         offset: pageParam <= 0 ? 0 : (pageParam - 1) * limit,
       });
diff --git a/site/src/api/queries/workspaceQuota.ts b/site/src/api/queries/workspaceQuota.ts
index ec27415b7ce2e..1735b0f71279b 100644
--- a/site/src/api/queries/workspaceQuota.ts
+++ b/site/src/api/queries/workspaceQuota.ts
@@ -1,4 +1,4 @@
-import { client } from "api/api";
+import { API } from "api/api";
 
 export const getWorkspaceQuotaQueryKey = (username: string) => [
   username,
@@ -8,7 +8,7 @@ export const getWorkspaceQuotaQueryKey = (username: string) => [
 export const workspaceQuota = (username: string) => {
   return {
     queryKey: getWorkspaceQuotaQueryKey(username),
-    queryFn: () => client.api.getWorkspaceQuota(username),
+    queryFn: () => API.getWorkspaceQuota(username),
   };
 };
 
@@ -20,6 +20,6 @@ export const getWorkspaceResolveAutostartQueryKey = (workspaceId: string) => [
 export const workspaceResolveAutostart = (workspaceId: string) => {
   return {
     queryKey: getWorkspaceResolveAutostartQueryKey(workspaceId),
-    queryFn: () => client.api.getWorkspaceResolveAutostart(workspaceId),
+    queryFn: () => API.getWorkspaceResolveAutostart(workspaceId),
   };
 };
diff --git a/site/src/api/queries/workspaceportsharing.ts b/site/src/api/queries/workspaceportsharing.ts
index fd37af90fa926..60bd99285aa54 100644
--- a/site/src/api/queries/workspaceportsharing.ts
+++ b/site/src/api/queries/workspaceportsharing.ts
@@ -1,4 +1,4 @@
-import { client } from "api/api";
+import { API } from "api/api";
 import type {
   DeleteWorkspaceAgentPortShareRequest,
   UpsertWorkspaceAgentPortShareRequest,
@@ -7,14 +7,14 @@ import type {
 export const workspacePortShares = (workspaceId: string) => {
   return {
     queryKey: ["sharedPorts", workspaceId],
-    queryFn: () => client.api.getWorkspaceAgentSharedPorts(workspaceId),
+    queryFn: () => API.getWorkspaceAgentSharedPorts(workspaceId),
   };
 };
 
 export const upsertWorkspacePortShare = (workspaceId: string) => {
   return {
     mutationFn: async (options: UpsertWorkspaceAgentPortShareRequest) => {
-      await client.api.upsertWorkspaceAgentSharedPort(workspaceId, options);
+      await API.upsertWorkspaceAgentSharedPort(workspaceId, options);
     },
   };
 };
@@ -22,7 +22,7 @@ export const upsertWorkspacePortShare = (workspaceId: string) => {
 export const deleteWorkspacePortShare = (workspaceId: string) => {
   return {
     mutationFn: async (options: DeleteWorkspaceAgentPortShareRequest) => {
-      await client.api.deleteWorkspaceAgentSharedPort(workspaceId, options);
+      await API.deleteWorkspaceAgentSharedPort(workspaceId, options);
     },
   };
 };
diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts
index da68598cc6358..95df3b7f592f6 100644
--- a/site/src/api/queries/workspaces.ts
+++ b/site/src/api/queries/workspaces.ts
@@ -4,7 +4,7 @@ import type {
   QueryOptions,
   UseMutationOptions,
 } from "react-query";
-import { type DeleteWorkspaceOptions, client } from "api/api";
+import { type DeleteWorkspaceOptions, API } from "api/api";
 import type {
   CreateWorkspaceRequest,
   ProvisionerLogLevel,
@@ -27,7 +27,7 @@ export const workspaceByOwnerAndName = (owner: string, name: string) => {
   return {
     queryKey: workspaceByOwnerAndNameKey(owner, name),
     queryFn: () =>
-      client.api.getWorkspaceByOwnerAndName(owner, name, {
+      API.getWorkspaceByOwnerAndName(owner, name, {
         include_deleted: true,
       }),
   };
@@ -50,7 +50,7 @@ export const createWorkspace = (queryClient: QueryClient) => {
   return {
     mutationFn: async (variables: CreateWorkspaceMutationVariables) => {
       const { userId, organizationId, ...req } = variables;
-      return client.api.createWorkspace(organizationId, userId, req);
+      return API.createWorkspace(organizationId, userId, req);
     },
     onSuccess: async () => {
       await queryClient.invalidateQueries(["workspaces"]);
@@ -72,14 +72,14 @@ export const autoCreateWorkspace = (queryClient: QueryClient) => {
       if (versionId) {
         templateVersionParameters = { template_version_id: versionId };
       } else {
-        const template = await client.api.getTemplateByName(
+        const template = await API.getTemplateByName(
           organizationId,
           templateName,
         );
         templateVersionParameters = { template_id: template.id };
       }
 
-      return client.api.createWorkspace(organizationId, "me", {
+      return API.createWorkspace(organizationId, "me", {
         ...templateVersionParameters,
         name: defaultName,
         rich_parameter_values: defaultBuildParameters,
@@ -103,7 +103,7 @@ export function workspaces(config: WorkspacesRequest = {}) {
 
   return {
     queryKey: workspacesKey(config),
-    queryFn: () => client.api.getWorkspaces({ q, limit }),
+    queryFn: () => API.getWorkspaces({ q, limit }),
   } as const satisfies QueryOptions<WorkspacesResponse>;
 }
 
@@ -112,7 +112,7 @@ export const updateDeadline = (
 ): UseMutationOptions<void, unknown, Dayjs> => {
   return {
     mutationFn: (deadline: Dayjs) => {
-      return client.api.putWorkspaceExtension(workspace.id, deadline);
+      return API.putWorkspaceExtension(workspace.id, deadline);
     },
   };
 };
@@ -129,11 +129,7 @@ export const changeVersion = (
       versionId: string;
       buildParameters?: WorkspaceBuildParameter[];
     }) => {
-      return client.api.changeWorkspaceVersion(
-        workspace,
-        versionId,
-        buildParameters,
-      );
+      return API.changeWorkspaceVersion(workspace, versionId, buildParameters);
     },
     onSuccess: async (build: WorkspaceBuild) => {
       await updateWorkspaceBuild(build, queryClient);
@@ -147,7 +143,7 @@ export const updateWorkspace = (
 ) => {
   return {
     mutationFn: (buildParameters?: WorkspaceBuildParameter[]) => {
-      return client.api.updateWorkspace(workspace, buildParameters);
+      return API.updateWorkspace(workspace, buildParameters);
     },
     onSuccess: async (build: WorkspaceBuild) => {
       await updateWorkspaceBuild(build, queryClient);
@@ -161,7 +157,7 @@ export const deleteWorkspace = (
 ) => {
   return {
     mutationFn: (options: DeleteWorkspaceOptions) => {
-      return client.api.deleteWorkspace(workspace.id, options);
+      return API.deleteWorkspace(workspace.id, options);
     },
     onSuccess: async (build: WorkspaceBuild) => {
       await updateWorkspaceBuild(build, queryClient);
@@ -175,7 +171,7 @@ export const stopWorkspace = (
 ) => {
   return {
     mutationFn: ({ logLevel }: { logLevel?: ProvisionerLogLevel }) => {
-      return client.api.stopWorkspace(workspace.id, logLevel);
+      return API.stopWorkspace(workspace.id, logLevel);
     },
     onSuccess: async (build: WorkspaceBuild) => {
       await updateWorkspaceBuild(build, queryClient);
@@ -195,7 +191,7 @@ export const startWorkspace = (
       buildParameters?: WorkspaceBuildParameter[];
       logLevel?: ProvisionerLogLevel;
     }) => {
-      return client.api.startWorkspace(
+      return API.startWorkspace(
         workspace.id,
         workspace.latest_build.template_version_id,
         logLevel,
@@ -211,7 +207,7 @@ export const startWorkspace = (
 export const cancelBuild = (workspace: Workspace, queryClient: QueryClient) => {
   return {
     mutationFn: () => {
-      return client.api.cancelWorkspaceBuild(workspace.latest_build.id);
+      return API.cancelWorkspaceBuild(workspace.latest_build.id);
     },
     onSuccess: async () => {
       await queryClient.invalidateQueries({
@@ -224,7 +220,7 @@ export const cancelBuild = (workspace: Workspace, queryClient: QueryClient) => {
 export const activate = (workspace: Workspace, queryClient: QueryClient) => {
   return {
     mutationFn: () => {
-      return client.api.updateWorkspaceDormancy(workspace.id, false);
+      return API.updateWorkspaceDormancy(workspace.id, false);
     },
     onSuccess: (updatedWorkspace: Workspace) => {
       queryClient.setQueryData(
@@ -268,9 +264,9 @@ export const toggleFavorite = (
   return {
     mutationFn: () => {
       if (workspace.favorite) {
-        return client.api.deleteFavoriteWorkspace(workspace.id);
+        return API.deleteFavoriteWorkspace(workspace.id);
       } else {
-        return client.api.putFavoriteWorkspace(workspace.id);
+        return API.putFavoriteWorkspace(workspace.id);
       }
     },
     onSuccess: async () => {
diff --git a/site/src/components/Filter/UserFilter.tsx b/site/src/components/Filter/UserFilter.tsx
index fe44940880380..a42dbf07d791c 100644
--- a/site/src/components/Filter/UserFilter.tsx
+++ b/site/src/components/Filter/UserFilter.tsx
@@ -1,5 +1,5 @@
 import type { FC } from "react";
-import { client } from "api/api";
+import { API } from "api/api";
 import { useAuthenticated } from "contexts/auth/RequireAuth";
 import { UserAvatar } from "../UserAvatar/UserAvatar";
 import { FilterSearchMenu, OptionItem } from "./filter";
@@ -42,7 +42,7 @@ export const useUserFilterMenu = ({
         };
       }
 
-      const usersRes = await client.api.getUsers({ q: value, limit: 1 });
+      const usersRes = await API.getUsers({ q: value, limit: 1 });
       const firstUser = usersRes.users.at(0);
       if (firstUser && firstUser.username === value) {
         return {
@@ -54,7 +54,7 @@ export const useUserFilterMenu = ({
       return null;
     },
     getOptions: async (query) => {
-      const usersRes = await client.api.getUsers({ q: query, limit: 25 });
+      const usersRes = await API.getUsers({ q: query, limit: 25 });
       let options: UserOption[] = usersRes.users.map((user) => ({
         label: user.username,
         value: user.username,
diff --git a/site/src/contexts/ProxyContext.tsx b/site/src/contexts/ProxyContext.tsx
index e05c1c04e8e55..767cdd54d1a67 100644
--- a/site/src/contexts/ProxyContext.tsx
+++ b/site/src/contexts/ProxyContext.tsx
@@ -8,7 +8,7 @@ import {
   useState,
 } from "react";
 import { useQuery } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import { cachedQuery } from "api/queries/util";
 import type { Region, WorkspaceProxy } from "api/typesGenerated";
 import { useAuthenticated } from "contexts/auth/RequireAuth";
@@ -109,8 +109,8 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => {
       queryKey: ["get-proxies"],
       queryFn: async (): Promise<readonly Region[]> => {
         const apiCall = permissions.editWorkspaceProxies
-          ? client.api.getWorkspaceProxies
-          : client.api.getWorkspaceProxyRegions;
+          ? API.getWorkspaceProxies
+          : API.getWorkspaceProxyRegions;
 
         const resp = await apiCall();
         return resp.regions;
diff --git a/site/src/contexts/auth/RequireAuth.tsx b/site/src/contexts/auth/RequireAuth.tsx
index 682320e10aa27..2d6b14d3db69f 100644
--- a/site/src/contexts/auth/RequireAuth.tsx
+++ b/site/src/contexts/auth/RequireAuth.tsx
@@ -1,6 +1,6 @@
 import { type FC, useEffect } from "react";
 import { Outlet, Navigate, useLocation } from "react-router-dom";
-import { client } from "api/api";
+import { API } from "api/api";
 import { isApiError } from "api/errors";
 import { Loader } from "components/Loader/Loader";
 import { ProxyProvider } from "contexts/ProxyContext";
@@ -18,7 +18,7 @@ export const RequireAuth: FC = () => {
       return;
     }
 
-    const axiosInstance = client.getAxiosInstance();
+    const axiosInstance = API.getAxiosInstance();
     const interceptorHandle = axiosInstance.interceptors.response.use(
       (okResponse) => okResponse,
       (error: unknown) => {
diff --git a/site/src/contexts/useProxyLatency.ts b/site/src/contexts/useProxyLatency.ts
index fb3d03d0269e4..df2afc277b44a 100644
--- a/site/src/contexts/useProxyLatency.ts
+++ b/site/src/contexts/useProxyLatency.ts
@@ -1,6 +1,6 @@
 import PerformanceObserver from "@fastly/performance-observer-polyfill";
 import { useEffect, useReducer, useState } from "react";
-import { client } from "api/api";
+import { API } from "api/api";
 import type { Region } from "api/typesGenerated";
 import { generateRandomString } from "utils/random";
 
@@ -197,7 +197,7 @@ export const useProxyLatency = (
     // The resource requests include xmlhttp requests.
     observer.observe({ entryTypes: ["resource"] });
 
-    const axiosInstance = client.getAxiosInstance();
+    const axiosInstance = API.getAxiosInstance();
     const proxyRequests = Object.keys(proxyChecks).map((latencyURL) => {
       return axiosInstance.get(latencyURL, {
         withCredentials: false,
diff --git a/site/src/modules/resources/AppLink/AppLink.tsx b/site/src/modules/resources/AppLink/AppLink.tsx
index bfbc03f4a9a91..7042c879385d0 100644
--- a/site/src/modules/resources/AppLink/AppLink.tsx
+++ b/site/src/modules/resources/AppLink/AppLink.tsx
@@ -4,7 +4,7 @@ import CircularProgress from "@mui/material/CircularProgress";
 import Link from "@mui/material/Link";
 import Tooltip from "@mui/material/Tooltip";
 import { type FC, useState } from "react";
-import { client } from "api/api";
+import { API } from "api/api";
 import type * as TypesGen from "api/typesGenerated";
 import { useProxy } from "contexts/ProxyContext";
 import { createAppLinkHref } from "utils/apps";
@@ -145,7 +145,7 @@ export const AppLink: FC<AppLinkProps> = ({ app, workspace, agent }) => {
             let url = href;
             if (hasMagicToken !== -1) {
               setFetchingSessionToken(true);
-              const key = await client.api.getApiKey();
+              const key = await API.getApiKey();
               url = href.replaceAll(magicTokenString, key.key);
               setFetchingSessionToken(false);
             }
diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx
index 984b21fa6632e..d22e986a1c074 100644
--- a/site/src/modules/resources/PortForwardButton.tsx
+++ b/site/src/modules/resources/PortForwardButton.tsx
@@ -19,7 +19,7 @@ import { type FormikContextType, useFormik } from "formik";
 import { useState, type FC } from "react";
 import { useQuery, useMutation } from "react-query";
 import * as Yup from "yup";
-import { client } from "api/api";
+import { API } from "api/api";
 import {
   deleteWorkspacePortShare,
   upsertWorkspacePortShare,
@@ -70,7 +70,7 @@ export const PortForwardButton: FC<PortForwardButtonProps> = (props) => {
 
   const portsQuery = useQuery({
     queryKey: ["portForward", agent.id],
-    queryFn: () => client.api.getAgentListeningPorts(agent.id),
+    queryFn: () => API.getAgentListeningPorts(agent.id),
     enabled: agent.status === "connected",
     refetchInterval: 5_000,
   });
diff --git a/site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx b/site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx
index 36b6f5822f804..73763439076bd 100644
--- a/site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx
+++ b/site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx
@@ -3,7 +3,7 @@ import ButtonGroup from "@mui/material/ButtonGroup";
 import Menu from "@mui/material/Menu";
 import MenuItem from "@mui/material/MenuItem";
 import { type FC, useState, useRef } from "react";
-import { client } from "api/api";
+import { API } from "api/api";
 import type { DisplayApp } from "api/typesGenerated";
 import { VSCodeIcon } from "components/Icons/VSCodeIcon";
 import { VSCodeInsidersIcon } from "components/Icons/VSCodeInsidersIcon";
@@ -119,8 +119,7 @@ const VSCodeButton: FC<VSCodeDesktopButtonProps> = ({
       disabled={loading}
       onClick={() => {
         setLoading(true);
-        client.api
-          .getApiKey()
+        API.getApiKey()
           .then(({ key }) => {
             const query = new URLSearchParams({
               owner: userName,
@@ -164,8 +163,7 @@ const VSCodeInsidersButton: FC<VSCodeDesktopButtonProps> = ({
       disabled={loading}
       onClick={() => {
         setLoading(true);
-        client.api
-          .getApiKey()
+        API.getApiKey()
           .then(({ key }) => {
             const query = new URLSearchParams({
               owner: userName,
diff --git a/site/src/pages/AuditPage/AuditPage.test.tsx b/site/src/pages/AuditPage/AuditPage.test.tsx
index ea5e8b438606f..be3317ee68099 100644
--- a/site/src/pages/AuditPage/AuditPage.test.tsx
+++ b/site/src/pages/AuditPage/AuditPage.test.tsx
@@ -1,7 +1,7 @@
 import { screen, waitFor } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import { HttpResponse, http } from "msw";
-import { client } from "api/api";
+import { API } from "api/api";
 import { DEFAULT_RECORDS_PER_PAGE } from "components/PaginationWidget/utils";
 import {
   MockAuditLog,
@@ -61,12 +61,10 @@ describe("AuditPage", () => {
   it("renders page 5", async () => {
     // Given
     const page = 5;
-    const getAuditLogsSpy = jest
-      .spyOn(client.api, "getAuditLogs")
-      .mockResolvedValue({
-        audit_logs: [MockAuditLog, MockAuditLog2],
-        count: 2,
-      });
+    const getAuditLogsSpy = jest.spyOn(API, "getAuditLogs").mockResolvedValue({
+      audit_logs: [MockAuditLog, MockAuditLog2],
+      count: 2,
+    });
 
     // When
     await renderPage({ page: page });
@@ -84,7 +82,7 @@ describe("AuditPage", () => {
   describe("Filtering", () => {
     it("filters by URL", async () => {
       const getAuditLogsSpy = jest
-        .spyOn(client.api, "getAuditLogs")
+        .spyOn(API, "getAuditLogs")
         .mockResolvedValue({ audit_logs: [MockAuditLog], count: 1 });
 
       const query = "resource_type:workspace action:create";
@@ -100,7 +98,7 @@ describe("AuditPage", () => {
     it("resets page to 1 when filter is changed", async () => {
       await renderPage({ page: 2 });
 
-      const getAuditLogsSpy = jest.spyOn(client.api, "getAuditLogs");
+      const getAuditLogsSpy = jest.spyOn(API, "getAuditLogs");
       getAuditLogsSpy.mockClear();
 
       const filterField = screen.getByLabelText("Filter");
diff --git a/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx b/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx
index 1c28b4a1e3afb..38cf8994011c3 100644
--- a/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx
+++ b/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx
@@ -1,6 +1,6 @@
 import { screen, waitFor, within } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import { client } from "api/api";
+import { API } from "api/api";
 import {
   MockTemplateExample,
   MockTemplateVersion,
@@ -35,14 +35,14 @@ test("Create template from starter template", async () => {
   const { router, container } = await renderPage(searchParams);
   const form = container.querySelector("form") as HTMLFormElement;
 
-  jest.spyOn(client.api, "createTemplateVersion").mockResolvedValueOnce({
+  jest.spyOn(API, "createTemplateVersion").mockResolvedValueOnce({
     ...MockTemplateVersion,
     job: {
       ...MockTemplateVersion.job,
       status: "pending",
     },
   });
-  jest.spyOn(client.api, "getTemplateVersion").mockResolvedValue({
+  jest.spyOn(API, "getTemplateVersion").mockResolvedValue({
     ...MockTemplateVersion,
     job: {
       ...MockTemplateVersion.job,
@@ -51,7 +51,7 @@ test("Create template from starter template", async () => {
     },
   });
   jest
-    .spyOn(client.api, "getTemplateVersionVariables")
+    .spyOn(API, "getTemplateVersionVariables")
     .mockResolvedValue([
       MockTemplateVersionVariable1,
       MockTemplateVersionVariable2,
@@ -85,42 +85,35 @@ test("Create template from starter template", async () => {
   // Setup the mock for the second template version creation before submit the form
   jest.clearAllMocks();
   jest
-    .spyOn(client.api, "createTemplateVersion")
+    .spyOn(API, "createTemplateVersion")
     .mockResolvedValue(MockTemplateVersion);
-  jest
-    .spyOn(client.api, "getTemplateVersion")
-    .mockResolvedValue(MockTemplateVersion);
-  jest.spyOn(client.api, "createTemplate").mockResolvedValue(MockTemplate);
+  jest.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion);
+  jest.spyOn(API, "createTemplate").mockResolvedValue(MockTemplate);
   await userEvent.click(
     within(form).getByRole("button", { name: /create template/i }),
   );
-  await waitFor(() => expect(client.api.createTemplate).toBeCalledTimes(1));
+  await waitFor(() => expect(API.createTemplate).toBeCalledTimes(1));
   expect(router.state.location.pathname).toEqual(
     `/templates/${MockTemplate.name}/files`,
   );
-  expect(client.api.createTemplateVersion).toHaveBeenCalledWith(
-    MockOrganization.id,
-    {
-      example_id: "aws-windows",
-      provisioner: "terraform",
-      storage_method: "file",
-      tags: {},
-      user_variable_values: [
-        { name: "first_variable", value: "First value" },
-        { name: "second_variable", value: "2" },
-        { name: "third_variable", value: "true" },
-      ],
-    },
-  );
+  expect(API.createTemplateVersion).toHaveBeenCalledWith(MockOrganization.id, {
+    example_id: "aws-windows",
+    provisioner: "terraform",
+    storage_method: "file",
+    tags: {},
+    user_variable_values: [
+      { name: "first_variable", value: "First value" },
+      { name: "second_variable", value: "2" },
+      { name: "third_variable", value: "true" },
+    ],
+  });
 });
 
 test("Create template from duplicating a template", async () => {
-  jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate);
+  jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
+  jest.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion);
   jest
-    .spyOn(client.api, "getTemplateVersion")
-    .mockResolvedValue(MockTemplateVersion);
-  jest
-    .spyOn(client.api, "getTemplateVersionVariables")
+    .spyOn(API, "getTemplateVersionVariables")
     .mockResolvedValue([MockTemplateVersionVariable1]);
 
   const searchParams = new URLSearchParams({
@@ -142,12 +135,10 @@ test("Create template from duplicating a template", async () => {
   ).toHaveValue(MockTemplateVersionVariable1.value);
   // Create template
   jest
-    .spyOn(client.api, "createTemplateVersion")
-    .mockResolvedValue(MockTemplateVersion);
-  jest
-    .spyOn(client.api, "getTemplateVersion")
+    .spyOn(API, "createTemplateVersion")
     .mockResolvedValue(MockTemplateVersion);
-  jest.spyOn(client.api, "createTemplate").mockResolvedValue(MockTemplate);
+  jest.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion);
+  jest.spyOn(API, "createTemplate").mockResolvedValue(MockTemplate);
   await userEvent.click(
     screen.getByRole("button", { name: /create template/i }),
   );
diff --git a/site/src/pages/CreateTokenPage/CreateTokenPage.test.tsx b/site/src/pages/CreateTokenPage/CreateTokenPage.test.tsx
index 20f74d4c4dd79..630834cd5fa72 100644
--- a/site/src/pages/CreateTokenPage/CreateTokenPage.test.tsx
+++ b/site/src/pages/CreateTokenPage/CreateTokenPage.test.tsx
@@ -1,6 +1,6 @@
 import { screen, within } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import { client } from "api/api";
+import { API } from "api/api";
 import {
   renderWithAuth,
   waitForLoaderToBeRemoved,
@@ -9,7 +9,7 @@ import { CreateTokenPage } from "./CreateTokenPage";
 
 describe("TokenPage", () => {
   it("shows the success modal", async () => {
-    jest.spyOn(client.api, "createToken").mockResolvedValueOnce({
+    jest.spyOn(API, "createToken").mockResolvedValueOnce({
       key: "abcd",
     });
 
diff --git a/site/src/pages/CreateTokenPage/CreateTokenPage.tsx b/site/src/pages/CreateTokenPage/CreateTokenPage.tsx
index a5883041b9299..1fcf9daaa43fb 100644
--- a/site/src/pages/CreateTokenPage/CreateTokenPage.tsx
+++ b/site/src/pages/CreateTokenPage/CreateTokenPage.tsx
@@ -3,7 +3,7 @@ import { type FC, useState } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation, useQuery } from "react-query";
 import { useNavigate } from "react-router-dom";
-import { client } from "api/api";
+import { API } from "api/api";
 import { ErrorAlert } from "components/Alert/ErrorAlert";
 import { CodeExample } from "components/CodeExample/CodeExample";
 import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
@@ -28,7 +28,7 @@ export const CreateTokenPage: FC = () => {
     isError: creationFailed,
     isSuccess: creationSuccessful,
     data: newToken,
-  } = useMutation(client.api.createToken);
+  } = useMutation(API.createToken);
   const {
     data: tokenConfig,
     isLoading: fetchingTokenConfig,
@@ -36,7 +36,7 @@ export const CreateTokenPage: FC = () => {
     error: tokenFetchError,
   } = useQuery({
     queryKey: ["tokenconfig"],
-    queryFn: client.api.getTokenConfig,
+    queryFn: API.getTokenConfig,
   });
 
   const [formError, setFormError] = useState<unknown>(undefined);
diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx
index 47c29e4ba91b7..02bde4b7134cf 100644
--- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx
+++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx
@@ -1,6 +1,6 @@
 import { fireEvent, screen, waitFor } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import { client } from "api/api";
+import { API } from "api/api";
 import {
   MockTemplate,
   MockUser,
@@ -36,16 +36,14 @@ const renderCreateWorkspacePage = () => {
 describe("CreateWorkspacePage", () => {
   it("succeeds with default owner", async () => {
     jest
-      .spyOn(client.api, "getUsers")
+      .spyOn(API, "getUsers")
       .mockResolvedValueOnce({ users: [MockUser], count: 1 });
     jest
-      .spyOn(client.api, "getWorkspaceQuota")
+      .spyOn(API, "getWorkspaceQuota")
       .mockResolvedValueOnce(MockWorkspaceQuota);
+    jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace);
     jest
-      .spyOn(client.api, "createWorkspace")
-      .mockResolvedValueOnce(MockWorkspace);
-    jest
-      .spyOn(client.api, "getTemplateVersionRichParameters")
+      .spyOn(API, "getTemplateVersionRichParameters")
       .mockResolvedValueOnce([MockTemplateVersionParameter1]);
 
     renderCreateWorkspacePage();
@@ -61,7 +59,7 @@ describe("CreateWorkspacePage", () => {
     await userEvent.click(submitButton);
 
     await waitFor(() =>
-      expect(client.api.createWorkspace).toBeCalledWith(
+      expect(API.createWorkspace).toBeCalledWith(
         MockUser.organization_ids[0],
         MockUser.id,
         expect.objectContaining({
@@ -75,7 +73,7 @@ describe("CreateWorkspacePage", () => {
     const param = "first_parameter";
     const paramValue = "It works!";
     jest
-      .spyOn(client.api, "getTemplateVersionRichParameters")
+      .spyOn(API, "getTemplateVersionRichParameters")
       .mockResolvedValueOnce([MockTemplateVersionParameter1]);
 
     renderWithAuth(<CreateWorkspacePage />, {
@@ -91,7 +89,7 @@ describe("CreateWorkspacePage", () => {
 
   it("rich parameter: number validation fails", async () => {
     jest
-      .spyOn(client.api, "getTemplateVersionRichParameters")
+      .spyOn(API, "getTemplateVersionRichParameters")
       .mockResolvedValueOnce([
         MockTemplateVersionParameter1,
         MockTemplateVersionParameter2,
@@ -126,7 +124,7 @@ describe("CreateWorkspacePage", () => {
 
   it("rich parameter: string validation fails", async () => {
     jest
-      .spyOn(client.api, "getTemplateVersionRichParameters")
+      .spyOn(API, "getTemplateVersionRichParameters")
       .mockResolvedValueOnce([
         MockTemplateVersionParameter1,
         MockTemplateVersionParameter3,
@@ -159,16 +157,14 @@ describe("CreateWorkspacePage", () => {
   });
 
   it("rich parameter: number validation fails with custom error", async () => {
-    jest
-      .spyOn(client.api, "getTemplateVersionRichParameters")
-      .mockResolvedValueOnce([
-        MockTemplateVersionParameter1,
-        {
-          ...MockTemplateVersionParameter2,
-          validation_error: "These are values: {min}, {max}, and {value}.",
-          validation_monotonic: undefined, // only needs min-max rules
-        },
-      ]);
+    jest.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce([
+      MockTemplateVersionParameter1,
+      {
+        ...MockTemplateVersionParameter2,
+        validation_error: "These are values: {min}, {max}, and {value}.",
+        validation_monotonic: undefined, // only needs min-max rules
+      },
+    ]);
 
     renderCreateWorkspacePage();
     await waitForLoaderToBeRemoved();
@@ -191,16 +187,14 @@ describe("CreateWorkspacePage", () => {
 
   it("external auth authenticates and succeeds", async () => {
     jest
-      .spyOn(client.api, "getWorkspaceQuota")
+      .spyOn(API, "getWorkspaceQuota")
       .mockResolvedValueOnce(MockWorkspaceQuota);
     jest
-      .spyOn(client.api, "getUsers")
+      .spyOn(API, "getUsers")
       .mockResolvedValueOnce({ users: [MockUser], count: 1 });
+    jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace);
     jest
-      .spyOn(client.api, "createWorkspace")
-      .mockResolvedValueOnce(MockWorkspace);
-    jest
-      .spyOn(client.api, "getTemplateVersionExternalAuth")
+      .spyOn(API, "getTemplateVersionExternalAuth")
       .mockResolvedValue([MockTemplateVersionExternalAuthGithub]);
 
     renderCreateWorkspacePage();
@@ -216,7 +210,7 @@ describe("CreateWorkspacePage", () => {
     await userEvent.click(githubButton);
 
     jest
-      .spyOn(client.api, "getTemplateVersionExternalAuth")
+      .spyOn(API, "getTemplateVersionExternalAuth")
       .mockResolvedValue([MockTemplateVersionExternalAuthGithubAuthenticated]);
 
     await screen.findByText(
@@ -229,7 +223,7 @@ describe("CreateWorkspacePage", () => {
     await userEvent.click(submitButton);
 
     await waitFor(() =>
-      expect(client.api.createWorkspace).toBeCalledWith(
+      expect(API.createWorkspace).toBeCalledWith(
         MockUser.organization_ids[0],
         MockUser.id,
         expect.objectContaining({
@@ -241,16 +235,14 @@ describe("CreateWorkspacePage", () => {
 
   it("optional external auth is optional", async () => {
     jest
-      .spyOn(client.api, "getWorkspaceQuota")
+      .spyOn(API, "getWorkspaceQuota")
       .mockResolvedValueOnce(MockWorkspaceQuota);
     jest
-      .spyOn(client.api, "getUsers")
+      .spyOn(API, "getUsers")
       .mockResolvedValueOnce({ users: [MockUser], count: 1 });
+    jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace);
     jest
-      .spyOn(client.api, "createWorkspace")
-      .mockResolvedValueOnce(MockWorkspace);
-    jest
-      .spyOn(client.api, "getTemplateVersionExternalAuth")
+      .spyOn(API, "getTemplateVersionExternalAuth")
       .mockResolvedValue([
         { ...MockTemplateVersionExternalAuthGithub, optional: true },
       ]);
@@ -271,7 +263,7 @@ describe("CreateWorkspacePage", () => {
     await userEvent.click(submitButton);
 
     await waitFor(() =>
-      expect(client.api.createWorkspace).toBeCalledWith(
+      expect(API.createWorkspace).toBeCalledWith(
         MockUser.organization_ids[0],
         MockUser.id,
         expect.objectContaining({
@@ -284,7 +276,7 @@ describe("CreateWorkspacePage", () => {
   it("auto create a workspace if uses mode=auto", async () => {
     const param = "first_parameter";
     const paramValue = "It works!";
-    const createWorkspaceSpy = jest.spyOn(client.api, "createWorkspace");
+    const createWorkspaceSpy = jest.spyOn(API, "createWorkspace");
 
     renderWithAuth(<CreateWorkspacePage />, {
       route:
@@ -315,10 +307,10 @@ describe("CreateWorkspacePage", () => {
   it("disables mode=auto if a required external auth provider is not connected", async () => {
     const param = "first_parameter";
     const paramValue = "It works!";
-    const createWorkspaceSpy = jest.spyOn(client.api, "createWorkspace");
+    const createWorkspaceSpy = jest.spyOn(API, "createWorkspace");
 
     const externalAuthSpy = jest
-      .spyOn(client.api, "getTemplateVersionExternalAuth")
+      .spyOn(API, "getTemplateVersionExternalAuth")
       .mockResolvedValue([MockTemplateVersionExternalAuthGithub]);
 
     renderWithAuth(<CreateWorkspacePage />, {
@@ -344,7 +336,7 @@ describe("CreateWorkspacePage", () => {
   it("auto create a workspace if uses mode=auto and version=version-id", async () => {
     const param = "first_parameter";
     const paramValue = "It works!";
-    const createWorkspaceSpy = jest.spyOn(client.api, "createWorkspace");
+    const createWorkspaceSpy = jest.spyOn(API, "createWorkspace");
 
     renderWithAuth(<CreateWorkspacePage />, {
       route:
diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx
index 48dd90aa3232d..df0bb38891f03 100644
--- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx
+++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx
@@ -2,7 +2,7 @@ import { type FC, useCallback, useEffect, useState, useRef } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation, useQuery, useQueryClient } from "react-query";
 import { useNavigate, useParams, useSearchParams } from "react-router-dom";
-import { client } from "api/api";
+import { API } from "api/api";
 import type { ApiErrorResponse } from "api/errors";
 import { checkAuthorization } from "api/queries/authCheck";
 import {
@@ -99,7 +99,7 @@ const CreateWorkspacePage: FC = () => {
   const autofillEnabled = experiments.includes("auto-fill-parameters");
   const userParametersQuery = useQuery({
     queryKey: ["userParameters"],
-    queryFn: () => client.api.getUserParameters(templateQuery.data!.id),
+    queryFn: () => API.getUserParameters(templateQuery.data!.id),
     enabled: autofillEnabled && templateQuery.isSuccess,
   });
   const autofillParameters = getAutofillParameters(
diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx
index f470446e4655e..b40d7a201dd55 100644
--- a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx
+++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx
@@ -2,7 +2,7 @@ import type { FC } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation } from "react-query";
 import { useNavigate } from "react-router-dom";
-import { client } from "api/api";
+import { API } from "api/api";
 import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
 import { pageTitle } from "utils/page";
 import { AddNewLicensePageView } from "./AddNewLicensePageView";
@@ -14,7 +14,7 @@ const AddNewLicensePage: FC = () => {
     mutate: saveLicenseKeyApi,
     isLoading: isCreating,
     error: savingLicenseError,
-  } = useMutation(client.api.createLicense, {
+  } = useMutation(API.createLicense, {
     onSuccess: () => {
       displaySuccess("You have successfully added a license");
       navigate("/deployment/licenses?success=true");
diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx
index bdcae4546e332..c3e353b63074e 100644
--- a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx
+++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx
@@ -3,7 +3,7 @@ import { Helmet } from "react-helmet-async";
 import { useMutation, useQuery, useQueryClient } from "react-query";
 import { useSearchParams } from "react-router-dom";
 import useToggle from "react-use/lib/useToggle";
-import { client } from "api/api";
+import { API } from "api/api";
 import { getErrorMessage } from "api/errors";
 import { entitlements, refreshEntitlements } from "api/queries/entitlements";
 import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
@@ -36,7 +36,7 @@ const LicensesSettingsPage: FC = () => {
   }, [entitlementsQuery.error]);
 
   const { mutate: removeLicenseApi, isLoading: isRemovingLicense } =
-    useMutation(client.api.removeLicense, {
+    useMutation(API.removeLicense, {
       onSuccess: () => {
         displaySuccess("Successfully removed license");
         void queryClient.invalidateQueries(["licenses"]);
@@ -48,7 +48,7 @@ const LicensesSettingsPage: FC = () => {
 
   const { data: licenses, isLoading } = useQuery({
     queryKey: ["licenses"],
-    queryFn: () => client.api.getLicenses(),
+    queryFn: () => API.getLicenses(),
   });
 
   useEffect(() => {
diff --git a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.test.tsx b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.test.tsx
index e1afd45f64531..d2867a80085b6 100644
--- a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.test.tsx
+++ b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.test.tsx
@@ -1,6 +1,6 @@
 import { screen } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import { client } from "api/api";
+import { API } from "api/api";
 import { TemplateLayout } from "pages/TemplatePage/TemplateLayout";
 import {
   MockTemplate,
@@ -15,7 +15,7 @@ import TemplateEmbedPage from "./TemplateEmbedPage";
 
 test("Users can fill the parameters and copy the open in coder url", async () => {
   jest
-    .spyOn(client.api, "getTemplateVersionRichParameters")
+    .spyOn(API, "getTemplateVersionRichParameters")
     .mockResolvedValue([parameter1, parameter2]);
 
   renderWithAuth(
diff --git a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx
index 6263126eda7b7..643f9c166fb7b 100644
--- a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx
+++ b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx
@@ -7,7 +7,7 @@ import RadioGroup from "@mui/material/RadioGroup";
 import { type FC, useEffect, useState } from "react";
 import { Helmet } from "react-helmet-async";
 import { useQuery } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import type { Template, TemplateVersionParameter } from "api/typesGenerated";
 import { FormSection, VerticalForm } from "components/Form/Form";
 import { Loader } from "components/Loader/Loader";
@@ -25,7 +25,7 @@ const TemplateEmbedPage: FC = () => {
   const { data: templateParameters } = useQuery({
     queryKey: ["template", template.id, "embed"],
     queryFn: () =>
-      client.api.getTemplateVersionRichParameters(template.active_version_id),
+      API.getTemplateVersionRichParameters(template.active_version_id),
   });
 
   return (
diff --git a/site/src/pages/TemplatePage/TemplateLayout.tsx b/site/src/pages/TemplatePage/TemplateLayout.tsx
index 23c663e32ba1c..e388c81feb27e 100644
--- a/site/src/pages/TemplatePage/TemplateLayout.tsx
+++ b/site/src/pages/TemplatePage/TemplateLayout.tsx
@@ -7,7 +7,7 @@ import {
 } from "react";
 import { useQuery } from "react-query";
 import { Outlet, useLocation, useNavigate, useParams } from "react-router-dom";
-import { client } from "api/api";
+import { API } from "api/api";
 import type { AuthorizationRequest } from "api/typesGenerated";
 import { ErrorAlert } from "components/Alert/ErrorAlert";
 import { Loader } from "components/Loader/Loader";
@@ -35,14 +35,11 @@ const templatePermissions = (
 });
 
 const fetchTemplate = async (organizationId: string, templateName: string) => {
-  const template = await client.api.getTemplateByName(
-    organizationId,
-    templateName,
-  );
+  const template = await API.getTemplateByName(organizationId, templateName);
 
   const [activeVersion, permissions] = await Promise.all([
-    client.api.getTemplateVersion(template.active_version_id),
-    client.api.checkAuthorization({
+    API.getTemplateVersion(template.active_version_id),
+    API.checkAuthorization({
       checks: templatePermissions(template.id),
     }),
   ]);
diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx
index fbd1ff993c802..226f6d7fa07fb 100644
--- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx
+++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx
@@ -1,7 +1,7 @@
 import type { FC } from "react";
 import { Helmet } from "react-helmet-async";
 import { useQuery } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout";
 import { getTemplatePageTitle } from "../utils";
 import { TemplateSummaryPageView } from "./TemplateSummaryPageView";
@@ -10,7 +10,7 @@ export const TemplateSummaryPage: FC = () => {
   const { template, activeVersion } = useTemplateLayoutContext();
   const { data: resources } = useQuery({
     queryKey: ["templates", template.id, "resources"],
-    queryFn: () => client.api.getTemplateVersionResources(activeVersion.id),
+    queryFn: () => API.getTemplateVersionResources(activeVersion.id),
   });
 
   return (
diff --git a/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx b/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx
index d08de7669e505..df05f167e776e 100644
--- a/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx
+++ b/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx
@@ -1,7 +1,7 @@
 import { useState } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation, useQuery } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import { getErrorMessage } from "api/errors";
 import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
 import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
@@ -13,7 +13,7 @@ const TemplateVersionsPage = () => {
   const { template, permissions } = useTemplateLayoutContext();
   const { data } = useQuery({
     queryKey: ["template", "versions", template.id],
-    queryFn: () => client.api.getTemplateVersions(template.id),
+    queryFn: () => API.getTemplateVersions(template.id),
   });
   // We use this to update the active version in the UI without having to refetch the template
   const [latestActiveVersion, setLatestActiveVersion] = useState(
@@ -21,7 +21,7 @@ const TemplateVersionsPage = () => {
   );
   const { mutate: promoteVersion, isLoading: isPromoting } = useMutation({
     mutationFn: (templateVersionId: string) => {
-      return client.api.updateActiveTemplateVersion(template.id, {
+      return API.updateActiveTemplateVersion(template.id, {
         id: templateVersionId,
       });
     },
@@ -37,7 +37,7 @@ const TemplateVersionsPage = () => {
 
   const { mutate: archiveVersion, isLoading: isArchiving } = useMutation({
     mutationFn: (templateVersionId: string) => {
-      return client.api.archiveTemplateVersion(templateVersionId);
+      return API.archiveTemplateVersion(templateVersionId);
     },
     onSuccess: async () => {
       // The reload is unfortunate. When a version is archived, we should hide
diff --git a/site/src/pages/TemplatePage/useDeletionDialogState.test.ts b/site/src/pages/TemplatePage/useDeletionDialogState.test.ts
index 5f8421f2d6584..d0dab66bbd975 100644
--- a/site/src/pages/TemplatePage/useDeletionDialogState.test.ts
+++ b/site/src/pages/TemplatePage/useDeletionDialogState.test.ts
@@ -1,5 +1,5 @@
 import { act, renderHook, waitFor } from "@testing-library/react";
-import { client } from "api/api";
+import { API } from "api/api";
 import { MockTemplate } from "testHelpers/entities";
 import { useDeletionDialogState } from "./useDeletionDialogState";
 
@@ -23,9 +23,9 @@ test("confirm template deletion", async () => {
   expect(result.current.isDeleteDialogOpen).toBeTruthy();
 
   // Confirm delete
-  jest.spyOn(client.api, "deleteTemplate");
+  jest.spyOn(API, "deleteTemplate");
   await act(async () => result.current.confirmDelete());
-  await waitFor(() => expect(client.api.deleteTemplate).toBeCalledTimes(1));
+  await waitFor(() => expect(API.deleteTemplate).toBeCalledTimes(1));
   expect(onDeleteTemplate).toBeCalledTimes(1);
 });
 
diff --git a/site/src/pages/TemplatePage/useDeletionDialogState.ts b/site/src/pages/TemplatePage/useDeletionDialogState.ts
index 8185d128efa20..cc7e55670e2be 100644
--- a/site/src/pages/TemplatePage/useDeletionDialogState.ts
+++ b/site/src/pages/TemplatePage/useDeletionDialogState.ts
@@ -1,5 +1,5 @@
 import { useState } from "react";
-import { client } from "api/api";
+import { API } from "api/api";
 import { getErrorMessage } from "api/errors";
 import { displayError } from "components/GlobalSnackbar/utils";
 
@@ -27,7 +27,7 @@ export const useDeletionDialogState = (
   const confirmDelete = async () => {
     try {
       setState({ status: "deleting" });
-      await client.api.deleteTemplate(templateId);
+      await API.deleteTemplate(templateId);
       onDelete();
     } catch (e) {
       setState({ status: "confirming" });
diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx
index 4b0120a29c546..716322f982288 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx
@@ -1,7 +1,7 @@
 import { screen, waitFor } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import { http, HttpResponse } from "msw";
-import { client, withDefaultFeatures } from "api/api";
+import { API, withDefaultFeatures } from "api/api";
 import type { Template, UpdateTemplateMeta } from "api/typesGenerated";
 import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter";
 import { MockEntitlements, MockTemplate } from "testHelpers/entities";
@@ -104,14 +104,12 @@ const fillAndSubmitForm = async ({
 describe("TemplateSettingsPage", () => {
   it("succeeds", async () => {
     await renderTemplateSettingsPage();
-    jest.spyOn(client.api, "updateTemplateMeta").mockResolvedValueOnce({
+    jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({
       ...MockTemplate,
       ...validFormValues,
     });
     await fillAndSubmitForm(validFormValues);
-    await waitFor(() =>
-      expect(client.api.updateTemplateMeta).toBeCalledTimes(1),
-    );
+    await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1));
   });
 
   it("allows a description of 128 chars", () => {
@@ -146,10 +144,7 @@ describe("TemplateSettingsPage", () => {
           });
         }),
       );
-      const updateTemplateMetaSpy = jest.spyOn(
-        client.api,
-        "updateTemplateMeta",
-      );
+      const updateTemplateMetaSpy = jest.spyOn(API, "updateTemplateMeta");
       const deprecationMessage = "This template is deprecated";
 
       await renderTemplateSettingsPage();
@@ -174,10 +169,7 @@ describe("TemplateSettingsPage", () => {
           });
         }),
       );
-      const updateTemplateMetaSpy = jest.spyOn(
-        client.api,
-        "updateTemplateMeta",
-      );
+      const updateTemplateMetaSpy = jest.spyOn(API, "updateTemplateMeta");
 
       await renderTemplateSettingsPage();
       await deprecateTemplate(
diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx
index 4d6bd9a21329c..4438cec0bea06 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx
@@ -2,7 +2,7 @@ import type { FC } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation, useQueryClient } from "react-query";
 import { useNavigate, useParams } from "react-router-dom";
-import { client } from "api/api";
+import { API } from "api/api";
 import { templateByNameKey } from "api/queries/templates";
 import type { UpdateTemplateMeta } from "api/typesGenerated";
 import { displaySuccess } from "components/GlobalSnackbar/utils";
@@ -31,7 +31,7 @@ export const TemplateSettingsPage: FC = () => {
     error: submitError,
   } = useMutation(
     (data: UpdateTemplateMeta) => {
-      return client.api.updateTemplateMeta(template.id, data);
+      return API.updateTemplateMeta(template.id, data);
     },
     {
       onSuccess: async (data) => {
diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx
index 61e474be7ee84..48d9d8ef44e4f 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx
@@ -1,6 +1,6 @@
 import { screen, waitFor } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import { client } from "api/api";
+import { API } from "api/api";
 import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter";
 import {
   MockEntitlementsWithScheduling,
@@ -127,38 +127,38 @@ function waitForWithCutoff(callback: () => void | Promise<void>) {
 describe("TemplateSchedulePage", () => {
   beforeEach(() => {
     jest
-      .spyOn(client.api, "getEntitlements")
+      .spyOn(API, "getEntitlements")
       .mockResolvedValue(MockEntitlementsWithScheduling);
   });
 
   it("Calls the API when user fills in and submits a form", async () => {
     await renderTemplateSchedulePage();
-    jest.spyOn(client.api, "updateTemplateMeta").mockResolvedValueOnce({
+    jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({
       ...MockTemplate,
       ...validFormValues,
     });
 
     await fillAndSubmitForm(validFormValues);
     await waitForWithCutoff(() =>
-      expect(client.api.updateTemplateMeta).toBeCalledTimes(1),
+      expect(API.updateTemplateMeta).toBeCalledTimes(1),
     );
   });
 
   test("default is converted to and from hours", async () => {
     await renderTemplateSchedulePage();
 
-    jest.spyOn(client.api, "updateTemplateMeta").mockResolvedValueOnce({
+    jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({
       ...MockTemplate,
       ...validFormValues,
     });
 
     await fillAndSubmitForm(validFormValues);
     await waitForWithCutoff(() =>
-      expect(client.api.updateTemplateMeta).toBeCalledTimes(1),
+      expect(API.updateTemplateMeta).toBeCalledTimes(1),
     );
 
     await waitForWithCutoff(() => {
-      expect(client.api.updateTemplateMeta).toBeCalledWith(
+      expect(API.updateTemplateMeta).toBeCalledWith(
         "test-template",
         expect.objectContaining({
           default_ttl_ms: (validFormValues.default_ttl_ms || 0) * 3600000,
@@ -170,18 +170,18 @@ describe("TemplateSchedulePage", () => {
   test("failure, dormancy, and dormancy auto-deletion converted to and from days", async () => {
     await renderTemplateSchedulePage();
 
-    jest.spyOn(client.api, "updateTemplateMeta").mockResolvedValueOnce({
+    jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({
       ...MockTemplate,
       ...validFormValues,
     });
 
     await fillAndSubmitForm(validFormValues);
     await waitForWithCutoff(() =>
-      expect(client.api.updateTemplateMeta).toBeCalledTimes(1),
+      expect(API.updateTemplateMeta).toBeCalledTimes(1),
     );
 
     await waitForWithCutoff(() => {
-      expect(client.api.updateTemplateMeta).toBeCalledWith(
+      expect(API.updateTemplateMeta).toBeCalledWith(
         "test-template",
         expect.objectContaining({
           failure_ttl_ms: (validFormValues.failure_ttl_ms || 0) * 86400000,
diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx
index 4c420bae5568e..db37ed32dbcc3 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx
@@ -2,7 +2,7 @@ import type { FC } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation, useQueryClient } from "react-query";
 import { useNavigate, useParams } from "react-router-dom";
-import { client } from "api/api";
+import { API } from "api/api";
 import { templateByNameKey } from "api/queries/templates";
 import type { UpdateTemplateMeta } from "api/typesGenerated";
 import { displaySuccess } from "components/GlobalSnackbar/utils";
@@ -27,8 +27,7 @@ const TemplateSchedulePage: FC = () => {
     isLoading: isSubmitting,
     error: submitError,
   } = useMutation(
-    (data: UpdateTemplateMeta) =>
-      client.api.updateTemplateMeta(template.id, data),
+    (data: UpdateTemplateMeta) => API.updateTemplateMeta(template.id, data),
     {
       onSuccess: async () => {
         await queryClient.invalidateQueries(
diff --git a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx
index 99047c540d352..a99d599dd3947 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx
@@ -1,6 +1,6 @@
 import { screen } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import { client } from "api/api";
+import { API } from "api/api";
 import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter";
 import {
   MockTemplate,
@@ -32,14 +32,12 @@ const renderTemplateVariablesPage = async () => {
 
 describe("TemplateVariablesPage", () => {
   it("renders with variables", async () => {
+    jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate);
     jest
-      .spyOn(client.api, "getTemplateByName")
-      .mockResolvedValueOnce(MockTemplate);
-    jest
-      .spyOn(client.api, "getTemplateVersion")
+      .spyOn(API, "getTemplateVersion")
       .mockResolvedValueOnce(MockTemplateVersion);
     jest
-      .spyOn(client.api, "getTemplateVersionVariables")
+      .spyOn(API, "getTemplateVersionVariables")
       .mockResolvedValueOnce([
         MockTemplateVersionVariable1,
         MockTemplateVersionVariable2,
@@ -59,26 +57,22 @@ describe("TemplateVariablesPage", () => {
   });
 
   it("user submits the form successfully", async () => {
+    jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate);
     jest
-      .spyOn(client.api, "getTemplateByName")
-      .mockResolvedValueOnce(MockTemplate);
-    jest
-      .spyOn(client.api, "getTemplateVersion")
+      .spyOn(API, "getTemplateVersion")
       .mockResolvedValue(MockTemplateVersion);
     jest
-      .spyOn(client.api, "getTemplateVersionVariables")
+      .spyOn(API, "getTemplateVersionVariables")
       .mockResolvedValueOnce([
         MockTemplateVersionVariable1,
         MockTemplateVersionVariable2,
       ]);
     jest
-      .spyOn(client.api, "createTemplateVersion")
+      .spyOn(API, "createTemplateVersion")
       .mockResolvedValueOnce(MockTemplateVersion2);
-    jest
-      .spyOn(client.api, "updateActiveTemplateVersion")
-      .mockResolvedValueOnce({
-        message: "done",
-      });
+    jest.spyOn(API, "updateActiveTemplateVersion").mockResolvedValueOnce({
+      message: "done",
+    });
 
     await renderTemplateVariablesPage();
 
diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx
index 22b172c9cd9ed..cd07c9688ea36 100644
--- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx
+++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx
@@ -26,7 +26,7 @@ import type { MonacoEditorProps } from "./MonacoEditor";
 import { Language } from "./PublishTemplateVersionDialog";
 import TemplateVersionEditorPage from "./TemplateVersionEditorPage";
 
-const { client } = api;
+const { API } = api;
 
 // For some reason this component in Jest is throwing a MUI style warning so,
 // since we don't need it for this test, we can mock it out
@@ -74,8 +74,8 @@ const buildTemplateVersion = async (
   user: UserEvent,
   topbar: HTMLElement,
 ) => {
-  jest.spyOn(client.api, "uploadFile").mockResolvedValueOnce({ hash: "hash" });
-  jest.spyOn(client.api, "createTemplateVersion").mockResolvedValue({
+  jest.spyOn(API, "uploadFile").mockResolvedValueOnce({ hash: "hash" });
+  jest.spyOn(API, "createTemplateVersion").mockResolvedValue({
     ...templateVersion,
     job: {
       ...templateVersion.job,
@@ -83,7 +83,7 @@ const buildTemplateVersion = async (
     },
   });
   jest
-    .spyOn(client.api, "getTemplateVersionByName")
+    .spyOn(API, "getTemplateVersionByName")
     .mockResolvedValue(templateVersion);
   jest
     .spyOn(api, "watchBuildLogsByTemplateVersionId")
@@ -118,10 +118,10 @@ test("Use custom name, message and set it as active when publishing", async () =
 
   // Publish
   const patchTemplateVersion = jest
-    .spyOn(client.api, "patchTemplateVersion")
+    .spyOn(API, "patchTemplateVersion")
     .mockResolvedValue(newTemplateVersion);
   const updateActiveTemplateVersion = jest
-    .spyOn(client.api, "updateActiveTemplateVersion")
+    .spyOn(API, "updateActiveTemplateVersion")
     .mockResolvedValue({ message: "" });
   const publishButton = within(topbar).getByRole("button", {
     name: "Publish",
@@ -164,10 +164,10 @@ test("Do not mark as active if promote is not checked", async () => {
 
   // Publish
   const patchTemplateVersion = jest
-    .spyOn(client.api, "patchTemplateVersion")
+    .spyOn(API, "patchTemplateVersion")
     .mockResolvedValue(newTemplateVersion);
   const updateActiveTemplateVersion = jest
-    .spyOn(client.api, "updateActiveTemplateVersion")
+    .spyOn(API, "updateActiveTemplateVersion")
     .mockResolvedValue({ message: "" });
   const publishButton = within(topbar).getByRole("button", {
     name: "Publish",
@@ -209,7 +209,7 @@ test("Patch request is not send when there are no changes", async () => {
 
   // Publish
   const patchTemplateVersion = jest
-    .spyOn(client.api, "patchTemplateVersion")
+    .spyOn(API, "patchTemplateVersion")
     .mockResolvedValue(newTemplateVersion);
   const publishButton = within(topbar).getByRole("button", {
     name: "Publish",
diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx
index 97e752c799459..3a622630cd770 100644
--- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx
+++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx
@@ -2,7 +2,7 @@ import { type FC, useEffect, useState } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation, useQuery, useQueryClient } from "react-query";
 import { useNavigate, useParams, useSearchParams } from "react-router-dom";
-import { client } from "api/api";
+import { API } from "api/api";
 import { file, uploadFile } from "api/queries/files";
 import {
   createTemplateVersion,
@@ -323,12 +323,12 @@ const publishVersion = async (options: {
   const publishActions: Promise<unknown>[] = [];
 
   if (haveChanges) {
-    publishActions.push(client.api.patchTemplateVersion(version.id, data));
+    publishActions.push(API.patchTemplateVersion(version.id, data));
   }
 
   if (isActiveVersion) {
     publishActions.push(
-      client.api.updateActiveTemplateVersion(version.template_id!, {
+      API.updateActiveTemplateVersion(version.template_id!, {
         id: version.id,
       }),
     );
diff --git a/site/src/pages/TerminalPage/TerminalPage.test.tsx b/site/src/pages/TerminalPage/TerminalPage.test.tsx
index 538087ca1b281..26112b743d1e7 100644
--- a/site/src/pages/TerminalPage/TerminalPage.test.tsx
+++ b/site/src/pages/TerminalPage/TerminalPage.test.tsx
@@ -2,7 +2,7 @@ import "jest-canvas-mock";
 import { waitFor } from "@testing-library/react";
 import WS from "jest-websocket-mock";
 import { HttpResponse, http } from "msw";
-import { client } from "api/api";
+import { API } from "api/api";
 import {
   MockUser,
   MockWorkspace,
@@ -56,7 +56,7 @@ describe("TerminalPage", () => {
 
   it("loads the right workspace data", async () => {
     jest
-      .spyOn(client.api, "getWorkspaceByOwnerAndName")
+      .spyOn(API, "getWorkspaceByOwnerAndName")
       .mockResolvedValue(MockWorkspace);
     new WS(
       `ws://localhost/api/v2/workspaceagents/${MockWorkspaceAgent.id}/pty`,
@@ -65,7 +65,7 @@ describe("TerminalPage", () => {
       `/${MockUser.username}/${MockWorkspace.name}/terminal`,
     );
     await waitFor(() => {
-      expect(client.api.getWorkspaceByOwnerAndName).toHaveBeenCalledWith(
+      expect(API.getWorkspaceByOwnerAndName).toHaveBeenCalledWith(
         MockUser.username,
         MockWorkspace.name,
         { include_deleted: true },
diff --git a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx
index 52a53b8a0803e..7687e95e90a49 100644
--- a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx
+++ b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx
@@ -1,5 +1,5 @@
 import { fireEvent, screen, waitFor } from "@testing-library/react";
-import { client } from "api/api";
+import { API } from "api/api";
 import { mockApiError } from "testHelpers/entities";
 import { renderWithAuth } from "testHelpers/renderHelpers";
 import * as AccountForm from "./AccountForm";
@@ -25,36 +25,34 @@ const fillAndSubmitForm = async () => {
 describe("AccountPage", () => {
   describe("when it is a success", () => {
     it("shows the success message", async () => {
-      jest
-        .spyOn(client.api, "updateProfile")
-        .mockImplementationOnce((userId, data) =>
-          Promise.resolve({
-            id: userId,
-            email: "user@coder.com",
-            created_at: new Date().toISOString(),
-            status: "active",
-            organization_ids: ["123"],
-            roles: [],
-            avatar_url: "",
-            last_seen_at: new Date().toISOString(),
-            login_type: "password",
-            theme_preference: "",
-            ...data,
-          }),
-        );
+      jest.spyOn(API, "updateProfile").mockImplementationOnce((userId, data) =>
+        Promise.resolve({
+          id: userId,
+          email: "user@coder.com",
+          created_at: new Date().toISOString(),
+          status: "active",
+          organization_ids: ["123"],
+          roles: [],
+          avatar_url: "",
+          last_seen_at: new Date().toISOString(),
+          login_type: "password",
+          theme_preference: "",
+          ...data,
+        }),
+      );
       renderWithAuth(<AccountPage />);
       await fillAndSubmitForm();
 
       const successMessage = await screen.findByText("Updated settings.");
       expect(successMessage).toBeDefined();
-      expect(client.api.updateProfile).toBeCalledTimes(1);
-      expect(client.api.updateProfile).toBeCalledWith("me", newData);
+      expect(API.updateProfile).toBeCalledTimes(1);
+      expect(API.updateProfile).toBeCalledWith("me", newData);
     });
   });
 
   describe("when the username is already taken", () => {
     it("shows an error", async () => {
-      jest.spyOn(client.api, "updateProfile").mockRejectedValueOnce(
+      jest.spyOn(API, "updateProfile").mockRejectedValueOnce(
         mockApiError({
           message: "Invalid profile",
           validations: [
@@ -70,14 +68,14 @@ describe("AccountPage", () => {
         "Username is already in use",
       );
       expect(errorMessage).toBeDefined();
-      expect(client.api.updateProfile).toBeCalledTimes(1);
-      expect(client.api.updateProfile).toBeCalledWith("me", newData);
+      expect(API.updateProfile).toBeCalledTimes(1);
+      expect(API.updateProfile).toBeCalledWith("me", newData);
     });
   });
 
   describe("when it is an unknown error", () => {
     it("shows a generic error message", async () => {
-      jest.spyOn(client.api, "updateProfile").mockRejectedValueOnce({
+      jest.spyOn(API, "updateProfile").mockRejectedValueOnce({
         data: "unknown error",
       });
 
@@ -86,8 +84,8 @@ describe("AccountPage", () => {
 
       const errorMessage = await screen.findByText("Something went wrong.");
       expect(errorMessage).toBeDefined();
-      expect(client.api.updateProfile).toBeCalledTimes(1);
-      expect(client.api.updateProfile).toBeCalledWith("me", newData);
+      expect(API.updateProfile).toBeCalledTimes(1);
+      expect(API.updateProfile).toBeCalledWith("me", newData);
     });
   });
 });
diff --git a/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx b/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx
index 8cca935805d8e..5cb6ad6d3edee 100644
--- a/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx
+++ b/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx
@@ -1,6 +1,6 @@
 import { screen } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import { client } from "api/api";
+import { API } from "api/api";
 import { MockUser } from "testHelpers/entities";
 import { renderWithAuth } from "testHelpers/renderHelpers";
 import { AppearancePage } from "./AppearancePage";
@@ -9,7 +9,7 @@ describe("appearance page", () => {
   it("does nothing when selecting current theme", async () => {
     renderWithAuth(<AppearancePage />);
 
-    jest.spyOn(client.api, "updateAppearanceSettings").mockResolvedValueOnce({
+    jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({
       ...MockUser,
       theme_preference: "dark",
     });
@@ -18,13 +18,13 @@ describe("appearance page", () => {
     await userEvent.click(dark);
 
     // Check if the API was called correctly
-    expect(client.api.updateAppearanceSettings).toBeCalledTimes(0);
+    expect(API.updateAppearanceSettings).toBeCalledTimes(0);
   });
 
   it("changes theme to dark blue", async () => {
     renderWithAuth(<AppearancePage />);
 
-    jest.spyOn(client.api, "updateAppearanceSettings").mockResolvedValueOnce({
+    jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({
       ...MockUser,
       theme_preference: "darkBlue",
     });
@@ -33,8 +33,8 @@ describe("appearance page", () => {
     await userEvent.click(darkBlue);
 
     // Check if the API was called correctly
-    expect(client.api.updateAppearanceSettings).toBeCalledTimes(1);
-    expect(client.api.updateAppearanceSettings).toHaveBeenCalledWith("me", {
+    expect(API.updateAppearanceSettings).toBeCalledTimes(1);
+    expect(API.updateAppearanceSettings).toHaveBeenCalledWith("me", {
       theme_preference: "darkBlue",
     });
   });
@@ -42,7 +42,7 @@ describe("appearance page", () => {
   it("changes theme to light", async () => {
     renderWithAuth(<AppearancePage />);
 
-    jest.spyOn(client.api, "updateAppearanceSettings").mockResolvedValueOnce({
+    jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({
       ...MockUser,
       theme_preference: "light",
     });
@@ -51,8 +51,8 @@ describe("appearance page", () => {
     await userEvent.click(light);
 
     // Check if the API was called correctly
-    expect(client.api.updateAppearanceSettings).toBeCalledTimes(1);
-    expect(client.api.updateAppearanceSettings).toHaveBeenCalledWith("me", {
+    expect(API.updateAppearanceSettings).toBeCalledTimes(1);
+    expect(API.updateAppearanceSettings).toHaveBeenCalledWith("me", {
       theme_preference: "light",
     });
   });
diff --git a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx
index e7c46bc466adf..c6e706f98e769 100644
--- a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx
+++ b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx
@@ -1,5 +1,5 @@
 import { fireEvent, screen, within } from "@testing-library/react";
-import { client } from "api/api";
+import { API } from "api/api";
 import { MockGitSSHKey, mockApiError } from "testHelpers/entities";
 import { renderWithAuth } from "testHelpers/renderHelpers";
 import { Language as SSHKeysPageLanguage, SSHKeysPage } from "./SSHKeysPage";
@@ -28,7 +28,7 @@ describe("SSH keys Page", () => {
 
         const newUserSSHKey =
           "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDSC/ouD/LqiT1Rd99vDv/MwUmqzJuinLTMTpk5kVy66";
-        jest.spyOn(client.api, "regenerateUserSSHKey").mockResolvedValueOnce({
+        jest.spyOn(API, "regenerateUserSSHKey").mockResolvedValueOnce({
           ...MockGitSSHKey,
           public_key: newUserSSHKey,
         });
@@ -43,7 +43,7 @@ describe("SSH keys Page", () => {
         await screen.findByText("SSH Key regenerated successfully.");
 
         // Check if the API was called correctly
-        expect(client.api.regenerateUserSSHKey).toBeCalledTimes(1);
+        expect(API.regenerateUserSSHKey).toBeCalledTimes(1);
 
         // Check if the SSH key is updated
         await screen.findByText(newUserSSHKey);
@@ -57,7 +57,7 @@ describe("SSH keys Page", () => {
         // Wait to the ssh be rendered on the screen
         await screen.findByText(MockGitSSHKey.public_key);
 
-        jest.spyOn(client.api, "regenerateUserSSHKey").mockRejectedValueOnce(
+        jest.spyOn(API, "regenerateUserSSHKey").mockRejectedValueOnce(
           mockApiError({
             message: SSHKeysPageLanguage.regenerationError,
           }),
@@ -82,7 +82,7 @@ describe("SSH keys Page", () => {
         expect(alert).toHaveTextContent(SSHKeysPageLanguage.regenerationError);
 
         // Check if the API was called correctly
-        expect(client.api.regenerateUserSSHKey).toBeCalledTimes(1);
+        expect(API.regenerateUserSSHKey).toBeCalledTimes(1);
       });
     });
   });
diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx
index 1665ece78b8b4..8289da7ee9e5b 100644
--- a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx
+++ b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx
@@ -1,6 +1,6 @@
 import { fireEvent, screen, waitFor, within } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import { client } from "api/api";
+import { API } from "api/api";
 import type { OAuthConversionResponse } from "api/typesGenerated";
 import { MockAuthMethodsAll, mockApiError } from "testHelpers/entities";
 import {
@@ -37,34 +37,29 @@ const fillAndSubmitSecurityForm = () => {
 };
 
 beforeEach(() => {
-  jest
-    .spyOn(client.api, "getAuthMethods")
-    .mockResolvedValue(MockAuthMethodsAll);
-  jest.spyOn(client.api, "getUserLoginType").mockResolvedValue({
+  jest.spyOn(API, "getAuthMethods").mockResolvedValue(MockAuthMethodsAll);
+  jest.spyOn(API, "getUserLoginType").mockResolvedValue({
     login_type: "password",
   });
 });
 
 test("update password successfully", async () => {
   jest
-    .spyOn(client.api, "updateUserPassword")
+    .spyOn(API, "updateUserPassword")
     .mockImplementationOnce((_userId, _data) => Promise.resolve(undefined));
   const { user } = await renderPage();
   fillAndSubmitSecurityForm();
 
   const successMessage = await screen.findByText("Updated password.");
   expect(successMessage).toBeDefined();
-  expect(client.api.updateUserPassword).toBeCalledTimes(1);
-  expect(client.api.updateUserPassword).toBeCalledWith(
-    user.id,
-    newSecurityFormValues,
-  );
+  expect(API.updateUserPassword).toBeCalledTimes(1);
+  expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues);
 
   await waitFor(() => expect(window.location).toBeAt("/"));
 });
 
 test("update password with incorrect old password", async () => {
-  jest.spyOn(client.api, "updateUserPassword").mockRejectedValueOnce(
+  jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce(
     mockApiError({
       message: "Incorrect password.",
       validations: [{ detail: "Incorrect password.", field: "old_password" }],
@@ -77,15 +72,12 @@ test("update password with incorrect old password", async () => {
   const errorMessage = await screen.findAllByText("Incorrect password.");
   expect(errorMessage).toBeDefined();
   expect(errorMessage).toHaveLength(2);
-  expect(client.api.updateUserPassword).toBeCalledTimes(1);
-  expect(client.api.updateUserPassword).toBeCalledWith(
-    user.id,
-    newSecurityFormValues,
-  );
+  expect(API.updateUserPassword).toBeCalledTimes(1);
+  expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues);
 });
 
 test("update password with invalid password", async () => {
-  jest.spyOn(client.api, "updateUserPassword").mockRejectedValueOnce(
+  jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce(
     mockApiError({
       message: "Invalid password.",
       validations: [{ detail: "Invalid password.", field: "password" }],
@@ -98,15 +90,12 @@ test("update password with invalid password", async () => {
   const errorMessage = await screen.findAllByText("Invalid password.");
   expect(errorMessage).toBeDefined();
   expect(errorMessage).toHaveLength(2);
-  expect(client.api.updateUserPassword).toBeCalledTimes(1);
-  expect(client.api.updateUserPassword).toBeCalledWith(
-    user.id,
-    newSecurityFormValues,
-  );
+  expect(API.updateUserPassword).toBeCalledTimes(1);
+  expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues);
 });
 
 test("update password when submit returns an unknown error", async () => {
-  jest.spyOn(client.api, "updateUserPassword").mockRejectedValueOnce({
+  jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce({
     data: "unknown error",
   });
 
@@ -115,18 +104,15 @@ test("update password when submit returns an unknown error", async () => {
 
   const errorMessage = await screen.findByText("Something went wrong.");
   expect(errorMessage).toBeDefined();
-  expect(client.api.updateUserPassword).toBeCalledTimes(1);
-  expect(client.api.updateUserPassword).toBeCalledWith(
-    user.id,
-    newSecurityFormValues,
-  );
+  expect(API.updateUserPassword).toBeCalledTimes(1);
+  expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues);
 });
 
 test("change login type to OIDC", async () => {
   const user = userEvent.setup();
   const { user: userData } = await renderPage();
   const convertToOAUTHSpy = jest
-    .spyOn(client.api, "convertToOAUTH")
+    .spyOn(API, "convertToOAUTH")
     .mockResolvedValue({
       state_string: "some-state-string",
       expires_at: "2021-01-01T00:00:00Z",
diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx
index afd0a66498d9f..b3cb38969f1c0 100644
--- a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx
+++ b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx
@@ -1,6 +1,6 @@
 import type { ComponentProps, FC } from "react";
 import { useMutation, useQuery } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import { authMethods, updatePassword } from "api/queries/users";
 import { displaySuccess } from "components/GlobalSnackbar/utils";
 import { Loader } from "components/Loader/Loader";
@@ -19,7 +19,7 @@ export const SecurityPage: FC = () => {
   const authMethodsQuery = useQuery(authMethods());
   const { data: userLoginType } = useQuery({
     queryKey: ["loginType"],
-    queryFn: client.api.getUserLoginType,
+    queryFn: API.getUserLoginType,
   });
   const singleSignOnSection = useSingleSignOnSection();
 
diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx
index 4b8bb9c9b11fd..78d7cfb0cb23f 100644
--- a/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx
+++ b/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx
@@ -7,7 +7,7 @@ import Link from "@mui/material/Link";
 import TextField from "@mui/material/TextField";
 import { type FC, useState } from "react";
 import { useMutation } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import { getErrorMessage } from "api/errors";
 import type {
   AuthMethods,
@@ -52,7 +52,7 @@ export const useSingleSignOnSection = () => {
   const [loginTypeConfirmation, setLoginTypeConfirmation] =
     useState<LoginTypeConfirmation>({ open: false, selectedType: undefined });
 
-  const mutation = useMutation(client.api.convertToOAUTH, {
+  const mutation = useMutation(API.convertToOAUTH, {
     onSuccess: (data) => {
       const loginTypeMsg =
         data.to_type === "github" ? "Github" : "OpenID Connect";
diff --git a/site/src/pages/UserSettingsPage/TokensPage/hooks.ts b/site/src/pages/UserSettingsPage/TokensPage/hooks.ts
index 3e923a89cd5bf..9909888dd0494 100644
--- a/site/src/pages/UserSettingsPage/TokensPage/hooks.ts
+++ b/site/src/pages/UserSettingsPage/TokensPage/hooks.ts
@@ -4,7 +4,7 @@ import {
   useQuery,
   useQueryClient,
 } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import type { TokensFilter } from "api/typesGenerated";
 
 // Load all tokens
@@ -12,10 +12,7 @@ export const useTokensData = ({ include_all }: TokensFilter) => {
   const queryKey = ["tokens", include_all];
   const result = useQuery({
     queryKey,
-    queryFn: () =>
-      client.api.getTokens({
-        include_all,
-      }),
+    queryFn: () => API.getTokens({ include_all }),
   });
 
   return {
@@ -29,7 +26,7 @@ export const useDeleteToken = (queryKey: QueryKey) => {
   const queryClient = useQueryClient();
 
   return useMutation({
-    mutationFn: client.api.deleteToken,
+    mutationFn: API.deleteToken,
     onSuccess: () => {
       // Invalidate and refetch
       void queryClient.invalidateQueries(queryKey);
diff --git a/site/src/pages/UsersPage/UsersPage.test.tsx b/site/src/pages/UsersPage/UsersPage.test.tsx
index 815b36a721b02..ebc5e24a5e6b6 100644
--- a/site/src/pages/UsersPage/UsersPage.test.tsx
+++ b/site/src/pages/UsersPage/UsersPage.test.tsx
@@ -1,7 +1,7 @@
 import { fireEvent, screen, within } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import { HttpResponse, http } from "msw";
-import { client } from "api/api";
+import { API } from "api/api";
 import type { Role } from "api/typesGenerated";
 import {
   MockUser,
@@ -261,7 +261,7 @@ describe("UsersPage", () => {
 
         await resetUserPassword(() => {
           jest
-            .spyOn(client.api, "updateUserPassword")
+            .spyOn(API, "updateUserPassword")
             .mockResolvedValueOnce(undefined);
         });
 
@@ -269,8 +269,8 @@ describe("UsersPage", () => {
         await screen.findByText("Successfully updated the user password.");
 
         // Check if the API was called correctly
-        expect(client.api.updateUserPassword).toBeCalledTimes(1);
-        expect(client.api.updateUserPassword).toBeCalledWith(MockUser.id, {
+        expect(API.updateUserPassword).toBeCalledTimes(1);
+        expect(API.updateUserPassword).toBeCalledWith(MockUser.id, {
           password: expect.any(String),
           old_password: "",
         });
@@ -281,17 +281,15 @@ describe("UsersPage", () => {
         renderPage();
 
         await resetUserPassword(() => {
-          jest
-            .spyOn(client.api, "updateUserPassword")
-            .mockRejectedValueOnce({});
+          jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce({});
         });
 
         // Check if the error message is displayed
         await screen.findByText("Error on resetting the user password.");
 
         // Check if the API was called correctly
-        expect(client.api.updateUserPassword).toBeCalledTimes(1);
-        expect(client.api.updateUserPassword).toBeCalledWith(MockUser.id, {
+        expect(API.updateUserPassword).toBeCalledTimes(1);
+        expect(API.updateUserPassword).toBeCalledWith(MockUser.id, {
           password: expect.any(String),
           old_password: "",
         });
diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx
index 523ef6c808df6..db5628bfc0bb3 100644
--- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx
+++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx
@@ -1,6 +1,6 @@
 import { screen, waitFor } from "@testing-library/react";
 import WS from "jest-websocket-mock";
-import { client } from "api/api";
+import { API } from "api/api";
 import {
   MockWorkspace,
   MockWorkspaceAgent,
@@ -18,7 +18,7 @@ afterEach(() => {
 describe("WorkspaceBuildPage", () => {
   test("gets the right workspace build", async () => {
     const getWorkspaceBuildSpy = jest
-      .spyOn(client.api, "getWorkspaceBuildByNumber")
+      .spyOn(API, "getWorkspaceBuildByNumber")
       .mockResolvedValue(MockWorkspaceBuild);
     renderWithAuth(<WorkspaceBuildPage />, {
       route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}/builds/${MockWorkspace.latest_build.build_number}`,
diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx
index 0345137b691c0..bc3a914a10bb1 100644
--- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx
+++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx
@@ -3,7 +3,7 @@ import type { FC } from "react";
 import { Helmet } from "react-helmet-async";
 import { useQuery } from "react-query";
 import { useParams } from "react-router-dom";
-import { client } from "api/api";
+import { API } from "api/api";
 import { workspaceBuildByNumber } from "api/queries/workspaceBuilds";
 import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs";
 import { pageTitle } from "utils/page";
@@ -26,7 +26,7 @@ export const WorkspaceBuildPage: FC = () => {
   const buildsQuery = useQuery({
     queryKey: ["builds", username, build?.workspace_id],
     queryFn: () => {
-      return client.api.getWorkspaceBuilds(build?.workspace_id ?? "", {
+      return API.getWorkspaceBuilds(build?.workspace_id ?? "", {
         since: dayjs().add(-30, "day").toISOString(),
       });
     },
diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx
index 3299e25b1a522..5916557a1c409 100644
--- a/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx
@@ -5,7 +5,7 @@ import visuallyHidden from "@mui/utils/visuallyHidden";
 import { useFormik } from "formik";
 import type { FC } from "react";
 import { useQuery } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import type {
   TemplateVersionParameter,
   Workspace,
@@ -49,7 +49,7 @@ export const BuildParametersPopover: FC<BuildParametersPopoverProps> = ({
 }) => {
   const { data: parameters } = useQuery({
     queryKey: ["workspace", workspace.id, "parameters"],
-    queryFn: () => client.api.getWorkspaceParameters(workspace),
+    queryFn: () => API.getWorkspaceParameters(workspace),
   });
   const ephemeralParameters = parameters
     ? parameters.templateVersionRichParameters.filter((p) => p.ephemeral)
diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx
index 2ac588f5ce373..757ea44927a3a 100644
--- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx
+++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx
@@ -1,7 +1,7 @@
 import { screen, waitFor, within } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import { HttpResponse, http } from "msw";
-import * as API from "api/api";
+import * as api from "api/api";
 import type { TemplateVersionParameter, Workspace } from "api/typesGenerated";
 import EventSourceMock from "eventsourcemock";
 import {
@@ -22,22 +22,18 @@ import { renderWithAuth } from "testHelpers/renderHelpers";
 import { server } from "testHelpers/server";
 import { WorkspacePage } from "./WorkspacePage";
 
-const { client } = API;
+const { API } = api;
 
 // Renders the workspace page and waits for it be loaded
 const renderWorkspacePage = async (workspace: Workspace) => {
+  jest.spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue(workspace);
+  jest.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate);
+  jest.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce([]);
   jest
-    .spyOn(client.api, "getWorkspaceByOwnerAndName")
-    .mockResolvedValue(workspace);
-  jest.spyOn(client.api, "getTemplate").mockResolvedValueOnce(MockTemplate);
-  jest
-    .spyOn(client.api, "getTemplateVersionRichParameters")
-    .mockResolvedValueOnce([]);
-  jest
-    .spyOn(client.api, "getDeploymentConfig")
+    .spyOn(API, "getDeploymentConfig")
     .mockResolvedValueOnce(MockDeploymentConfig);
   jest
-    .spyOn(API, "watchWorkspaceAgentLogs")
+    .spyOn(api, "watchWorkspaceAgentLogs")
     .mockImplementation((_, options) => {
       options.onDone?.();
       return new WebSocket("");
@@ -93,7 +89,7 @@ describe("WorkspacePage", () => {
   it("requests a delete job when the user presses Delete and confirms", async () => {
     const user = userEvent.setup({ delay: 0 });
     const deleteWorkspaceMock = jest
-      .spyOn(client.api, "deleteWorkspace")
+      .spyOn(API, "deleteWorkspace")
       .mockResolvedValueOnce(MockWorkspaceBuild);
     await renderWorkspacePage(MockWorkspace);
 
@@ -133,7 +129,7 @@ describe("WorkspacePage", () => {
     );
 
     const deleteWorkspaceMock = jest
-      .spyOn(client.api, "deleteWorkspace")
+      .spyOn(API, "deleteWorkspace")
       .mockResolvedValueOnce(MockWorkspaceBuildDelete);
     await renderWorkspacePage(MockFailedWorkspace);
 
@@ -179,7 +175,7 @@ describe("WorkspacePage", () => {
     );
 
     const startWorkspaceMock = jest
-      .spyOn(client.api, "startWorkspace")
+      .spyOn(API, "startWorkspace")
       .mockImplementation(() => Promise.resolve(MockWorkspaceBuild));
 
     await testButton(MockStoppedWorkspace, "Start", startWorkspaceMock);
@@ -187,7 +183,7 @@ describe("WorkspacePage", () => {
 
   it("requests a stop job when the user presses Stop", async () => {
     const stopWorkspaceMock = jest
-      .spyOn(client.api, "stopWorkspace")
+      .spyOn(API, "stopWorkspace")
       .mockResolvedValueOnce(MockWorkspaceBuild);
 
     await testButton(MockWorkspace, "Stop", stopWorkspaceMock);
@@ -195,7 +191,7 @@ describe("WorkspacePage", () => {
 
   it("requests a stop when the user presses Restart", async () => {
     const stopWorkspaceMock = jest
-      .spyOn(client.api, "stopWorkspace")
+      .spyOn(API, "stopWorkspace")
       .mockResolvedValueOnce(MockWorkspaceBuild);
 
     // Render
@@ -221,7 +217,7 @@ describe("WorkspacePage", () => {
     );
 
     const cancelWorkspaceMock = jest
-      .spyOn(client.api, "cancelWorkspaceBuild")
+      .spyOn(API, "cancelWorkspaceBuild")
       .mockImplementation(() => Promise.resolve({ message: "job canceled" }));
 
     await testButton(MockStartingWorkspace, "Cancel", cancelWorkspaceMock);
@@ -230,11 +226,11 @@ describe("WorkspacePage", () => {
   it("requests an update when the user presses Update", async () => {
     // Mocks
     jest
-      .spyOn(client.api, "getWorkspaceByOwnerAndName")
+      .spyOn(API, "getWorkspaceByOwnerAndName")
       .mockResolvedValueOnce(MockOutdatedWorkspace);
 
     const updateWorkspaceMock = jest
-      .spyOn(client.api, "updateWorkspace")
+      .spyOn(API, "updateWorkspace")
       .mockResolvedValueOnce(MockWorkspaceBuild);
 
     // Render
@@ -255,12 +251,12 @@ describe("WorkspacePage", () => {
   it("updates the parameters when they are missing during update", async () => {
     // Mocks
     jest
-      .spyOn(client.api, "getWorkspaceByOwnerAndName")
+      .spyOn(API, "getWorkspaceByOwnerAndName")
       .mockResolvedValueOnce(MockOutdatedWorkspace);
     const updateWorkspaceSpy = jest
-      .spyOn(client.api, "updateWorkspace")
+      .spyOn(API, "updateWorkspace")
       .mockRejectedValueOnce(
-        new API.MissingBuildParameters(
+        new api.MissingBuildParameters(
           [MockTemplateVersionParameter1, MockTemplateVersionParameter2],
           MockOutdatedWorkspace.template_active_version_id,
         ),
@@ -277,7 +273,7 @@ describe("WorkspacePage", () => {
 
     // The update was called
     await waitFor(() => {
-      expect(client.api.updateWorkspace).toBeCalled();
+      expect(API.updateWorkspace).toBeCalled();
       updateWorkspaceSpy.mockClear();
     });
 
@@ -300,7 +296,7 @@ describe("WorkspacePage", () => {
 
     // Check if the update was called using the values from the form
     await waitFor(() => {
-      expect(client.api.updateWorkspace).toBeCalledWith(MockOutdatedWorkspace, [
+      expect(API.updateWorkspace).toBeCalledWith(MockOutdatedWorkspace, [
         {
           name: MockTemplateVersionParameter1.name,
           value: "some-value",
@@ -315,7 +311,7 @@ describe("WorkspacePage", () => {
 
   it("restart the workspace with one time parameters when having the confirmation dialog", async () => {
     localStorage.removeItem(`${MockUser.id}_ignoredWarnings`);
-    jest.spyOn(client.api, "getWorkspaceParameters").mockResolvedValue({
+    jest.spyOn(API, "getWorkspaceParameters").mockResolvedValue({
       templateVersionRichParameters: [
         {
           ...MockTemplateVersionParameter1,
@@ -327,7 +323,7 @@ describe("WorkspacePage", () => {
       ],
       buildParameters: [{ name: "rebuild", value: "false" }],
     });
-    const restartWorkspaceSpy = jest.spyOn(client.api, "restartWorkspace");
+    const restartWorkspaceSpy = jest.spyOn(API, "restartWorkspace");
     const user = userEvent.setup();
     await renderWorkspacePage(MockWorkspace);
     await user.click(screen.getByTestId("build-parameters-button"));
@@ -357,7 +353,7 @@ describe("WorkspacePage", () => {
     const retryDebugButtonRe = /^Debug$/i;
 
     describe("Retries a failed 'Start' transition", () => {
-      const mockStart = jest.spyOn(client.api, "startWorkspace");
+      const mockStart = jest.spyOn(API, "startWorkspace");
       const failedStart: Workspace = {
         ...MockFailedWorkspace,
         latest_build: {
@@ -390,7 +386,7 @@ describe("WorkspacePage", () => {
     });
 
     describe("Retries a failed 'Stop' transition", () => {
-      const mockStop = jest.spyOn(client.api, "stopWorkspace");
+      const mockStop = jest.spyOn(API, "stopWorkspace");
       const failedStop: Workspace = {
         ...MockFailedWorkspace,
         latest_build: {
@@ -411,7 +407,7 @@ describe("WorkspacePage", () => {
     });
 
     describe("Retries a failed 'Delete' transition", () => {
-      const mockDelete = jest.spyOn(client.api, "deleteWorkspace");
+      const mockDelete = jest.spyOn(API, "deleteWorkspace");
       const failedDelete: Workspace = {
         ...MockFailedWorkspace,
         latest_build: {
@@ -456,7 +452,7 @@ describe("WorkspacePage", () => {
         return HttpResponse.json([parameter]);
       }),
     );
-    const startWorkspaceSpy = jest.spyOn(client.api, "startWorkspace");
+    const startWorkspaceSpy = jest.spyOn(API, "startWorkspace");
 
     await renderWorkspacePage(workspace);
     const retryWithBuildParametersButton = await screen.findByRole("button", {
@@ -502,7 +498,7 @@ describe("WorkspacePage", () => {
         return HttpResponse.json([parameter]);
       }),
     );
-    const startWorkspaceSpy = jest.spyOn(client.api, "startWorkspace");
+    const startWorkspaceSpy = jest.spyOn(API, "startWorkspace");
 
     await renderWorkspacePage(workspace);
     const retryWithBuildParametersButton = await screen.findByRole("button", {
diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx
index 9b072cd4280f1..f3750051823ff 100644
--- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx
@@ -3,7 +3,7 @@ import { type FC, useEffect, useState } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation, useQuery, useQueryClient } from "react-query";
 import { useNavigate } from "react-router-dom";
-import { MissingBuildParameters, client } from "api/api";
+import { MissingBuildParameters, API } from "api/api";
 import { getErrorMessage } from "api/errors";
 import { buildInfo } from "api/queries/buildInfo";
 import { deploymentConfig, deploymentSSHConfig } from "api/queries/deployment";
@@ -83,7 +83,7 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
   }>({ open: false });
   const { mutate: mutateRestartWorkspace, isLoading: isRestarting } =
     useMutation({
-      mutationFn: client.api.restartWorkspace,
+      mutationFn: API.restartWorkspace,
     });
 
   // SSH Prefix
diff --git a/site/src/pages/WorkspacePage/WorkspaceScheduleControls.test.tsx b/site/src/pages/WorkspacePage/WorkspaceScheduleControls.test.tsx
index 19a0ef19fe34a..07c13a10122c4 100644
--- a/site/src/pages/WorkspacePage/WorkspaceScheduleControls.test.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceScheduleControls.test.tsx
@@ -5,7 +5,7 @@ import { HttpResponse, http } from "msw";
 import type { FC } from "react";
 import { QueryClient, QueryClientProvider, useQuery } from "react-query";
 import { RouterProvider, createMemoryRouter } from "react-router-dom";
-import { client } from "api/api";
+import { API } from "api/api";
 import { workspaceByOwnerAndName } from "api/queries/workspaces";
 import { GlobalSnackbar } from "components/GlobalSnackbar/GlobalSnackbar";
 import { ThemeProvider } from "contexts/ThemeProvider";
@@ -62,7 +62,7 @@ const renderScheduleControls = async () => {
 test("add 3 hours to deadline", async () => {
   const user = userEvent.setup();
   const updateDeadlineSpy = jest
-    .spyOn(client.api, "putWorkspaceExtension")
+    .spyOn(API, "putWorkspaceExtension")
     .mockResolvedValue();
 
   await renderScheduleControls();
@@ -91,7 +91,7 @@ test("add 3 hours to deadline", async () => {
 test("remove 2 hours to deadline", async () => {
   const user = userEvent.setup();
   const updateDeadlineSpy = jest
-    .spyOn(client.api, "putWorkspaceExtension")
+    .spyOn(API, "putWorkspaceExtension")
     .mockResolvedValue();
 
   await renderScheduleControls();
@@ -119,7 +119,7 @@ test("remove 2 hours to deadline", async () => {
 test("rollback to previous deadline on error", async () => {
   const user = userEvent.setup();
   const initialScheduleMessage = "Stop in 3 hours";
-  jest.spyOn(client.api, "putWorkspaceExtension").mockRejectedValue({});
+  jest.spyOn(API, "putWorkspaceExtension").mockRejectedValue({});
 
   await renderScheduleControls();
 
@@ -139,7 +139,7 @@ test("rollback to previous deadline on error", async () => {
 test("request is only sent once when clicking multiple times", async () => {
   const user = userEvent.setup();
   const updateDeadlineSpy = jest
-    .spyOn(client.api, "putWorkspaceExtension")
+    .spyOn(API, "putWorkspaceExtension")
     .mockResolvedValue();
 
   await renderScheduleControls();
diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx
index 1879a28c01564..af15a4423a44a 100644
--- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx
+++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx
@@ -1,6 +1,6 @@
 import { screen, waitFor, within } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import { client } from "api/api";
+import { API } from "api/api";
 import {
   MockWorkspace,
   MockTemplateVersionParameter1,
@@ -20,17 +20,15 @@ import WorkspaceParametersPage from "./WorkspaceParametersPage";
 test("Submit the workspace settings page successfully", async () => {
   // Mock the API calls that loads data
   jest
-    .spyOn(client.api, "getWorkspaceByOwnerAndName")
+    .spyOn(API, "getWorkspaceByOwnerAndName")
     .mockResolvedValueOnce(MockWorkspace);
-  jest
-    .spyOn(client.api, "getTemplateVersionRichParameters")
-    .mockResolvedValueOnce([
-      MockTemplateVersionParameter1,
-      MockTemplateVersionParameter2,
-      // Immutable parameters
-      MockTemplateVersionParameter4,
-    ]);
-  jest.spyOn(client.api, "getWorkspaceBuildParameters").mockResolvedValueOnce([
+  jest.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce([
+    MockTemplateVersionParameter1,
+    MockTemplateVersionParameter2,
+    // Immutable parameters
+    MockTemplateVersionParameter4,
+  ]);
+  jest.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValueOnce([
     MockWorkspaceBuildParameter1,
     MockWorkspaceBuildParameter2,
     // Immutable value
@@ -38,7 +36,7 @@ test("Submit the workspace settings page successfully", async () => {
   ]);
   // Mock the API calls that submit data
   const postWorkspaceBuildSpy = jest
-    .spyOn(client.api, "postWorkspaceBuild")
+    .spyOn(API, "postWorkspaceBuild")
     .mockResolvedValue(MockWorkspaceBuild);
   // Setup event and rendering
   const user = userEvent.setup();
diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx
index 72dc73d672185..7da0fc203d401 100644
--- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx
+++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx
@@ -4,7 +4,7 @@ import type { FC } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation, useQuery } from "react-query";
 import { useNavigate } from "react-router-dom";
-import { client } from "api/api";
+import { API } from "api/api";
 import { isApiValidationError } from "api/errors";
 import { checkAuthorization } from "api/queries/authCheck";
 import { templateByName } from "api/queries/templates";
@@ -29,12 +29,12 @@ const WorkspaceParametersPage: FC = () => {
   const workspace = useWorkspaceSettings();
   const parameters = useQuery({
     queryKey: ["workspace", workspace.id, "parameters"],
-    queryFn: () => client.api.getWorkspaceParameters(workspace),
+    queryFn: () => API.getWorkspaceParameters(workspace),
   });
   const navigate = useNavigate();
   const updateParameters = useMutation({
     mutationFn: (buildParameters: WorkspaceBuildParameter[]) =>
-      client.api.postWorkspaceBuild(workspace.id, {
+      API.postWorkspaceBuild(workspace.id, {
         transition: "start",
         rich_parameter_values: buildParameters,
       }),
@@ -93,9 +93,7 @@ const WorkspaceParametersPage: FC = () => {
 export type WorkspaceParametersPageViewProps = {
   workspace: Workspace;
   canChangeVersions: boolean;
-  data:
-    | Awaited<ReturnType<typeof client.api.getWorkspaceParameters>>
-    | undefined;
+  data: Awaited<ReturnType<typeof API.getWorkspaceParameters>> | undefined;
   submitError: unknown;
   isSubmitting: boolean;
   onSubmit: (formValues: WorkspaceParametersFormValues) => void;
diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.test.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.test.tsx
index 920c9e4b2e352..629c4d24173e3 100644
--- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.test.tsx
+++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.test.tsx
@@ -1,5 +1,5 @@
 import { screen } from "@testing-library/react";
-import { client } from "api/api";
+import { API } from "api/api";
 import { defaultSchedule } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule";
 import { MockTemplate } from "testHelpers/entities";
 import { render } from "testHelpers/renderHelpers";
@@ -271,7 +271,7 @@ const defaultFormProps: WorkspaceScheduleFormProps = {
 
 describe("templateInheritance", () => {
   it("disables the entire autostart feature appropriately", async () => {
-    jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate);
+    jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
     const props = {
       ...defaultFormProps,
       template: {
@@ -299,7 +299,7 @@ describe("templateInheritance", () => {
   it("disables the autostart days of the week appropriately", async () => {
     const enabledDayLabels = ["Sat", "Sun"];
 
-    jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate);
+    jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
     const props = {
       ...defaultFormProps,
       template: {
@@ -343,7 +343,7 @@ describe("templateInheritance", () => {
         allow_user_autostop: false,
       },
     };
-    jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate);
+    jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
     render(<WorkspaceScheduleForm {...props} />);
 
     const autoStopToggle = await screen.findByLabelText("Enable Autostop");
@@ -355,7 +355,7 @@ describe("templateInheritance", () => {
     expect(ttlInput).toBeDisabled();
   });
   it("disables secondary autostart fields if main feature switch is toggled off", async () => {
-    jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate);
+    jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
     render(
       <WorkspaceScheduleForm
         {...defaultFormProps}
@@ -379,7 +379,7 @@ describe("templateInheritance", () => {
     });
   });
   it("disables secondary autostop fields if main feature switch is toggled off", async () => {
-    jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate);
+    jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
     render(
       <WorkspaceScheduleForm
         {...defaultFormProps}
@@ -398,7 +398,7 @@ describe("templateInheritance", () => {
 });
 
 test("form should be enabled when both auto stop and auto start features are disabled, given that the template permits these actions", async () => {
-  jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate);
+  jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
   render(
     <WorkspaceScheduleForm
       {...defaultFormProps}
@@ -423,7 +423,7 @@ test("form should be disabled when both auto stop and auto start features are di
       allow_user_autostop: false,
     },
   };
-  jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate);
+  jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
   render(<WorkspaceScheduleForm {...props} />);
 
   const submitButton = await screen.findByRole("button", { name: "Submit" });
diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx
index cb5870b3c9db8..79b14bec16184 100644
--- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx
+++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx
@@ -3,7 +3,7 @@ import { type FC, useState } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation, useQuery, useQueryClient } from "react-query";
 import { useNavigate, useParams } from "react-router-dom";
-import { client } from "api/api";
+import { API } from "api/api";
 import { checkAuthorization } from "api/queries/authCheck";
 import { templateByName } from "api/queries/templates";
 import { workspaceByOwnerAndNameKey } from "api/queries/workspaces";
@@ -68,10 +68,7 @@ export const WorkspaceSchedulePage: FC = () => {
   const [isConfirmingApply, setIsConfirmingApply] = useState(false);
   const { mutate: updateWorkspace } = useMutation({
     mutationFn: () =>
-      client.api.startWorkspace(
-        workspace.id,
-        workspace.template_active_version_id,
-      ),
+      API.startWorkspace(workspace.id, workspace.template_active_version_id),
   });
 
   return (
@@ -166,11 +163,11 @@ const submitSchedule = async (data: SubmitScheduleData) => {
   const actions: Promise<void>[] = [];
 
   if (autostartChanged) {
-    actions.push(client.api.putWorkspaceAutostart(workspace.id, autostart));
+    actions.push(API.putWorkspaceAutostart(workspace.id, autostart));
   }
 
   if (autostopChanged) {
-    actions.push(client.api.putWorkspaceAutostop(workspace.id, ttl));
+    actions.push(API.putWorkspaceAutostop(workspace.id, ttl));
   }
 
   return Promise.all(actions);
diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.test.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.test.tsx
index e2b5acf67e2d8..a7ce4d63c897d 100644
--- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.test.tsx
+++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.test.tsx
@@ -1,6 +1,6 @@
 import { screen, waitFor, within } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import { client } from "api/api";
+import { API } from "api/api";
 import { MockWorkspace } from "testHelpers/entities";
 import {
   renderWithWorkspaceSettingsLayout,
@@ -11,11 +11,11 @@ import WorkspaceSettingsPage from "./WorkspaceSettingsPage";
 test("Submit the workspace settings page successfully", async () => {
   // Mock the API calls that loads data
   jest
-    .spyOn(client.api, "getWorkspaceByOwnerAndName")
+    .spyOn(API, "getWorkspaceByOwnerAndName")
     .mockResolvedValueOnce({ ...MockWorkspace });
   // Mock the API calls that submit data
   const patchWorkspaceSpy = jest
-    .spyOn(client.api, "patchWorkspace")
+    .spyOn(API, "patchWorkspace")
     .mockResolvedValue();
   // Setup event and rendering
   const user = userEvent.setup();
@@ -43,7 +43,7 @@ test("Submit the workspace settings page successfully", async () => {
 test("Name field is disabled if renames are disabled", async () => {
   // Mock the API calls that loads data
   jest
-    .spyOn(client.api, "getWorkspaceByOwnerAndName")
+    .spyOn(API, "getWorkspaceByOwnerAndName")
     .mockResolvedValueOnce({ ...MockWorkspace, allow_renames: false });
   renderWithWorkspaceSettingsLayout(<WorkspaceSettingsPage />, {
     route: "/@test-user/test-workspace/settings",
diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx
index e15d429178920..09bf002fd7cb9 100644
--- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx
+++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx
@@ -2,7 +2,7 @@ import type { FC } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation } from "react-query";
 import { useNavigate, useParams } from "react-router-dom";
-import { client } from "api/api";
+import { API } from "api/api";
 import { displaySuccess } from "components/GlobalSnackbar/utils";
 import { pageTitle } from "utils/page";
 import type { WorkspaceSettingsFormValues } from "./WorkspaceSettingsForm";
@@ -22,8 +22,8 @@ const WorkspaceSettingsPage: FC = () => {
   const mutation = useMutation({
     mutationFn: async (formValues: WorkspaceSettingsFormValues) => {
       await Promise.all([
-        client.api.patchWorkspace(workspace.id, { name: formValues.name }),
-        client.api.updateWorkspaceAutomaticUpdates(
+        API.patchWorkspace(workspace.id, { name: formValues.name }),
+        API.updateWorkspaceAutomaticUpdates(
           workspace.id,
           formValues.automatic_updates,
         ),
diff --git a/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx b/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx
index cc49cd091f22d..f5ea3589e2af4 100644
--- a/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx
+++ b/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx
@@ -7,7 +7,7 @@ import dayjs from "dayjs";
 import relativeTime from "dayjs/plugin/relativeTime";
 import { type FC, type ReactNode, useMemo, useState, useEffect } from "react";
 import { useQueries } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import type { TemplateVersion, Workspace } from "api/typesGenerated";
 import { ErrorAlert } from "components/Alert/ErrorAlert";
 import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
@@ -129,7 +129,7 @@ export const BatchUpdateConfirmation: FC<BatchUpdateConfirmationProps> = ({
         // ...but the query _also_ doesn't have everything we need, like the
         // template display name!
         ...version,
-        ...(await client.api.getTemplateVersion(version.id)),
+        ...(await API.getTemplateVersion(version.id)),
       }),
     })),
   });
diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx
index 196ba291dd942..9c152b1ac0534 100644
--- a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx
+++ b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx
@@ -1,7 +1,7 @@
 import { screen, waitFor, within } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import { HttpResponse, http } from "msw";
-import { client } from "api/api";
+import { API } from "api/api";
 import type { Workspace } from "api/typesGenerated";
 import {
   MockStoppedWorkspace,
@@ -58,9 +58,9 @@ describe("WorkspacesPage", () => {
       { ...MockWorkspace, id: "3" },
     ];
     jest
-      .spyOn(client.api, "getWorkspaces")
+      .spyOn(API, "getWorkspaces")
       .mockResolvedValue({ workspaces, count: workspaces.length });
-    const deleteWorkspace = jest.spyOn(client.api, "deleteWorkspace");
+    const deleteWorkspace = jest.spyOn(API, "deleteWorkspace");
     const user = userEvent.setup();
     renderWithAuth(<WorkspacesPage />);
     await waitForLoaderToBeRemoved();
@@ -95,9 +95,9 @@ describe("WorkspacesPage", () => {
         { ...MockOutdatedWorkspace, id: "4" },
       ];
       jest
-        .spyOn(client.api, "getWorkspaces")
+        .spyOn(API, "getWorkspaces")
         .mockResolvedValue({ workspaces, count: workspaces.length });
-      const updateWorkspace = jest.spyOn(client.api, "updateWorkspace");
+      const updateWorkspace = jest.spyOn(API, "updateWorkspace");
       const user = userEvent.setup();
       renderWithAuth(<WorkspacesPage />);
       await waitForLoaderToBeRemoved();
@@ -134,9 +134,9 @@ describe("WorkspacesPage", () => {
         { ...MockOutdatedWorkspace, id: "3" },
       ];
       jest
-        .spyOn(client.api, "getWorkspaces")
+        .spyOn(API, "getWorkspaces")
         .mockResolvedValue({ workspaces, count: workspaces.length });
-      const updateWorkspace = jest.spyOn(client.api, "updateWorkspace");
+      const updateWorkspace = jest.spyOn(API, "updateWorkspace");
       const user = userEvent.setup();
       renderWithAuth(<WorkspacesPage />);
       await waitForLoaderToBeRemoved();
@@ -172,9 +172,9 @@ describe("WorkspacesPage", () => {
         { ...MockOutdatedWorkspace, id: "3" },
       ];
       jest
-        .spyOn(client.api, "getWorkspaces")
+        .spyOn(API, "getWorkspaces")
         .mockResolvedValue({ workspaces, count: workspaces.length });
-      const updateWorkspace = jest.spyOn(client.api, "updateWorkspace");
+      const updateWorkspace = jest.spyOn(API, "updateWorkspace");
       const user = userEvent.setup();
       renderWithAuth(<WorkspacesPage />);
       await waitForLoaderToBeRemoved();
@@ -212,9 +212,9 @@ describe("WorkspacesPage", () => {
         { ...MockWorkspace, id: "5" },
       ];
       jest
-        .spyOn(client.api, "getWorkspaces")
+        .spyOn(API, "getWorkspaces")
         .mockResolvedValue({ workspaces, count: workspaces.length });
-      const updateWorkspace = jest.spyOn(client.api, "updateWorkspace");
+      const updateWorkspace = jest.spyOn(API, "updateWorkspace");
       const user = userEvent.setup();
       renderWithAuth(<WorkspacesPage />);
       await waitForLoaderToBeRemoved();
@@ -254,9 +254,9 @@ describe("WorkspacesPage", () => {
       { ...MockWorkspace, id: "3" },
     ];
     jest
-      .spyOn(client.api, "getWorkspaces")
+      .spyOn(API, "getWorkspaces")
       .mockResolvedValue({ workspaces, count: workspaces.length });
-    const stopWorkspace = jest.spyOn(client.api, "stopWorkspace");
+    const stopWorkspace = jest.spyOn(API, "stopWorkspace");
     const user = userEvent.setup();
     renderWithAuth(<WorkspacesPage />);
     await waitForLoaderToBeRemoved();
@@ -281,9 +281,9 @@ describe("WorkspacesPage", () => {
       { ...MockStoppedWorkspace, id: "3" },
     ];
     jest
-      .spyOn(client.api, "getWorkspaces")
+      .spyOn(API, "getWorkspaces")
       .mockResolvedValue({ workspaces, count: workspaces.length });
-    const startWorkspace = jest.spyOn(client.api, "startWorkspace");
+    const startWorkspace = jest.spyOn(API, "startWorkspace");
     const user = userEvent.setup();
     renderWithAuth(<WorkspacesPage />);
     await waitForLoaderToBeRemoved();
diff --git a/site/src/pages/WorkspacesPage/batchActions.tsx b/site/src/pages/WorkspacesPage/batchActions.tsx
index 0698c7b2a9c5c..38819cdf60c88 100644
--- a/site/src/pages/WorkspacesPage/batchActions.tsx
+++ b/site/src/pages/WorkspacesPage/batchActions.tsx
@@ -1,5 +1,5 @@
 import { useMutation } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import type { Workspace } from "api/typesGenerated";
 import { displayError } from "components/GlobalSnackbar/utils";
 
@@ -14,7 +14,7 @@ export function useBatchActions(options: UseBatchActionsProps) {
     mutationFn: (workspaces: readonly Workspace[]) => {
       return Promise.all(
         workspaces.map((w) =>
-          client.api.startWorkspace(w.id, w.latest_build.template_version_id),
+          API.startWorkspace(w.id, w.latest_build.template_version_id),
         ),
       );
     },
@@ -26,7 +26,7 @@ export function useBatchActions(options: UseBatchActionsProps) {
 
   const stopAllMutation = useMutation({
     mutationFn: (workspaces: readonly Workspace[]) => {
-      return Promise.all(workspaces.map((w) => client.api.stopWorkspace(w.id)));
+      return Promise.all(workspaces.map((w) => API.stopWorkspace(w.id)));
     },
     onSuccess,
     onError: () => {
@@ -36,9 +36,7 @@ export function useBatchActions(options: UseBatchActionsProps) {
 
   const deleteAllMutation = useMutation({
     mutationFn: (workspaces: readonly Workspace[]) => {
-      return Promise.all(
-        workspaces.map((w) => client.api.deleteWorkspace(w.id)),
-      );
+      return Promise.all(workspaces.map((w) => API.deleteWorkspace(w.id)));
     },
     onSuccess,
     onError: () => {
@@ -51,7 +49,7 @@ export function useBatchActions(options: UseBatchActionsProps) {
       return Promise.all(
         workspaces
           .filter((w) => w.outdated && !w.dormant_at)
-          .map((w) => client.api.updateWorkspace(w)),
+          .map((w) => API.updateWorkspace(w)),
       );
     },
     onSuccess,
@@ -65,7 +63,7 @@ export function useBatchActions(options: UseBatchActionsProps) {
       return Promise.all(
         workspaces
           .filter((w) => !w.favorite)
-          .map((w) => client.api.putFavoriteWorkspace(w.id)),
+          .map((w) => API.putFavoriteWorkspace(w.id)),
       );
     },
     onSuccess,
@@ -79,7 +77,7 @@ export function useBatchActions(options: UseBatchActionsProps) {
       return Promise.all(
         workspaces
           .filter((w) => w.favorite)
-          .map((w) => client.api.deleteFavoriteWorkspace(w.id)),
+          .map((w) => API.deleteFavoriteWorkspace(w.id)),
       );
     },
     onSuccess,
diff --git a/site/src/pages/WorkspacesPage/data.ts b/site/src/pages/WorkspacesPage/data.ts
index d6840a35c31fa..e1b8eec25ccb3 100644
--- a/site/src/pages/WorkspacesPage/data.ts
+++ b/site/src/pages/WorkspacesPage/data.ts
@@ -5,7 +5,7 @@ import {
   useQuery,
   useQueryClient,
 } from "react-query";
-import { client } from "api/api";
+import { API } from "api/api";
 import { getErrorMessage } from "api/errors";
 import type {
   Workspace,
@@ -30,7 +30,7 @@ export const useWorkspacesData = ({
   const result = useQuery({
     queryKey,
     queryFn: () =>
-      client.api.getWorkspaces({
+      API.getWorkspaces({
         q: query,
         limit: limit,
         offset: page <= 0 ? 0 : (page - 1) * limit,
@@ -54,7 +54,7 @@ export const useWorkspaceUpdate = (queryKey: QueryKey) => {
   const queryClient = useQueryClient();
 
   return useMutation({
-    mutationFn: client.api.updateWorkspaceVersion,
+    mutationFn: API.updateWorkspaceVersion,
     onMutate: async (workspace) => {
       await queryClient.cancelQueries({ queryKey });
       queryClient.setQueryData<WorkspacesResponse>(queryKey, (oldResponse) => {
diff --git a/site/src/pages/WorkspacesPage/filter/menus.ts b/site/src/pages/WorkspacesPage/filter/menus.ts
index 48789d789997c..f8b6755f50e82 100644
--- a/site/src/pages/WorkspacesPage/filter/menus.ts
+++ b/site/src/pages/WorkspacesPage/filter/menus.ts
@@ -1,4 +1,4 @@
-import { client } from "api/api";
+import { API } from "api/api";
 import type { WorkspaceStatus } from "api/typesGenerated";
 import {
   useFilterMenu,
@@ -21,7 +21,7 @@ export const useTemplateFilterMenu = ({
     id: "template",
     getSelectedOption: async () => {
       // Show all templates including deprecated
-      const templates = await client.api.getTemplates(organizationId);
+      const templates = await API.getTemplates(organizationId);
       const template = templates.find((template) => template.name === value);
       if (template) {
         return {
@@ -37,7 +37,7 @@ export const useTemplateFilterMenu = ({
     },
     getOptions: async (query) => {
       // Show all templates including deprecated
-      const templates = await client.api.getTemplates(organizationId);
+      const templates = await API.getTemplates(organizationId);
       const filteredTemplates = templates.filter(
         (template) =>
           template.name.toLowerCase().includes(query.toLowerCase()) ||
diff --git a/site/src/utils/terminal.ts b/site/src/utils/terminal.ts
index 643847cdcdb26..82c98a370a51f 100644
--- a/site/src/utils/terminal.ts
+++ b/site/src/utils/terminal.ts
@@ -1,4 +1,4 @@
-import { client } from "api/api";
+import { API } from "api/api";
 
 export const terminalWebsocketUrl = async (
   baseUrl: string | undefined,
@@ -30,7 +30,7 @@ export const terminalWebsocketUrl = async (
   }
 
   // Do ticket issuance and set the query parameter.
-  const tokenRes = await client.api.issueReconnectingPTYSignedToken({
+  const tokenRes = await API.issueReconnectingPTYSignedToken({
     url: url.toString(),
     agentID: agentId,
   });

From c52827c1130a3752967767b0c05a7dd9c23ce9c7 Mon Sep 17 00:00:00 2001
From: Parkreiner <throwawayclover@gmail.com>
Date: Sun, 12 May 2024 18:55:54 +0000
Subject: [PATCH 7/7] refactor: update import setup for tests

---
 site/src/modules/resources/AgentLogs/AgentLogs.tsx        | 4 ++--
 .../TemplateVersionEditorPage.test.tsx                    | 6 +++---
 site/src/pages/WorkspacePage/WorkspacePage.test.tsx       | 8 ++++----
 3 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/site/src/modules/resources/AgentLogs/AgentLogs.tsx b/site/src/modules/resources/AgentLogs/AgentLogs.tsx
index 2216b7eae24ae..407e3c12fe9b5 100644
--- a/site/src/modules/resources/AgentLogs/AgentLogs.tsx
+++ b/site/src/modules/resources/AgentLogs/AgentLogs.tsx
@@ -8,7 +8,7 @@ import {
   useState,
 } from "react";
 import { FixedSizeList as List } from "react-window";
-import * as API from "api/api";
+import { watchWorkspaceAgentLogs } from "api/api";
 import type { WorkspaceAgentLogSource } from "api/typesGenerated";
 import {
   AGENT_LOG_LINE_HEIGHT,
@@ -193,7 +193,7 @@ export const useAgentLogs = (
       return;
     }
 
-    const socket = API.watchWorkspaceAgentLogs(agentId, {
+    const socket = watchWorkspaceAgentLogs(agentId, {
       // Get all logs
       after: 0,
       onMessage: (logs) => {
diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx
index cd07c9688ea36..8c63b7db428d1 100644
--- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx
+++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx
@@ -4,7 +4,7 @@ import WS from "jest-websocket-mock";
 import { HttpResponse, http } from "msw";
 import { QueryClient } from "react-query";
 import { RouterProvider, createMemoryRouter } from "react-router-dom";
-import * as api from "api/api";
+import * as apiModule from "api/api";
 import { templateVersionVariablesKey } from "api/queries/templates";
 import type { TemplateVersion } from "api/typesGenerated";
 import { AppProviders } from "App";
@@ -26,7 +26,7 @@ import type { MonacoEditorProps } from "./MonacoEditor";
 import { Language } from "./PublishTemplateVersionDialog";
 import TemplateVersionEditorPage from "./TemplateVersionEditorPage";
 
-const { API } = api;
+const { API } = apiModule;
 
 // For some reason this component in Jest is throwing a MUI style warning so,
 // since we don't need it for this test, we can mock it out
@@ -86,7 +86,7 @@ const buildTemplateVersion = async (
     .spyOn(API, "getTemplateVersionByName")
     .mockResolvedValue(templateVersion);
   jest
-    .spyOn(api, "watchBuildLogsByTemplateVersionId")
+    .spyOn(apiModule, "watchBuildLogsByTemplateVersionId")
     .mockImplementation((_, options) => {
       options.onMessage(MockWorkspaceBuildLogs[0]);
       options.onDone?.();
diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx
index 757ea44927a3a..9766d76f692a3 100644
--- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx
+++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx
@@ -1,7 +1,7 @@
 import { screen, waitFor, within } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import { HttpResponse, http } from "msw";
-import * as api from "api/api";
+import * as apiModule from "api/api";
 import type { TemplateVersionParameter, Workspace } from "api/typesGenerated";
 import EventSourceMock from "eventsourcemock";
 import {
@@ -22,7 +22,7 @@ import { renderWithAuth } from "testHelpers/renderHelpers";
 import { server } from "testHelpers/server";
 import { WorkspacePage } from "./WorkspacePage";
 
-const { API } = api;
+const { API, MissingBuildParameters } = apiModule;
 
 // Renders the workspace page and waits for it be loaded
 const renderWorkspacePage = async (workspace: Workspace) => {
@@ -33,7 +33,7 @@ const renderWorkspacePage = async (workspace: Workspace) => {
     .spyOn(API, "getDeploymentConfig")
     .mockResolvedValueOnce(MockDeploymentConfig);
   jest
-    .spyOn(api, "watchWorkspaceAgentLogs")
+    .spyOn(apiModule, "watchWorkspaceAgentLogs")
     .mockImplementation((_, options) => {
       options.onDone?.();
       return new WebSocket("");
@@ -256,7 +256,7 @@ describe("WorkspacePage", () => {
     const updateWorkspaceSpy = jest
       .spyOn(API, "updateWorkspace")
       .mockRejectedValueOnce(
-        new api.MissingBuildParameters(
+        new MissingBuildParameters(
           [MockTemplateVersionParameter1, MockTemplateVersionParameter2],
           MockOutdatedWorkspace.template_active_version_id,
         ),