diff --git a/src/assets/main.css b/src/assets/main.css
index cb6974cba0e2a5c6b64ede79fec9c1befe81c2f9..bdbb663e010b6c5e9b4ab67ecc0de411dbeded73 100644
--- a/src/assets/main.css
+++ b/src/assets/main.css
@@ -33,3 +33,6 @@ pre {
 .hover-info:hover {
     color: var(--bs-info) !important;
 }
+.hover-danger:hover {
+    color: var(--bs-danger) !important;
+}
diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue
index 32eea49ad2638076157acee2f2bb7b2cf06f70e1..60eea6e114d6aabceae76d62572c9e1caf1fb771 100644
--- a/src/components/AppHeader.vue
+++ b/src/components/AppHeader.vue
@@ -344,6 +344,10 @@ watch(
           </tr>
         </tbody>
       </table>
+      <p>
+        Personal API tokens are encouraged to use when accessing the REST API of
+        CloWM directly.
+      </p>
     </template>
     <template v-slot:footer>
       <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
diff --git a/src/components/user/CreateApiTokenModal.vue b/src/components/user/CreateApiTokenModal.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7dd479717748e390ad408535934631dc8f5da6ed
--- /dev/null
+++ b/src/components/user/CreateApiTokenModal.vue
@@ -0,0 +1,192 @@
+<script setup lang="ts">
+import { onMounted, reactive, ref } from "vue";
+import { type ApiTokenIn, type ApiTokenPrivateOut, ScopeEnum } from "@/client";
+import { Modal } from "bootstrap";
+import BootstrapModal from "@/components/modals/BootstrapModal.vue";
+import dayjs from "dayjs";
+import { useUserStore } from "@/stores/users";
+import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
+
+const props = defineProps<{
+  modalId: string;
+}>();
+
+const tokenIn = reactive<ApiTokenIn>({
+  name: "",
+  expires_at: undefined,
+  scopes: [],
+});
+
+const formState = reactive<{
+  validated: boolean;
+  loading: boolean;
+}>({
+  validated: false,
+  loading: false,
+});
+
+const userRepository = useUserStore();
+const tokenCreateForm = ref<HTMLFormElement | undefined>(undefined);
+let createTokenModal: Modal | null = null;
+
+const scopeDescription: Record<ScopeEnum, string> = {
+  token: "Some description about token scope",
+  user: "Some desc for user",
+  x: "Placeholder for x scope",
+};
+
+const emit = defineEmits<{
+  (e: "token-created", token: ApiTokenPrivateOut): void;
+}>();
+
+function modalClosed() {
+  formState.validated = false;
+}
+
+function expiresAtTimestampChanged(target?: HTMLInputElement | null) {
+  tokenIn.expires_at = target?.value ? dayjs(target?.value).unix() : undefined;
+}
+
+function createToken() {
+  formState.validated = true;
+  if (tokenCreateForm.value?.checkValidity()) {
+    formState.loading = true;
+    userRepository
+      .createApiToken(tokenIn)
+      .then((token) => {
+        emit("token-created", token);
+        createTokenModal?.hide();
+        tokenIn.expires_at = undefined;
+        tokenIn.name = "";
+        tokenIn.scopes = [];
+      })
+      .finally(() => {
+        formState.loading = false;
+      });
+  }
+}
+
+onMounted(() => {
+  createTokenModal = new Modal("#" + props.modalId);
+});
+</script>
+
+<template>
+  <bootstrap-modal
+    :modalId="modalId"
+    static-backdrop
+    modal-label="Create API Token Modal"
+    v-on="{ 'hidden.bs.modal': modalClosed }"
+    size-modifier-modal="lg"
+  >
+    <template #header> Create personal API token</template>
+    <template #body>
+      <form
+        id="api-token-create-form"
+        :class="{ 'was-validated': formState.validated }"
+        ref="tokenCreateForm"
+        @submit.prevent="createToken"
+      >
+        <div class="mb-3">
+          <label for="token-name-input" class="form-label">Token name</label>
+          <input
+            type="text"
+            class="form-control"
+            id="token-name-input"
+            required
+            minlength="3"
+            maxlength="63"
+            pattern="^[a-z\d\-]+$"
+            v-model="tokenIn.name"
+          />
+          <div class="invalid-feedback">
+            <div>
+              Name must ...
+              <ul>
+                <li>be at least 3 Characters long</li>
+                <li>contains the characters a-z 0-9 and /</li>
+              </ul>
+            </div>
+          </div>
+        </div>
+        <div class="mb-3">
+          <label for="expires-at-input" class="form-label"
+            >Expiration date</label
+          >
+          <div :class="{ 'input-group': tokenIn.expires_at != undefined }">
+            <input
+              class="form-control"
+              id="expires-at-input"
+              type="date"
+              :min="dayjs().add(1, 'day').format('YYYY-MM-DD')"
+              :value="
+                tokenIn.expires_at
+                  ? dayjs.unix(tokenIn.expires_at).format('YYYY-MM-DD')
+                  : undefined
+              "
+              @input="
+                (event) =>
+                  expiresAtTimestampChanged(event.target as HTMLInputElement)
+              "
+            />
+            <div
+              v-if="tokenIn.expires_at"
+              class="input-group-text cursor-pointer hover-danger"
+              @click="tokenIn.expires_at = undefined"
+            >
+              <font-awesome-icon icon="fa-solid fa-xmark" class="fs-5" />
+            </div>
+          </div>
+        </div>
+        <div class="mb-3">
+          <div>Select scopes</div>
+          <div
+            v-for="scope in Object.values(ScopeEnum)"
+            :key="scope"
+            class="form-check mb-2"
+          >
+            <input
+              class="form-check-input"
+              type="checkbox"
+              :value="scope"
+              :id="`scope-input-${scope}`"
+              v-model="tokenIn.scopes"
+            />
+            <label
+              class="form-check-label pt-1"
+              style="line-height: 0.8rem"
+              :for="`scope-input-${scope}`"
+            >
+              <span>{{ scope.toLowerCase() }}</span>
+              <br />
+              <span class="text-secondary" style="font-size: 0.8rem">
+                {{ scopeDescription[scope] }}
+              </span>
+            </label>
+          </div>
+        </div>
+      </form>
+    </template>
+    <template v-slot:footer>
+      <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
+        Close
+      </button>
+      <button
+        type="submit"
+        form="api-token-create-form"
+        class="btn btn-primary"
+        :disabled="formState.loading || tokenIn.scopes.length === 0"
+      >
+        <span
+          v-if="formState.loading"
+          class="spinner-border spinner-border-sm"
+          role="status"
+          aria-hidden="true"
+        ></span>
+        Save
+      </button>
+    </template>
+  </bootstrap-modal>
+</template>
+
+<style scoped></style>
diff --git a/src/stores/users.ts b/src/stores/users.ts
index f0cc80ded9e03dc4eb12f3528376ccba246ac27c..fd2581db96e904071eec41219b99d2373feac51f 100644
--- a/src/stores/users.ts
+++ b/src/stores/users.ts
@@ -1,5 +1,12 @@
 import { defineStore } from "pinia";
-import type { UserOutExtended, UserOut, UserIn, ApiTokenOut } from "@/client";
+import type {
+  UserOutExtended,
+  UserOut,
+  UserIn,
+  ApiTokenOut,
+  ApiTokenIn,
+  ApiTokenPrivateOut,
+} from "@/client";
 import { UserService, RoleEnum, ApiTokenService } from "@/client";
 import { useWorkflowExecutionStore } from "@/stores/workflowExecutions";
 import { useBucketStore } from "@/stores/buckets";
@@ -137,6 +144,12 @@ export const useUserStore = defineStore({
         })
         .finally(onFinally);
     },
+    createApiToken(tokenIn: ApiTokenIn): Promise<ApiTokenPrivateOut> {
+      return ApiTokenService.apiTokenCreateToken(tokenIn).then((token) => {
+        this.apiTokensMapping[token.token_id] = token as ApiTokenOut;
+        return token;
+      });
+    },
     deleteApiToken(tokenId: string): Promise<void> {
       return ApiTokenService.apiTokenDeleteToken(tokenId).then(() => {
         delete this.apiTokensMapping[tokenId];
diff --git a/src/views/user/ListApiTokenView.vue b/src/views/user/ListApiTokenView.vue
index d4d0d06227c46d50f985c70ffd138d15802d3d0f..06154a659582f62d8b4370992e4d7c79080b6f1e 100644
--- a/src/views/user/ListApiTokenView.vue
+++ b/src/views/user/ListApiTokenView.vue
@@ -3,15 +3,23 @@ import { onMounted, reactive } from "vue";
 import { useUserStore } from "@/stores/users";
 import dayjs from "dayjs";
 import DeleteModal from "@/components/modals/DeleteModal.vue";
+import CreateApiTokenModal from "@/components/user/CreateApiTokenModal.vue";
+import type { ApiTokenPrivateOut } from "@/client";
+import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
+import CopyToClipboardIcon from "@/components/CopyToClipboardIcon.vue";
 
 const userRepository = useUserStore();
 
 const state = reactive<{
   loading: boolean;
   deleteTokenId: string;
+  createdToken?: ApiTokenPrivateOut;
+  showCreatedToken: boolean;
 }>({
   loading: false,
   deleteTokenId: "",
+  createdToken: undefined,
+  showCreatedToken: false,
 });
 
 function fetchTokens() {
@@ -31,12 +39,25 @@ function confirmedDeleteApiToken() {
   });
 }
 
+function apiTokenCreated(token: ApiTokenPrivateOut) {
+  state.createdToken = token;
+}
+
+function closeSuccessAlert() {
+  state.createdToken = undefined;
+  state.showCreatedToken = false;
+}
+
 onMounted(() => {
   fetchTokens();
 });
 </script>
 
 <template>
+  <create-api-token-modal
+    modal-id="create-personal-api-token-modal"
+    @token-created="apiTokenCreated"
+  />
   <delete-modal
     modal-id="delete-personal-api-token-modal"
     @confirm-delete="confirmedDeleteApiToken"
@@ -49,11 +70,51 @@ onMounted(() => {
   >
     <h2 class="w-fit">Personal API tokens</h2>
     <span class="w-fit" tabindex="0">
-      <button type="button" class="btn btn-primary" disabled>
+      <button
+        type="button"
+        class="btn btn-primary"
+        data-bs-toggle="modal"
+        data-bs-target="#create-personal-api-token-modal"
+      >
         Create token
       </button>
     </span>
   </div>
+  <div
+    v-if="state.createdToken"
+    class="alert alert-success alert-dismissible"
+    role="alert"
+    style="min-width: 50%"
+  >
+    <strong>Your new API token</strong>
+    <button
+      type="button"
+      class="btn-close"
+      aria-label="Close"
+      @click="closeSuccessAlert"
+    ></button>
+    <br />
+    <div class="input-group my-2 w-fit">
+      <input
+        :type="state.showCreatedToken ? 'text' : 'password'"
+        class="form-control"
+        readonly
+        :value="state.createdToken.token"
+      />
+      <div
+        class="input-group-text cursor-pointer hover-info"
+        @click="state.showCreatedToken = !state.showCreatedToken"
+      >
+        <font-awesome-icon
+          :icon="`fa-solid fa-eye${state.showCreatedToken ? '-slash' : ''}`"
+        />
+      </div>
+      <div class="input-group-text">
+        <copy-to-clipboard-icon :text="state.createdToken.token" />
+      </div>
+    </div>
+    <div>Make sure you save it - you won't be able to access it again.</div>
+  </div>
   <table class="table table-striped align-middle caption-top">
     <caption>
       Displaying