diff --git a/client/components/admin.vue b/client/components/admin.vue
index fbb09d9061ba317bff055b7df669d4302bc10404..4a8fee1a255a5014c3a8c14ca4b849b2ab3c7277 100644
--- a/client/components/admin.vue
+++ b/client/components/admin.vue
@@ -83,8 +83,8 @@
           template(v-if='hasPermission([`manage:system`, `manage:api`])')
             v-divider.my-2
             v-subheader.pl-4 {{ $t('admin:nav.system') }}
-            v-list-item(to='/api', v-if='hasPermission([`manage:system`, `manage:api`])', disabled)
-              v-list-item-avatar(size='24', tile): v-icon(color='grey lighten-2') mdi-call-split
+            v-list-item(to='/api', v-if='hasPermission([`manage:system`, `manage:api`])')
+              v-list-item-avatar(size='24', tile): v-icon mdi-call-split
               v-list-item-title {{ $t('admin:api.title') }}
             v-list-item(to='/mail', color='primary', v-if='hasPermission(`manage:system`)')
               v-list-item-avatar(size='24', tile): v-icon mdi-email-multiple-outline
diff --git a/client/components/admin/admin-api-create.vue b/client/components/admin/admin-api-create.vue
new file mode 100644
index 0000000000000000000000000000000000000000..c7218dffcae3b21f29d8a4f8440f93468129c224
--- /dev/null
+++ b/client/components/admin/admin-api-create.vue
@@ -0,0 +1,236 @@
+<template lang="pug">
+  div
+    v-dialog(v-model='isShown', max-width='650', persistent)
+      v-card
+        .dialog-header.is-short
+          v-icon.mr-3(color='white') mdi-plus
+          span {{$t('admin:api.newKeyTitle')}}
+        v-card-text.pt-5
+          v-text-field(
+            outlined
+            prepend-icon='mdi-format-title'
+            v-model='name'
+            :label='$t(`admin:api.newKeyName`)'
+            persistent-hint
+            ref='keyNameInput'
+            :hint='$t(`admin:api.newKeyNameHint`)'
+            counter='255'
+            )
+          v-select.mt-3(
+            :items='expirations'
+            outlined
+            prepend-icon='mdi-clock'
+            v-model='expiration'
+            :label='$t(`admin:api.newKeyExpiration`)'
+            :hint='$t(`admin:api.newKeyExpirationHint`)'
+            persistent-hint
+            )
+          v-divider.mt-4
+          v-subheader.pl-2: strong.indigo--text {{$t('admin:api.newKeyPermissionScopes')}}
+          v-list.pl-8(nav)
+            v-list-item-group(v-model='fullAccess')
+              v-list-item(
+                :value='true'
+                active-class='indigo--text'
+                )
+                template(v-slot:default='{ active, toggle }')
+                  v-list-item-action
+                    v-checkbox(
+                      :input-value='active'
+                      :true-value='true'
+                      color='indigo'
+                      @click='toggle'
+                    )
+                  v-list-item-content
+                    v-list-item-title {{$t('admin:api.newKeyFullAccess')}}
+            v-divider.mt-3
+            v-subheader.caption.indigo--text {{$t('admin:api.newKeyGroupPermissions')}}
+            v-list-item
+              v-select(
+                :disabled='fullAccess'
+                :items='groups'
+                item-text='name'
+                item-value='id'
+                outlined
+                color='indigo'
+                v-model='group'
+                :label='$t(`admin:api.newKeyGroup`)'
+                :hint='$t(`admin:api.newKeyGroupHint`)'
+                persistent-hint
+                )
+        v-card-chin
+          v-spacer
+          v-btn(text, @click='isShown = false', :disabled='loading') {{$t('common:actions.cancel')}}
+          v-btn.px-3(depressed, color='primary', @click='generate', :loading='loading')
+            v-icon(left) mdi-chevron-right
+            span {{$t('common:actions.generate')}}
+
+    v-dialog(
+      v-model='isCopyKeyDialogShown'
+      max-width='750'
+      persistent
+      overlay-color='blue darken-5'
+      overlay-opacity='.9'
+      )
+      v-card
+        v-toolbar(dense, flat, color='primary', dark) {{$t('admin:api.newKeyTitle')}}
+        v-card-text.pt-5
+          .body-2.text-center
+            i18next(tag='span', path='admin:api.newKeyCopyWarn')
+              strong(place='bold') {{$t('admin:api.newKeyCopyWarnBold')}}
+          v-textarea.mt-3(
+            ref='keyContentsIpt'
+            filled
+            no-resize
+            readonly
+            v-model='key'
+            :rows='10'
+            hide-details
+          )
+        v-card-chin
+          v-spacer
+          v-btn.px-3(depressed, dark, color='primary', @click='isCopyKeyDialogShown = false') {{$t('common:actions.close')}}
+</template>
+
+<script>
+import _ from 'lodash'
+import gql from 'graphql-tag'
+
+import groupsQuery from 'gql/admin/users/users-query-groups.gql'
+
+export default {
+  props: {
+    value: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      name: '',
+      expiration: '1y',
+      fullAccess: true,
+      groups: [],
+      group: null,
+      isCopyKeyDialogShown: false,
+      key: ''
+    }
+  },
+  computed: {
+    isShown: {
+      get() { return this.value },
+      set(val) { this.$emit('input', val) }
+    },
+    expirations() {
+      return [
+        { value: '30d', text: this.$t('admin:api.expiration30d') },
+        { value: '90d', text: this.$t('admin:api.expiration90d') },
+        { value: '180d', text: this.$t('admin:api.expiration180d') },
+        { value: '1y', text: this.$t('admin:api.expiration1y') },
+        { value: '3y', text: this.$t('admin:api.expiration3y') }
+      ]
+    }
+  },
+  watch: {
+    value (newValue, oldValue) {
+      if (newValue) {
+        setTimeout(() => {
+          this.$refs.keyNameInput.focus()
+        }, 400)
+      }
+    }
+  },
+  methods: {
+    async generate () {
+      try {
+        if (_.trim(this.name).length < 2 || this.name.length > 255) {
+          throw new Error(this.$t('admin:api.newKeyNameError'))
+        } else if (!this.fullAccess && !this.group) {
+          throw new Error(this.$t('admin:api.newKeyGroupError'))
+        } else if (!this.fullAccess && this.group === 2) {
+          throw new Error(this.$t('admin:api.newKeyGuestGroupError'))
+        }
+      } catch (err) {
+        return this.$store.commit('showNotification', {
+          style: 'red',
+          message: err,
+          icon: 'alert'
+        })
+      }
+
+      this.loading = true
+
+      try {
+        const resp = await this.$apollo.mutate({
+          mutation: gql`
+            mutation ($name: String!, $expiration: String!, $fullAccess: Boolean!, $group: Int) {
+              authentication {
+                createApiKey (name: $name, expiration: $expiration, fullAccess: $fullAccess, group: $group) {
+                  key
+                  responseResult {
+                    succeeded
+                    errorCode
+                    slug
+                    message
+                  }
+                }
+              }
+            }
+          `,
+          variables: {
+            name: this.name,
+            expiration: this.expiration,
+            fullAccess: (this.fullAccess === true),
+            group: this.group
+          },
+          watchLoading (isLoading) {
+            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-create')
+          }
+        })
+        if (_.get(resp, 'data.authentication.createApiKey.responseResult.succeeded', false)) {
+          this.$store.commit('showNotification', {
+            style: 'success',
+            message: this.$t('admin:api.newKeySuccess'),
+            icon: 'check'
+          })
+
+          this.name = ''
+          this.expiration = '1y'
+          this.fullAccess = true
+          this.group = null
+          this.isShown = false
+          this.$emit('refresh')
+
+          this.key = _.get(resp, 'data.authentication.createApiKey.key', '???')
+          this.isCopyKeyDialogShown = true
+
+          setTimeout(() => {
+            this.$refs.keyContentsIpt.$refs.input.select()
+          }, 400)
+        } else {
+          this.$store.commit('showNotification', {
+            style: 'red',
+            message: _.get(resp, 'data.authentication.createApiKey.responseResult.message', 'An unexpected error occured.'),
+            icon: 'alert'
+          })
+        }
+      } catch (err) {
+        this.$store.commit('pushGraphError', err)
+      }
+
+      this.loading = false
+    }
+  },
+  apollo: {
+    groups: {
+      query: groupsQuery,
+      fetchPolicy: 'network-only',
+      update: (data) => data.groups.list,
+      watchLoading (isLoading) {
+        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-groups-refresh')
+      }
+    }
+  }
+}
+</script>
diff --git a/client/components/admin/admin-api.vue b/client/components/admin/admin-api.vue
index a48b05d802205495491e25799299a3675d98bbf9..552ab66f6ed68acf3def829061834e6cef92b160 100644
--- a/client/components/admin/admin-api.vue
+++ b/client/components/admin/admin-api.vue
@@ -3,128 +3,232 @@
     v-layout(row, wrap)
       v-flex(xs12)
         .admin-header
-          img(src='/svg/icon-rest-api.svg', alt='API', style='width: 80px;')
+          img.animated.fadeInUp(src='/svg/icon-rest-api.svg', alt='API', style='width: 80px;')
           .admin-header-title
-            .headline.blue--text.text--darken-2 API Access
-            .subtitle-1.grey--text Manage keys to access the API #[v-chip(label, color='primary', small).white--text coming soon]
+            .headline.primary--text.animated.fadeInLeft {{$t('admin:api.title')}}
+            .subtitle-1.grey--text.animated.fadeInLeft {{$t('admin:api.subtitle')}}
           v-spacer
-          v-btn(outline, color='grey', large, @click='refresh', disabled)
-            v-icon refresh
-          v-btn(color='green', disabled, depressed, large, @click='globalSwitch')
-            v-icon(left) power_settings_new
-            | Enable API
-          v-btn(color='primary', depressed, large, @click='newKey', disabled)
-            v-icon(left) add
-            | New API Key
-        v-card.mt-3
-          v-data-table(
-            v-model='selected'
-            :items='items',
-            :headers='headers',
-            :search='search',
-            :pagination.sync='pagination',
-            :rows-per-page-items='[15]'
-            select-all,
-            hide-actions,
-            disable-initial-sort
-          )
-            template(slot='headers', slot-scope='props')
-              tr
-                th(width='50')
-                th.text-xs-right(
-                  width='80'
-                  :class='[`column sortable`, pagination.descending ? `desc` : `asc`, pagination.sortBy === `id` ? `active` : ``]'
-                  @click='changeSort(`id`)'
-                )
-                  v-icon(small) arrow_upward
-                  | ID
-                th.text-xs-left(
-                  v-for='header in props.headers'
-                  :key='header.text'
-                  :width='header.width'
-                  :class='[`column sortable`, pagination.descending ? `desc` : `asc`, header.value === pagination.sortBy ? `active` : ``]'
-                  @click='changeSort(header.value)'
-                )
-                  | {{ header.text }}
-                  v-icon(small) arrow_upward
-            template(slot='items', slot-scope='props')
-              tr(:active='props.selected')
-                td
-                  v-checkbox(hide-details, :input-value='props.selected', color='blue darken-2', @click='props.selected = !props.selected')
-                td.text-xs-right {{ props.item.id }}
-                td {{ props.item.name }}
-                td {{ props.item.key }}
-                td {{ props.item.createdOn }}
-                td {{ props.item.updatedOn }}
-                td: v-btn(icon): v-icon.grey--text.text--darken-1 more_horiz
-            template(slot='no-data')
-              v-alert.mt-3(icon='info', :value='true', outline, color='info') No API keys have been generated yet.
-          .text-xs-center.py-2
-            v-pagination(v-model='pagination.page', :length='pages')
+          template(v-if='enabled')
+            status-indicator.mr-3(positive, pulse)
+            .caption.green--text.animated.fadeInLeft {{$t('admin:api.enabled')}}
+          template(v-else)
+            status-indicator.mr-3(negative, pulse)
+            .caption.red--text.animated.fadeInLeft {{$t('admin:api.disabled')}}
+          v-spacer
+          v-btn.mr-3.animated.fadeInDown.wait-p2s(outlined, color='grey', large, @click='refresh')
+            v-icon mdi-refresh
+          v-btn.mr-3.animated.fadeInDown.wait-p1s(:color='enabled ? `red` : `green`', depressed, large, @click='globalSwitch', dark, :loading='isToggleLoading')
+            v-icon(left) mdi-power
+            span(v-if='!enabled') {{$t('admin:api.enableButton')}}
+            span(v-else) {{$t('admin:api.disableButton')}}
+          v-btn.animated.fadeInDown(color='primary', depressed, large, @click='newKey', dark)
+            v-icon(left) mdi-plus
+            span {{$t('admin:api.newKeyButton')}}
+        v-card.mt-3.animated.fadeInUp
+          v-simple-table(v-if='keys && keys.length > 0')
+            template(v-slot:default)
+              thead
+                tr.grey(:class='$vuetify.theme.dark ? `darken-4-d5` : `lighten-5`')
+                  th {{$t('admin:api.headerName')}}
+                  th {{$t('admin:api.headerKeyEnding')}}
+                  th {{$t('admin:api.headerExpiration')}}
+                  th {{$t('admin:api.headerCreated')}}
+                  th {{$t('admin:api.headerLastUpdated')}}
+                  th(width='100') {{$t('admin:api.headerRevoke')}}
+              tbody
+                tr(v-for='key of keys', :key='`key-` + key.id')
+                  td
+                    strong(:class='key.isRevoked ? `red--text` : ``') {{ key.name }}
+                    em.caption.ml-1.red--text(v-if='key.isRevoked') (revoked)
+                  td.caption {{ key.keyShort }}
+                  td(:style='key.isRevoked ? `text-decoration: line-through;` : ``') {{ key.expiration | moment('LL') }}
+                  td {{ key.createdAt | moment('calendar') }}
+                  td {{ key.updatedAt | moment('calendar') }}
+                  td: v-btn(icon, @click='revoke(key)', :disabled='key.isRevoked'): v-icon(color='error') mdi-cancel
+          v-card-text(v-else)
+            v-alert.mb-0(icon='mdi-information', :value='true', outlined, color='info') {{$t('admin:api.noKeyInfo')}}
+
+    create-api-key(v-model='isCreateDialogShown', @refresh='refresh(false)')
+
+    v-dialog(v-model='isRevokeConfirmDialogShown', max-width='500', persistent)
+      v-card
+        .dialog-header.is-red {{$t('admin:api.revokeConfirm')}}
+        v-card-text.pa-4
+          i18next(tag='span', path='admin:api.revokeConfirmText')
+            strong(place='name') {{ current.name }}
+        v-card-actions
+          v-spacer
+          v-btn(text, @click='isRevokeConfirmDialogShown = false', :disabled='revokeLoading') {{$t('common:actions.cancel')}}
+          v-btn(color='red', dark, @click='revokeConfirm', :loading='revokeLoading') {{$t('admin:api.revoke')}}
 </template>
 
 <script>
+import _ from 'lodash'
+import gql from 'graphql-tag'
+import { StatusIndicator } from 'vue-status-indicator'
+
+import CreateApiKey from './admin-api-create.vue'
+
 export default {
+  components: {
+    StatusIndicator,
+    CreateApiKey
+  },
   data() {
     return {
-      selected: [],
-      pagination: {},
-      items: [],
-      headers: [
-        { text: 'Name', value: 'name' },
-        { text: 'Key', value: 'key' },
-        { text: 'Created On', value: 'createdOn' },
-        { text: 'Updated On', value: 'updatedOn' },
-        { text: '', value: 'actions', sortable: false, width: 50 }
-      ],
-      search: ''
-    }
-  },
-  computed: {
-    pages () {
-      if (this.pagination.rowsPerPage == null || this.pagination.totalItems == null) {
-        return 0
-      }
-
-      return Math.ceil(this.pagination.totalItems / this.pagination.rowsPerPage)
+      enabled: false,
+      isToggleLoading: false,
+      keys: [],
+      isCreateDialogShown: false,
+      isRevokeConfirmDialogShown: false,
+      revokeLoading: false,
+      current: {}
     }
   },
   methods: {
-    changeSort (column) {
-      if (this.pagination.sortBy === column) {
-        this.pagination.descending = !this.pagination.descending
-      } else {
-        this.pagination.sortBy = column
-        this.pagination.descending = false
+    async refresh (notify = true) {
+      this.$apollo.queries.keys.refetch()
+      if (notify) {
+        this.$store.commit('showNotification', {
+          message: this.$t('admin:api.refreshSuccess'),
+          style: 'success',
+          icon: 'cached'
+        })
       }
     },
-    toggleAll () {
-      if (this.selected.length) {
-        this.selected = []
-      } else {
-        this.selected = this.items.slice()
+    async globalSwitch () {
+      this.isToggleLoading = true
+      try {
+        const resp = await this.$apollo.mutate({
+          mutation: gql`
+            mutation ($enabled: Boolean!) {
+              authentication {
+                setApiState (enabled: $enabled) {
+                  responseResult {
+                    succeeded
+                    errorCode
+                    slug
+                    message
+                  }
+                }
+              }
+            }
+          `,
+          variables: {
+            enabled: !this.enabled
+          },
+          watchLoading (isLoading) {
+            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-toggle')
+          }
+        })
+        if (_.get(resp, 'data.authentication.setApiState.responseResult.succeeded', false)) {
+          this.$store.commit('showNotification', {
+            style: 'success',
+            message: this.enabled ? this.$t('admin:api.toggleStateDisabledSuccess') : this.$t('admin:api.toggleStateEnabledSuccess'),
+            icon: 'check'
+          })
+          await this.$apollo.queries.enabled.refetch()
+        } else {
+          this.$store.commit('showNotification', {
+            style: 'red',
+            message: _.get(resp, 'data.authentication.setApiState.responseResult.message', 'An unexpected error occured.'),
+            icon: 'alert'
+          })
+        }
+      } catch (err) {
+        this.$store.commit('pushGraphError', err)
       }
+      this.isToggleLoading = false
     },
-    async refresh() {
-      this.$store.commit('showNotification', {
-        style: 'indigo',
-        message: `Coming soon...`,
-        icon: 'directions_boat'
-      })
+    async newKey () {
+      this.isCreateDialogShown = true
     },
-    async globalSwitch() {
-      this.$store.commit('showNotification', {
-        style: 'indigo',
-        message: `Coming soon...`,
-        icon: 'directions_boat'
-      })
+    revoke (key) {
+      this.current = key
+      this.isRevokeConfirmDialogShown = true
+    },
+    async revokeConfirm () {
+      this.revokeLoading = true
+      try {
+        const resp = await this.$apollo.mutate({
+          mutation: gql`
+            mutation ($id: Int!) {
+              authentication {
+                revokeApiKey (id: $id) {
+                  responseResult {
+                    succeeded
+                    errorCode
+                    slug
+                    message
+                  }
+                }
+              }
+            }
+          `,
+          variables: {
+            id: this.current.id
+          },
+          watchLoading (isLoading) {
+            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-revoke')
+          }
+        })
+        if (_.get(resp, 'data.authentication.revokeApiKey.responseResult.succeeded', false)) {
+          this.$store.commit('showNotification', {
+            style: 'success',
+            message: this.$t('admin:api.revokeSuccess'),
+            icon: 'check'
+          })
+          this.refresh(false)
+        } else {
+          this.$store.commit('showNotification', {
+            style: 'red',
+            message: _.get(resp, 'data.authentication.revokeApiKey.responseResult.message', 'An unexpected error occured.'),
+            icon: 'alert'
+          })
+        }
+      } catch (err) {
+        this.$store.commit('pushGraphError', err)
+      }
+      this.isRevokeConfirmDialogShown = false
+      this.revokeLoading = false
+    }
+  },
+  apollo: {
+    enabled: {
+      query: gql`
+        {
+          authentication {
+            apiState
+          }
+        }
+      `,
+      fetchPolicy: 'network-only',
+      update: (data) => data.authentication.apiState,
+      watchLoading (isLoading) {
+        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-state-refresh')
+      }
     },
-    async newKey() {
-      this.$store.commit('showNotification', {
-        style: 'indigo',
-        message: `Coming soon...`,
-        icon: 'directions_boat'
-      })
+    keys: {
+      query: gql`
+        {
+          authentication {
+            apiKeys {
+              id
+              name
+              keyShort
+              expiration
+              isRevoked
+              createdAt
+              updatedAt
+            }
+          }
+        }
+      `,
+      fetchPolicy: 'network-only',
+      update: (data) => data.authentication.apiKeys,
+      watchLoading (isLoading) {
+        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-keys-refresh')
+      }
     }
   }
 }
diff --git a/package.json b/package.json
index 68c68cc0da1266e82351e99665f7ce21f90128f7..cd63c81023c320d109a8820ea04f64e2f5593cc9 100644
--- a/package.json
+++ b/package.json
@@ -114,6 +114,7 @@
     "moment": "2.24.0",
     "moment-timezone": "0.5.27",
     "mongodb": "3.5.2",
+    "ms": "2.1.2",
     "mssql": "6.0.1",
     "multer": "1.4.2",
     "mysql2": "2.1.0",
diff --git a/server/app/data.yml b/server/app/data.yml
index f313d25a8abadbaa17f77f9fdb85d58a7085c116..f6f0816bcd56aafddee5d5f070bc43b3edce0830 100644
--- a/server/app/data.yml
+++ b/server/app/data.yml
@@ -29,6 +29,8 @@ defaults:
       maxFiles: 10
     offline: false
     # DB defaults
+    api:
+      isEnabled: false
     graphEndpoint: 'https://graph.requarks.io'
     lang:
       code: en
diff --git a/server/core/auth.js b/server/core/auth.js
index 270039e73736050a7bf25531c23fc570ef291199..6f0cde1c2417310d6f753ecfb38160d4ffe2a587 100644
--- a/server/core/auth.js
+++ b/server/core/auth.js
@@ -17,6 +17,7 @@ module.exports = {
     cacheExpiration: moment.utc().subtract(1, 'd')
   },
   groups: {},
+  validApiKeys: [],
 
   /**
    * Initialize the authentication module
@@ -44,6 +45,7 @@ module.exports = {
     })
 
     this.reloadGroups()
+    this.reloadApiKeys()
 
     return this
   },
@@ -64,7 +66,8 @@ module.exports = {
         jwtFromRequest: securityHelper.extractJWT,
         secretOrKey: WIKI.config.certs.public,
         audience: WIKI.config.auth.audience,
-        issuer: 'urn:wiki.js'
+        issuer: 'urn:wiki.js',
+        algorithms: ['RS256']
       }, (jwtPayload, cb) => {
         cb(null, jwtPayload)
       }))
@@ -135,6 +138,31 @@ module.exports = {
         return next()
       }
 
+      // Process API tokens
+      if (_.has(user, 'api')) {
+        if (_.includes(WIKI.auth.validApiKeys, user.api)) {
+          req.user = {
+            id: 1,
+            email: 'api@localhost',
+            name: 'API',
+            pictureUrl: null,
+            timezone: 'America/New_York',
+            localeCode: 'en',
+            permissions: _.get(WIKI.auth.groups, `${user.grp}.permissions`, []),
+            groups: [user.grp],
+            getGlobalPermissions () {
+              return req.user.permissions
+            },
+            getGroups () {
+              return req.user.groups
+            }
+          }
+          return next()
+        } else {
+          return next(new Error('API Key is invalid or was revoked.'))
+        }
+      }
+
       // JWT is valid
       req.logIn(user, { session: false }, (errc) => {
         if (errc) { return next(errc) }
@@ -248,15 +276,23 @@ module.exports = {
   /**
    * Reload Groups from DB
    */
-  async reloadGroups() {
+  async reloadGroups () {
     const groupsArray = await WIKI.models.groups.query()
     this.groups = _.keyBy(groupsArray, 'id')
   },
 
+  /**
+   * Reload valid API Keys from DB
+   */
+  async reloadApiKeys () {
+    const keys = await WIKI.models.apiKeys.query().select('id').where('isRevoked', false).andWhere('expiration', '>', moment.utc().toISOString())
+    this.validApiKeys = _.map(keys, 'id')
+  },
+
   /**
    * Generate New Authentication Public / Private Key Certificates
    */
-  async regenerateCertificates() {
+  async regenerateCertificates () {
     WIKI.logger.info('Regenerating certificates...')
 
     _.set(WIKI.config, 'sessionSecret', (await crypto.randomBytesAsync(32)).toString('hex'))
diff --git a/server/db/migrations-sqlite/2.2.3.js b/server/db/migrations-sqlite/2.2.3.js
new file mode 100644
index 0000000000000000000000000000000000000000..bb29f975c4e649f2e24732eaa9c9fa2deff32df1
--- /dev/null
+++ b/server/db/migrations-sqlite/2.2.3.js
@@ -0,0 +1,14 @@
+exports.up = knex => {
+  return knex.schema
+    .createTable('apiKeys', table => {
+      table.increments('id').primary()
+      table.string('name').notNullable()
+      table.text('key').notNullable()
+      table.string('expiration').notNullable()
+      table.boolean('isRevoked').notNullable().defaultTo(false)
+      table.string('createdAt').notNullable()
+      table.string('updatedAt').notNullable()
+    })
+}
+
+exports.down = knex => { }
diff --git a/server/db/migrations/2.2.3.js b/server/db/migrations/2.2.3.js
new file mode 100644
index 0000000000000000000000000000000000000000..29d87c74352d4133b970d2fc8be027a0a456bb28
--- /dev/null
+++ b/server/db/migrations/2.2.3.js
@@ -0,0 +1,20 @@
+/* global WIKI */
+
+exports.up = knex => {
+  const dbCompat = {
+    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)
+  }
+  return knex.schema
+    .createTable('apiKeys', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.increments('id').primary()
+      table.string('name').notNullable()
+      table.text('key').notNullable()
+      table.string('expiration').notNullable()
+      table.boolean('isRevoked').notNullable().defaultTo(false)
+      table.string('createdAt').notNullable()
+      table.string('updatedAt').notNullable()
+    })
+}
+
+exports.down = knex => { }
diff --git a/server/graph/resolvers/authentication.js b/server/graph/resolvers/authentication.js
index eb27b7afd283d07e7c9087fbc98054ff6be3b3a0..a21793e7d6d51c6bfac7cbd9df12800fce6a78b7 100644
--- a/server/graph/resolvers/authentication.js
+++ b/server/graph/resolvers/authentication.js
@@ -13,6 +13,27 @@ module.exports = {
     async authentication () { return {} }
   },
   AuthenticationQuery: {
+    /**
+     * List of API Keys
+     */
+    async apiKeys (obj, args, context) {
+      const keys = await WIKI.models.apiKeys.query().orderBy(['isRevoked', 'name'])
+      return keys.map(k => ({
+        id: k.id,
+        name: k.name,
+        keyShort: '...' + k.key.substring(k.key.length - 20),
+        isRevoked: k.isRevoked,
+        expiration: k.expiration,
+        createdAt: k.createdAt,
+        updatedAt: k.updatedAt
+      }))
+    },
+    /**
+     * Current API State
+     */
+    apiState () {
+      return WIKI.config.api.isEnabled
+    },
     /**
      * Fetch active authentication strategies
      */
@@ -41,6 +62,19 @@ module.exports = {
     }
   },
   AuthenticationMutation: {
+    /**
+     * Create New API Key
+     */
+    async createApiKey (obj, args, context) {
+      try {
+        return {
+          key: await WIKI.models.apiKeys.createNewKey(args),
+          responseResult: graphHelper.generateSuccess('API Key created successfully')
+        }
+      } catch (err) {
+        return graphHelper.generateError(err)
+      }
+    },
     /**
      * Perform Login
      */
@@ -101,6 +135,36 @@ module.exports = {
         return graphHelper.generateError(err)
       }
     },
+    /**
+     * Set API state
+     */
+    async setApiState (obj, args, context) {
+      try {
+        WIKI.config.api.isEnabled = args.enabled
+        await WIKI.configSvc.saveToDb(['api'])
+        return {
+          responseResult: graphHelper.generateSuccess('API State changed successfully')
+        }
+      } catch (err) {
+        return graphHelper.generateError(err)
+      }
+    },
+    /**
+     * Revoke an API key
+     */
+    async revokeApiKey (obj, args, context) {
+      try {
+        await WIKI.models.apiKeys.query().findById(args.id).patch({
+          isRevoked: true
+        })
+        await WIKI.auth.reloadApiKeys()
+        return {
+          responseResult: graphHelper.generateSuccess('API Key revoked successfully')
+        }
+      } catch (err) {
+        return graphHelper.generateError(err)
+      }
+    },
     /**
      * Update Authentication Strategies
      */
diff --git a/server/graph/schemas/authentication.graphql b/server/graph/schemas/authentication.graphql
index 712c421db344a2190d5205b562623c0e9eabf29e..263a1aff2c0dcc587776bf0401462434992610f9 100644
--- a/server/graph/schemas/authentication.graphql
+++ b/server/graph/schemas/authentication.graphql
@@ -15,6 +15,10 @@ extend type Mutation {
 # -----------------------------------------------
 
 type AuthenticationQuery {
+  apiKeys: [AuthenticationApiKey] @auth(requires: ["manage:system", "manage:api"])
+
+  apiState: Boolean! @auth(requires: ["manage:system", "manage:api"])
+
   strategies(
     isEnabled: Boolean
   ): [AuthenticationStrategy]
@@ -25,6 +29,13 @@ type AuthenticationQuery {
 # -----------------------------------------------
 
 type AuthenticationMutation {
+  createApiKey(
+    name: String!
+    expiration: String!
+    fullAccess: Boolean!
+    group: Int
+  ): AuthenticationCreateApiKeyResponse @auth(requires: ["manage:system", "manage:api"])
+
   login(
     username: String!
     password: String!
@@ -47,12 +58,21 @@ type AuthenticationMutation {
     name: String!
   ): AuthenticationRegisterResponse
 
+  revokeApiKey(
+    id: Int!
+  ): DefaultResponse @auth(requires: ["manage:system", "manage:api"])
+
+  setApiState(
+    enabled: Boolean!
+  ): DefaultResponse @auth(requires: ["manage:system", "manage:api"])
+
   updateStrategies(
     strategies: [AuthenticationStrategyInput]!
     config: AuthenticationConfigInput
   ): DefaultResponse @auth(requires: ["manage:system"])
 
   regenerateCertificates: DefaultResponse @auth(requires: ["manage:system"])
+
   resetGuestUser: DefaultResponse @auth(requires: ["manage:system"])
 }
 
@@ -105,3 +125,18 @@ input AuthenticationConfigInput {
   tokenExpiration: String!
   tokenRenewal: String!
 }
+
+type AuthenticationApiKey {
+  id: Int!
+  name: String!
+  keyShort: String!
+  expiration: Date!
+  createdAt: Date!
+  updatedAt: Date!
+  isRevoked: Boolean!
+}
+
+type AuthenticationCreateApiKeyResponse {
+  responseResult: ResponseStatus
+  key: String
+}
diff --git a/server/master.js b/server/master.js
index b1c1dd75f2dc3e5e6b7e2edc0c2e26346e5e5e9f..ccae1fcede45a6fc6c9bbe1641740ee49ebc63c4 100644
--- a/server/master.js
+++ b/server/master.js
@@ -167,12 +167,22 @@ module.exports = async () => {
   })
 
   app.use((err, req, res, next) => {
-    res.status(err.status || 500)
-    _.set(res.locals, 'pageMeta.title', 'Error')
-    res.render('error', {
-      message: err.message,
-      error: WIKI.IS_DEBUG ? err : {}
-    })
+    if (req.path === '/graphql') {
+      res.status(err.status || 500).json({
+        data: {},
+        errors: [{
+          message: err.message,
+          path: []
+        }]
+      })
+    } else {
+      res.status(err.status || 500)
+      _.set(res.locals, 'pageMeta.title', 'Error')
+      res.render('error', {
+        message: err.message,
+        error: WIKI.IS_DEBUG ? err : {}
+      })
+    }
   })
 
   // ----------------------------------------
diff --git a/server/models/apiKeys.js b/server/models/apiKeys.js
new file mode 100644
index 0000000000000000000000000000000000000000..0707e318058f6d456ef2cbee98ba871c755cbabd
--- /dev/null
+++ b/server/models/apiKeys.js
@@ -0,0 +1,71 @@
+/* global WIKI */
+
+const Model = require('objection').Model
+const moment = require('moment')
+const ms = require('ms')
+const jwt = require('jsonwebtoken')
+
+/**
+ * Users model
+ */
+module.exports = class ApiKey extends Model {
+  static get tableName() { return 'apiKeys' }
+
+  static get jsonSchema () {
+    return {
+      type: 'object',
+      required: ['name', 'key'],
+
+      properties: {
+        id: {type: 'integer'},
+        name: {type: 'string'},
+        key: {type: 'string'},
+        expiration: {type: 'string'},
+        isRevoked: {type: 'boolean'},
+        createdAt: {type: 'string'},
+        validUntil: {type: 'string'}
+      }
+    }
+  }
+
+  async $beforeUpdate(opt, context) {
+    await super.$beforeUpdate(opt, context)
+
+    this.updatedAt = moment.utc().toISOString()
+  }
+  async $beforeInsert(context) {
+    await super.$beforeInsert(context)
+
+    this.createdAt = moment.utc().toISOString()
+    this.updatedAt = moment.utc().toISOString()
+  }
+
+  static async createNewKey ({ name, expiration, fullAccess, group }) {
+    const entry = await WIKI.models.apiKeys.query().insert({
+      name,
+      key: 'pending',
+      expiration: moment.utc().add(ms(expiration), 'ms').toISOString(),
+      isRevoked: true
+    })
+
+    const key = jwt.sign({
+      api: entry.id,
+      grp: fullAccess ? 1 : group
+    }, {
+      key: WIKI.config.certs.private,
+      passphrase: WIKI.config.sessionSecret
+    }, {
+      algorithm: 'RS256',
+      expiresIn: expiration,
+      audience: WIKI.config.auth.audience,
+      issuer: 'urn:wiki.js'
+    })
+
+    await WIKI.models.apiKeys.query().findById(entry.id).patch({
+      key,
+      isRevoked: false
+    })
+
+    return key
+  }
+}
diff --git a/server/models/users.js b/server/models/users.js
index 7b5e74668fd223311fc871e37e84526a2c272f33..426efd07ac6b38d540e0bedfac538c0b9e986dd1 100644
--- a/server/models/users.js
+++ b/server/models/users.js
@@ -26,7 +26,6 @@ module.exports = class User extends Model {
         name: {type: 'string', minLength: 1, maxLength: 255},
         providerId: {type: 'string'},
         password: {type: 'string'},
-        role: {type: 'string', enum: ['admin', 'guest', 'user']},
         tfaIsActive: {type: 'boolean', default: false},
         tfaSecret: {type: 'string'},
         jobTitle: {type: 'string'},
diff --git a/yarn.lock b/yarn.lock
index b6009a94d7a704eaed56eb35ab0e35a96d8a8e40..ff5459b3e8e752fd01b29db67baf74663c21d31c 100644
Binary files a/yarn.lock and b/yarn.lock differ