diff --git a/pass/app/PaymentReference.js b/pass/app/PaymentReference.js
index 6e756de7b585be40db31f0a879e413733ac86134..787ce1a830b04a7c78e4cac0c547a3402af8f447 100644
--- a/pass/app/PaymentReference.js
+++ b/pass/app/PaymentReference.js
@@ -59,12 +59,14 @@ class PaymentReference {
    * @param {number} amount
    * @param {string} key
    * @param {dayjs.Dayjs} expiration
+   * @param {boolean} expiration
    * @returns
    */
   static async CREATE_NEW_REQUEST(
     amount,
     key,
-    expiration = dayjs().add(PaymentReference.DEFAULT_EXPIRATION_HOURS, "hours")
+    expiration = dayjs().add(PaymentReference.DEFAULT_EXPIRATION_HOURS, "hours"),
+    ignore_charge_limit = false
   ) {
     // Calculate price from amount
     let price = amount * config.get("price.per_token");
@@ -78,7 +80,7 @@ class PaymentReference {
 
     return Key.GET_KEY(key)
       .then((key) => {
-        if (!key.isChargable()) {
+        if (!key.isChargable() && !ignore_charge_limit) {
           throw "Key cannot be charged";
         } else {
           loaded_key = key;
diff --git a/pass/lang/de/admin.json b/pass/lang/de/admin.json
index 577514ae0b3a814efd5b1d130574a9c86d1f3d01..423bf472bcdd604b79c75208c4f529317420028a 100644
--- a/pass/lang/de/admin.json
+++ b/pass/lang/de/admin.json
@@ -2,14 +2,16 @@
   "breadcrumps": {
     "overview": "Ãœbersicht",
     "payments-cash": "Barzahlung erfassen",
-    "receipt": "Rechnung erstellen"
+    "receipt": "Rechnung erstellen",
+    "key-management": "Schlüsselverwaltung"
   },
   "index": {
     "heading": "MetaGer Schlüssel Admin",
     "actions": {
       "cash-payment": "Bargeldzahlung erfassen",
       "heading": "Aktionen:",
-      "receipt": "Rechnung erstellen"
+      "receipt": "Rechnung erstellen",
+      "keymanagement": "Schlüssel verwalten"
     }
   },
   "cash-payment": {
@@ -49,5 +51,28 @@
       "label": "Anschrift"
     },
     "submit-userdata": "Rechnungsdaten übernehmen"
+  },
+  "key": {
+    "key-input": {
+      "label": "Schlüssel eingeben",
+      "submit": "Abschicken"
+    },
+    "key-overview": {
+      "charge": "Guthaben: {{token}}",
+      "charge-success": "Aufladung erfolgreich",
+      "delete": "Löschen",
+      "expiration": "Gültig bis {{expiration}}",
+      "charge-form": {
+        "heading": "Manuelle Aufladung",
+        "hint": "Für eine manuelle Aufladung wird keine Zahlung hinterlegt. Das Guthaben wird einfach ohne Gegenleistung gutgeschrieben. Vorsichtig verwenden!",
+        "amount": {
+          "label": "Anzahl"
+        },
+        "price": {
+          "label": "Betrag (€)"
+        },
+        "submit": "Jetzt aufladen!"
+      }
+    }
   }
-}
+}
\ No newline at end of file
diff --git a/pass/public/js/admin/key-overview.js b/pass/public/js/admin/key-overview.js
new file mode 100644
index 0000000000000000000000000000000000000000..971e25916694412a20f5d308277252c1b79dd5dd
--- /dev/null
+++ b/pass/public/js/admin/key-overview.js
@@ -0,0 +1,14 @@
+let amount_input = document.querySelector("input[name=amount]");
+let price_input = document.querySelector("input[name=price]");
+
+amount_input.addEventListener("change", e => {
+    if (e.target.value) {
+        price_input.value = "";
+    }
+});
+
+price_input.addEventListener("change", e => {
+    if (e.target.value) {
+        amount_input.value = "";
+    }
+});
\ No newline at end of file
diff --git a/pass/public/styles/admin/index.css b/pass/public/styles/admin/index.css
index 28890d16f5a1857fa154143823c1943ceb7625b0..03bf180a1260ab3a989db468fa4e5945bd1b5ac8 100644
--- a/pass/public/styles/admin/index.css
+++ b/pass/public/styles/admin/index.css
@@ -1 +1 @@
-div#admin-container div#action-container{display:flex;flex-wrap:wrap;margin-top:2rem}div#admin-container div#action-container>a{display:flex;gap:.5rem;align-items:center;border:1px solid #ef7700;padding:1rem;border-radius:10px;text-decoration:none;color:inherit;cursor:pointer}div#admin-container div#action-container>a>img{width:2em}
\ No newline at end of file
+div#admin-container div#action-container{display:flex;gap:1rem;flex-wrap:wrap;margin-top:2rem}div#admin-container div#action-container>a{display:flex;gap:.5rem;align-items:center;border:1px solid #ef7700;padding:1rem;border-radius:10px;text-decoration:none;color:inherit;cursor:pointer}div#admin-container div#action-container>a>img{width:2em}
\ No newline at end of file
diff --git a/pass/public/styles/admin/index.less b/pass/public/styles/admin/index.less
index 9c7074db4cd1d4c4231c457a9ced409136ef035c..4ffe815d97b74164885500c0c7ad27ae53d8398c 100644
--- a/pass/public/styles/admin/index.less
+++ b/pass/public/styles/admin/index.less
@@ -3,6 +3,7 @@
 div#admin-container {
   div#action-container {
     display: flex;
+    gap: 1rem;
     flex-wrap: wrap;
     margin-top: 2rem;
     > a {
diff --git a/pass/public/styles/admin/key-management.css b/pass/public/styles/admin/key-management.css
new file mode 100644
index 0000000000000000000000000000000000000000..5e8c8297bb63e32dc0e231aeb7e48d5c5115cf8c
--- /dev/null
+++ b/pass/public/styles/admin/key-management.css
@@ -0,0 +1 @@
+form{display:grid;gap:1rem}form .input-group{display:grid;gap:.5rem;grid-template-columns:max-content}form .input-group label{font-weight:bold}form .input-group input{padding:.25rem .5rem;border-radius:5px}form button.button{padding:.5rem}
\ No newline at end of file
diff --git a/pass/public/styles/admin/key-management.less b/pass/public/styles/admin/key-management.less
new file mode 100644
index 0000000000000000000000000000000000000000..b4970d7a1bbfc46bfc650da479edb214516add51
--- /dev/null
+++ b/pass/public/styles/admin/key-management.less
@@ -0,0 +1,20 @@
+form {
+  display: grid;
+  gap: 1rem;
+  .input-group {
+    display: grid;
+    gap: 0.5rem;
+    grid-template-columns: max-content;
+    label {
+      font-weight: bold;
+    }
+    input {
+      padding: 0.25rem 0.5rem;
+      border-radius: 5px;
+    }
+  }
+
+  button.button {
+    padding: 0.5rem;
+  }
+}
diff --git a/pass/public/styles/admin/key-overview.css b/pass/public/styles/admin/key-overview.css
new file mode 100644
index 0000000000000000000000000000000000000000..2fc4a7bae848fcd3176071ad2af2fe0936ac941b
--- /dev/null
+++ b/pass/public/styles/admin/key-overview.css
@@ -0,0 +1 @@
+#admin-container{display:grid;gap:1rem}#admin-container .breadcrumps{margin:0}#admin-container #key-info{display:grid;gap:.5rem}#admin-container #key-info h2{margin:0}#admin-container #key-info .charge-success{color:green}#admin-container #key-info #charges{display:flex;gap:1rem;flex-wrap:wrap}#admin-container #key-info #charges .charge{display:grid;gap:.5rem;place-items:center;border:1px solid #777;width:max-content;padding:1rem;border-radius:5px}#admin-container #key-info #charges .charge .charge-amount{font-size:2rem}#admin-container form#charge-form{display:grid;grid-template-columns:repeat(3, auto);gap:1rem;place-items:center}#admin-container form#charge-form .heading,#admin-container form#charge-form .hint,#admin-container form#charge-form .button,#admin-container form#charge-form #errors{grid-column:span 3}@media (max-width:530px){#admin-container form#charge-form{grid-template-columns:auto}#admin-container form#charge-form .heading,#admin-container form#charge-form .hint,#admin-container form#charge-form .button,#admin-container form#charge-form #errors{grid-column:1}}#admin-container form#charge-form h3{margin:0}#admin-container form#charge-form #errors{list-style-type:none;padding:0;color:red}#admin-container form#charge-form .button{padding:.5rem;cursor:pointer}#admin-container form#charge-form .input-group{display:grid;place-items:center;gap:.5rem}#admin-container form#charge-form .input-group>label{font-weight:bold}#admin-container form#charge-form .input-group input{padding:.25rem .5rem;border-radius:5px}
\ No newline at end of file
diff --git a/pass/public/styles/admin/key-overview.less b/pass/public/styles/admin/key-overview.less
new file mode 100644
index 0000000000000000000000000000000000000000..674bf101bd26c76c4c8f0e91de5f3a28fad7ed36
--- /dev/null
+++ b/pass/public/styles/admin/key-overview.less
@@ -0,0 +1,80 @@
+#admin-container {
+  display: grid;
+  gap: 1rem;
+  .breadcrumps {
+    margin: 0;
+  }
+  #key-info {
+    display: grid;
+    gap: 0.5rem;
+    h2 {
+      margin: 0;
+    }
+    .charge-success {
+      color: green;
+    }
+    #charges {
+      display: flex;
+      gap: 1rem;
+      flex-wrap: wrap;
+      .charge {
+        display: grid;
+        gap: 0.5rem;
+        place-items: center;
+        border: 1px solid #777;
+        width: max-content;
+        padding: 1rem;
+        border-radius: 5px;
+        .charge-amount {
+          font-size: 2rem;
+        }
+      }
+    }
+  }
+  form#charge-form {
+    display: grid;
+    grid-template-columns: repeat(3, auto);
+
+    gap: 1rem;
+    place-items: center;
+    .heading,
+    .hint,
+    .button,
+    #errors {
+      grid-column: span 3;
+    }
+    @media (max-width: 530px) {
+      grid-template-columns: auto;
+      .heading,
+      .hint,
+      .button,
+      #errors {
+        grid-column: 1;
+      }
+    }
+    h3 {
+      margin: 0;
+    }
+    #errors {
+      list-style-type: none;
+      padding: 0;
+      color: red;
+    }
+    .button {
+      padding: 0.5rem;
+      cursor: pointer;
+    }
+    .input-group {
+      display: grid;
+      place-items: center;
+      gap: 0.5rem;
+      > label {
+        font-weight: bold;
+      }
+      input {
+        padding: 0.25rem 0.5rem;
+        border-radius: 5px;
+      }
+    }
+  }
+}
diff --git a/pass/routes/admin/index.js b/pass/routes/admin/index.js
index 98f9d8e10946edc42dc9cba08bde15704d6940b8..2ca7f2ecb1c31c3460f6cc8b1dc162c5338471b5 100644
--- a/pass/routes/admin/index.js
+++ b/pass/routes/admin/index.js
@@ -8,6 +8,7 @@ const {
   matchedData,
   body,
   query,
+  oneOf
 } = require("express-validator");
 const OrderReceipt = require("../../app/pdf/OrderReceipt");
 const crypto = require("crypto");
@@ -15,6 +16,7 @@ const Payment = require("../../app/Payment");
 const PaymentReference = require("../../app/PaymentReference");
 const Receipt = require("../../app/Receipt");
 const Cash = require("../../app/payment_processor/Cash");
+const Key = require("../../app/Key");
 
 router.use((req, res, next) => {
   let cookie_path = new URL(res.locals.baseDir).pathname.replace(/(\/)?$/, "/admin");
@@ -320,4 +322,76 @@ router.post(
   }
 );
 
+router.get("/key", (req, res) => {
+  res.render("admin/key/index");
+});
+
+router.post("/key", (req, res) => {
+  Key.GET_KEY(req.body.key, false).then(key => {
+    return res.redirect(`${res.baseDir}/admin/key/${key.get_key()}`);
+  });
+})
+
+router.use("/key/:key", (req, res, next) => {
+  if (req.query.charge_success) {
+    res.locals.success = true;
+  }
+  Key.GET_KEY(req.params.key, false).then(key => {
+    res.locals.key = key;
+    next();
+  });
+});
+
+router.get("/key/:key", (req, res) => {
+  res.render("admin/key/overview");
+});
+router.post("/key/:key",
+  oneOf([
+    body("amount").isInt({ gt: 0 }),
+    body("price").isCurrency({ allow_negatives: false, allow_decimal: true })
+  ]),
+  (req, res) => {
+    let queryData = matchedData(req, { location: ["body"] });
+    let amount = queryData.amount;
+    if (!amount) {
+      // If amount is not given but price is
+      // Calculate amount from price
+      let price = queryData.price;
+      if (price) {
+        amount = Math.ceil(price / config.get("price.per_token"));
+      }
+    }
+    const errors = validationResult(req);
+    if (!errors.isEmpty()) {
+      res.locals.errors = errors.errors;
+      res.render("admin/key/overview");
+      return;
+    }
+    return PaymentReference.CREATE_NEW_REQUEST(amount, res.locals.key.get_key(), undefined, true)
+      .then(payment_reference => payment_reference.chargeKey())
+      .then(() => {
+        res.redirect(`${res.baseDir}/admin/key/${res.locals.key.get_key()}?charge_success=true`);
+      });
+  });
+
+router.post("/key/:key/remove-charge",
+  body("payment_reference").notEmpty().isInt().toInt(),
+  (req, res) => {
+    let queryData = matchedData(req, { location: ["body"] });
+    const errors = validationResult(req);
+    if (!errors.isEmpty()) {
+      return res.redirect(`${res.baseDir}/admin/key/${res.locals.key.get_key()}?charge_success=true`);
+    }
+    let payment_reference_id = queryData.payment_reference;
+    /** @type {Key} */
+    let key = res.locals.key;
+    let payment_reference_charge = key.get_charge(payment_reference_id);
+    return Key.GET_KEY(key.get_key(), true).then(writable_key => {
+      writable_key.discharge_key(payment_reference_charge, payment_reference_id);
+      return writable_key.save();
+    }).then(() => {
+      res.redirect(`${res.baseDir}/admin/key/${res.locals.key.get_key()}?charge_success=true`);
+    });
+  });
+
 module.exports = router;
diff --git a/pass/views/admin/index.ejs b/pass/views/admin/index.ejs
index 3b9e912eb17275e295a045ce697a94c0ddac53ef..f9b301aab61ffc493817d095a3f0a1f9b21e1a0a 100644
--- a/pass/views/admin/index.ejs
+++ b/pass/views/admin/index.ejs
@@ -14,6 +14,10 @@
       <img src="<%= baseDir %>/images/invoice.svg" alt="Geldschein">
       <div><%= req.t("index.actions.receipt", {ns: "admin"}) _%></div>
     </a>
+    <a id="action-keymanagement" href="<%= baseDir %>/admin/key">
+      <img src="<%= baseDir %>/images/key-icon.svg" alt="Geldschein">
+      <div><%= req.t("index.actions.keymanagement", {ns: "admin"}) _%></div>
+    </a>
   </div>
 </div>
 <%- include('../templates/page_footer'); -%>
\ No newline at end of file
diff --git a/pass/views/admin/key/index.ejs b/pass/views/admin/key/index.ejs
new file mode 100644
index 0000000000000000000000000000000000000000..d716519ab667e8bdd0ceb3efc3eca3fcbc770802
--- /dev/null
+++ b/pass/views/admin/key/index.ejs
@@ -0,0 +1,19 @@
+<%- include('../../templates/page_header', {css: [`${baseDir}/styles/admin/base.css`, `${baseDir}/styles/admin/key-management.css`], js: []}); %>
+<div id="admin-container">
+  <ul class="breadcrumps">
+    <li>
+      <a href="<%= baseDir _%>/admin/">
+        <%= req.t("breadcrumps.overview", {ns: "admin"}) _%>
+      </a>
+    </li>
+    <li><%= req.t("breadcrumps.key-management", {ns: "admin"}) _%></li>
+  </ul>
+  <form method="POST">
+    <div class="input-group">
+      <label for="key"><%= req.t("key.key-input.label", {ns: "admin"}) _%></label>
+      <input type="text" name="key" id="key" size="36" placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX">
+    </div>
+    <button class="button"><%= req.t("key.key-input.submit", {ns: "admin"}) _%></button>
+  </form>
+</div>
+<%- include('../../templates/page_footer'); -%>
\ No newline at end of file
diff --git a/pass/views/admin/key/overview.ejs b/pass/views/admin/key/overview.ejs
new file mode 100644
index 0000000000000000000000000000000000000000..3d90d9d3a02bcb893f96b548b18f249bedf93192
--- /dev/null
+++ b/pass/views/admin/key/overview.ejs
@@ -0,0 +1,52 @@
+<%- include('../../templates/page_header', {css: [`${baseDir}/styles/admin/base.css`, `${baseDir}/styles/admin/key-overview.css`], js: [`${baseDir}/js/admin/key-overview.js`]}); %>
+<div id="admin-container">
+  <ul class="breadcrumps">
+    <li>
+      <a href="<%= baseDir _%>/admin/">
+        <%= req.t("breadcrumps.overview", {ns: "admin"}) _%>
+      </a>
+    </li>
+    <li><%= req.t("breadcrumps.key-management", {ns: "admin"}) _%></li>
+  </ul>
+  <div id="key-info">
+    <h1><%= key.get_key() _%></h1>
+    <h2><%= req.t("key.key-overview.charge", {ns: "admin", token: key.get_charge()}) _%></h2>
+    <%_ if(typeof success !== "undefined") { %>
+    <div class="charge-success"><%= req.t("key.key-overview.charge-success", {ns: "admin"}) _%></div>
+    <%_ } _%>
+    <div id="charges">
+      <%_ for(let i = 0; i < key.get_charge_orders().length; i++) { _%>
+      <div class="charge">
+        <div class="charge-amount"><%= key.get_charge_orders()[i].amount _%></div>
+        <div><%= req.t("key.key-overview.expiration", {ns: "admin", expiration: key.get_charge_orders()[i].expiration.format("DD.MM.YYYY HH:mm:ss")}) _%></div>
+        <form method="POST" action="<%= `${baseDir}/admin/key/${key.get_key()}/remove-charge`%>">
+          <input type="hidden" name="payment_reference" value="<%= key.get_charge_orders()[i].payment_reference_id _%>">
+          <button type="submit"><%= req.t("key.key-overview.delete", {ns: "admin"}) _%></button>
+        </form>
+      </div>
+      <%_ } _%>
+    </div>
+  </div>
+  <form method="POST" id="charge-form">
+    <h3 class="heading"><%= req.t("key.key-overview.charge-form.heading", {ns: "admin"}) _%></h3>
+    <div class="hint"><%= req.t("key.key-overview.charge-form.hint", {ns: "admin"}) _%></div>
+    <%_ if(typeof errors !== "undefined") { _%>
+    <ul id="errors">
+      <%_ for(let i = 0; i < errors.length; i++) { _%>
+      <li class="error"><%= errors[i].param + ": " + errors[i].msg _%></li>
+      <%_ } _%>
+    </ul>
+    <%_ } _%>
+    <div class="input-group">
+      <label for="amount"><%= req.t("key.key-overview.charge-form.amount.label", {ns: "admin"}) _%></label>
+      <input type="number" name="amount" id="amount" min="0" step="1" placeholder="1000">
+    </div>
+    <div>oder</div>
+    <div class="input-group">
+      <label for="price"><%= req.t("key.key-overview.charge-form.price.label", {ns: "admin"}) _%></label>
+      <input type="number" name="price" id="price" min="0" step="0.01" placeholder="10.00">
+    </div>
+    <button class="button" type="submit"><%= req.t("key.key-overview.charge-form.submit", {ns: "admin"}) _%></buton>
+  </form>
+</div>
+<%- include('../../templates/page_footer'); -%>
\ No newline at end of file