diff --git a/CHANGELOG.md b/CHANGELOG.md
index 41f569a3647f8d428c5b95674a4ee9050a8e6317..d3689e84e60ae5e3b37ac21425e486bff9a0e6cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,30 @@
 # Changelog
 All notable changes to this project will be documented in this file.
 
-The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [Unreleased]
+
+### Added (for new features, dependecies etc.)
+
+* Module `ext_qrcode` which generates a QR Code for an entity (pointing to the
+  the head or the exact version).
+
+### Changed (for changes in existing functionality)
+
+### Deprecated (for soon-to-be removed features)
+
+### Removed (for now removed features)
+
+* `getEntityId`, a former duplicate of `getEntityID` which must be used instead.
+
+### Fixed (for any bug fixes)
+
+### Security (in case of vulnerabilities)
+
+### Documentation (for notable additions or changes of the documentation)
+
 ## [v0.4.0-rc1] - 2021-06-16
 
 ### Added (for new features, dependecies etc.)
diff --git a/Makefile b/Makefile
index 0aa47756d72dcfcaff88efb673b5fa5044f8e05a..4c64a38d01f0273059e29f6dac0448967b005b86 100644
--- a/Makefile
+++ b/Makefile
@@ -43,7 +43,7 @@ LIBS_DIR = $(abspath libs)
 TEST_CORE_DIR = $(abspath test/core/)
 TEST_EXT_DIR = $(abspath test/ext)
 TEST_SSS_DIR =$(abspath test/server_side_scripting)
-LIBS = fonts css/fonts css/bootstrap.css css/bootstrap-icons.css js/state-machine.js js/jquery.js js/showdown.js js/dropzone.js css/dropzone.css js/loglevel.js js/leaflet.js css/leaflet.css css/images js/leaflet-latlng-graticule.js js/proj4.js js/proj4leaflet.js js/leaflet-coordinates.js css/leaflet-coordinates.css js/leaflet-graticule.js js/bootstrap-select.js css/bootstrap-select.css js/bootstrap-autocomplete.min.js js/plotly.js js/pako.js js/utif.js js/bootstrap.js
+LIBS = fonts css/fonts css/bootstrap.css css/bootstrap-icons.css js/state-machine.js js/jquery.js js/showdown.js js/dropzone.js css/dropzone.css js/loglevel.js js/leaflet.js css/leaflet.css css/images js/leaflet-latlng-graticule.js js/proj4.js js/proj4leaflet.js js/leaflet-coordinates.js css/leaflet-coordinates.css js/leaflet-graticule.js js/bootstrap-select.js css/bootstrap-select.css js/bootstrap-autocomplete.min.js js/plotly.js js/pako.js js/utif.js js/bootstrap.js js/qrcode.js
 
 
 TEST_LIBS = $(LIBS) js/qunit.js css/qunit.css $(subst $(TEST_CORE_DIR)/,,$(shell find $(TEST_CORE_DIR)/))
@@ -299,6 +299,9 @@ $(LIBS_DIR)/js/pako.js: unzip $(LIBS_DIR)/js
 $(LIBS_DIR)/js/utif.js: unzip $(LIBS_DIR)/js
 	ln -s $(LIBS_DIR)/UTIF-8205c1f/UTIF.js $@
 
+$(LIBS_DIR)/js/qrcode.js: unzip $(LIBS_DIR)/js
+	ln -s $(LIBS_DIR)/qrcode-1.4.4/qrcode.min.js $@
+
 
 $(addprefix $(LIBS_DIR)/, js css):
 	mkdir $@ || true
diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties
index 386eb8bd22bd3f32b94d9b9ea4714b80e355ea8e..412773eb86d46151d1126d29c5e539439d68a6fe 100644
--- a/build.properties.d/00_default.properties
+++ b/build.properties.d/00_default.properties
@@ -51,6 +51,7 @@ BUILD_MODULE_EXT_BOTTOM_LINE_TIFF_PREVIEW=DISABLED
 BUILD_MODULE_EXT_BOOKMARKS=ENABLED
 BUILD_MODULE_EXT_ANNOTATION=ENABLED
 BUILD_MODULE_EXT_COSMETICS_LINKIFY=DISABLED
+BUILD_MODULE_EXT_QRCODE=ENABLED
 
 BUILD_MODULE_USER_MANAGEMENT=ENABLED
 BUILD_MODULE_USER_MANAGEMENT_CHANGE_OWN_PASSWORD_REALM=CaosDB
@@ -152,4 +153,6 @@ MODULE_DEPENDENCIES=(
     ext_trigger_crawler_form.js
     ext_bookmarks.js
     ext_cosmetics.js
+    qrcode.js
+    ext_qrcode.js
 )
diff --git a/libs/qrcode-1.4.4.zip b/libs/qrcode-1.4.4.zip
new file mode 100644
index 0000000000000000000000000000000000000000..ed1e0f854985cfcec8cd4f419fe27c195f39c22c
Binary files /dev/null and b/libs/qrcode-1.4.4.zip differ
diff --git a/src/core/js/ext_map.js b/src/core/js/ext_map.js
index 719d4f77943fdfca562961838dc02d95d5fd5cb1..c20cdefc2e9de73a21db81e3a7d5ebacebe73416 100644
--- a/src/core/js/ext_map.js
+++ b/src/core/js/ext_map.js
@@ -1470,7 +1470,7 @@ var caosdb_map = new function () {
          */
         this.make_entity_name_label = function (entity) {
             const name = getEntityName(entity);
-            const id = getEntityId(entity);
+            const id = getEntityID(entity);
 
             const entity_on_page = $(`#${id}`).length > 0;
             const href = entity_on_page ? `#${id}` : connection.getBasePath() + `Entity/${id}`
diff --git a/src/core/js/ext_qrcode.js b/src/core/js/ext_qrcode.js
new file mode 100644
index 0000000000000000000000000000000000000000..d075ef884a89d407cb1e79b98f2045c6d4e25a26
--- /dev/null
+++ b/src/core/js/ext_qrcode.js
@@ -0,0 +1,201 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+"use strict";
+
+/**
+ * Adds QR-Code generation to entities.
+ *
+ * @author Timm Fitschen
+ */
+var ext_qrcode = function ($, connection, getEntityVersion, getEntityID, QRCode, logger) {
+
+    const _buttons_list_class = "caosdb-v-entity-header-buttons-list";
+    const _qrcode_button_class = "caosdb-f-entity-qrcode-button";
+    const _qrcode_canvas_container = "caosdb-f-entity-qrcode";
+    const _qrcode_link_container = "caosdb-f-entity-qrcode-link";
+    const _qrcode_icon = `<i class="bi bi-upc"></i>`;
+
+    /**
+     * Create a new QR Code and a caption with a link, either linking to the
+     * entity head or to the exact version of the entity, based on the selected
+     * radio buttons and insert it into the modal.
+     *
+     * @param {HTMLElement} modal
+     * @param {string} entity_id
+     * @param {string} entity_version
+     */
+    var update_qrcode = function (modal, entity_id, entity_version) {
+        modal = $(modal);
+        const uri = modal.find("input[name=entity-qrcode-versioned]:checked").val();
+        var display_version = "";
+        if (uri.indexOf("@") > -1) {
+            display_version = `@${entity_version.substring(0,8)}`;
+        }
+        const description = `Entity <a href="${uri}">${entity_id}${display_version}</a>`;
+        modal.find(`.${_qrcode_canvas_container}`).empty();
+        modal.find(`.${_qrcode_link_container}`).empty().append(description);
+        QRCode.toCanvas(uri, {
+            "scale": 6
+        }).then((canvas) => {
+            modal.find(`.${_qrcode_canvas_container}`).empty().append(canvas);
+        }).catch(logger.error);
+    }
+
+    /**
+     * Create modal which shows the QR Code and a form where the user can choose
+     * whether the QR Code links to the entity head or the exact version of the
+     * entity.
+     *
+     * @param {string} modal_id - id of the resulting HTMLElement
+     * @param {string} entity_id
+     * @param {string} entity_version
+     * @return {HTMLElement} the resulting modal.
+     */
+    var create_qrcode_modal = function (modal_id, entity_id, entity_version) {
+        const uri = `${connection.getEntityUri([entity_id])}`;
+        const short_version = entity_version.substring(0, 8);
+        const modal = $(`<div class="modal fade" id="${modal_id}" tabindex="-1" aria-labelledby="${modal_id}-label" aria-hidden="true">
+      <div class="modal-dialog">
+        <div class="modal-content">
+          <div class="modal-header">
+            <h4 class="modal-title" id="${modal_id}-label">QR Code</h5>
+            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+          </div>
+          <div class="modal-body text-center">
+            <div class="${_qrcode_canvas_container}"></div>
+            <div class="${_qrcode_link_container}"></div>
+          </div>
+          <div class="modal-footer justify-content-start">
+            <form>
+            <div class="form-check">
+              <label class="form-check-label">
+                <input value="${uri}" class="form-check-input" type="radio" name="entity-qrcode-versioned" checked>
+                Link to this entity.
+              </label>
+            </div>
+            <div class="form-check">
+              <label class="form-check-label" for="flexRadioDefault1">
+                <input value="${uri}@${entity_version}" class="form-check-input" type="radio" name="entity-qrcode-versioned">
+                Link to this exact version of this entity.
+              </label>
+            </div>
+            </form>
+          </div>
+        </div>
+      </div>
+    </div>`);
+        modal.find("form").change(() => {
+            update_qrcode(modal, entity_id, entity_version);
+        });
+        return modal[0];
+    }
+
+    /**
+     * Click handler of the QR Code button. The click event opens a modal showing
+     * the QR Code and a form where the user can choose whether the QR Code links
+     * to the entity head or the exact version of the entity.
+     *
+     * @param {string} entity_id
+     * @param {string} entity_version
+     */
+    var qrcode_button_click_handler = function (entity_id, entity_version) {
+        const modal_id = `qrcode-modal-${entity_id}-${entity_version}`;
+        var modal_element = document.getElementById(modal_id);
+        if (modal_element) {
+            // toggle modal
+            const modal = bootstrap.Modal.getInstance(modal_element);
+            modal.toggle();
+        } else {
+            modal_element = create_qrcode_modal(modal_id, entity_id, entity_version);
+            update_qrcode(modal_element, entity_id, entity_version);
+            $("body").append(modal_element);
+            const options = {};
+            const modal = new bootstrap.Modal(modal_element, options);
+            modal.show();
+        }
+    }
+
+    /**
+     * Create a button which opens the QR Code modal on click.
+     *
+     * @param {string} entity_id
+     * @param {string} entity_version
+     * @return {HTMLElement} the newly created button.
+     */
+    var create_qrcode_button = function (entity_id, entity_version) {
+        const button = $(`<button title="Create QR Code" type="button" class="${_qrcode_button_class} caosdb-v-entity-qrcode-button btn">${_qrcode_icon}</button>`);
+        button.click(() => {
+            qrcode_button_click_handler(entity_id, entity_version);
+        });
+        return button[0];
+    }
+
+    /**
+     * Add a qrcode button to a given entity.
+     * @param {HTMLElement} entity
+     */
+    var add_qrcode_to_entity = function (entity) {
+        const entity_id = getEntityID(entity);
+        const entity_version = getEntityVersion(entity);
+
+        $(entity).find(`.${_buttons_list_class}`).append(create_qrcode_button(entity_id, entity_version));
+    }
+
+    var remove_qrcode_button = function (entity) {
+        $(entity).find(`.${_buttons_list_class} .${_qrcode_button_class}`).remove();
+    }
+
+    var _init = function () {
+        for (let entity of $(".caosdb-entity-panel")) {
+            remove_qrcode_button(entity);
+            add_qrcode_to_entity(entity);
+        }
+    }
+
+    /**
+     * Initialize this module and append a QR Code button to all entities panels on the page.
+     *
+     * Removes all respective buttons if present before adding a new one.
+     */
+    var init = function () {
+        _init();
+
+        // edit-mode-listener
+        document.body.addEventListener(edit_mode.end_edit.type, _init, true);
+    };
+
+    return {
+        update_qrcode: update_qrcode,
+        add_qrcode_to_entity: add_qrcode_to_entity,
+        remove_qrcode_button: remove_qrcode_button,
+        create_qrcode_button: create_qrcode_button,
+        create_qrcode_modal: create_qrcode_modal,
+        qrcode_button_click_handler: qrcode_button_click_handler,
+        init: init
+    };
+
+}($, connection, getEntityVersion, getEntityID, QRCode, console);
+
+$(document).ready(function () {
+    if ("${BUILD_MODULE_EXT_QRCODE}" == "ENABLED") {
+        caosdb_modules.register(ext_qrcode);
+    }
+});
\ No newline at end of file
diff --git a/src/core/js/fileupload.js b/src/core/js/fileupload.js
index 31d86589286f1761f481cc9d9bb6a557a63cbce1..8a988e86a6a9b5bbb39f39a37d63b32b144c748d 100644
--- a/src/core/js/fileupload.js
+++ b/src/core/js/fileupload.js
@@ -156,7 +156,7 @@ var fileupload = new function() {
         // get property-value input element (in case of FILE property)
         var input = $(property).find(".caosdb-f-property-value input");
         var set_value = function(entity) {
-            input.val(getEntityId(entity));
+            input.val(getEntityID(entity));
         }
 
         if (input.length == 0) {
@@ -207,7 +207,7 @@ var fileupload = new function() {
                 getEntityName(entity) + `</code> has been uploaded.</div>`);
 
             input.after(`<a class="btn btn-secondary btn-sm"
-  href="` + connection.getEntityUri([getEntityId(entity)]) + `" target= "_blank">` +
+  href="` + connection.getEntityUri([getEntityID(entity)]) + `" target= "_blank">` +
                 getEntityName(entity) + `</a>`);
 
         };
diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js
index 74dc62be15551f987707253ca201777f7c3929ac..dbb4e269a247cbbc40f1ea58623ec7b515dc2d57 100644
--- a/src/core/js/webcaosdb.js
+++ b/src/core/js/webcaosdb.js
@@ -812,7 +812,7 @@ this.transaction = new function () {
                     $(updatePanel).insertBefore(entity);
                     // create and add waiting notification
                     updatePanel.appendChild(transaction.update.createWaitRetrieveNotification());
-                    let entityId = getEntityId(entity);
+                    let entityId = getEntityID(entity);
                     transaction.update.retrieveOldEntityXmlString(entityId).then(xmlstr => {
                         app.openForm(xmlstr);
                     }, err => {
@@ -1524,19 +1524,6 @@ function removeAllWaitingNotifications(elem) {
     return elem;
 }
 
-/**
- * Extract the ID of an entity by parsing the textContent of the first occuring element with
- * class `caosdb-id`.
- *
- * @param {HTMLElement} entity
- * @returns {Number} ID of entity.
- */
-function getEntityId(entity) {
-    let id = Number.parseInt(entity.getElementsByClassName("caosdb-id")[0].textContent);
-    if (isNaN(id)) throw new Error("id was NaN");
-    return id;
-}
-
 // TODO remove and use connection.post
 /**
  * Post an xml document to basepath/Entity
diff --git a/test/core/js/modules/ext_qrcode.js.js b/test/core/js/modules/ext_qrcode.js.js
new file mode 100644
index 0000000000000000000000000000000000000000..d4d505913035d17d14cb7b110e8dc67b0a018a44
--- /dev/null
+++ b/test/core/js/modules/ext_qrcode.js.js
@@ -0,0 +1,101 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+'use strict';
+
+QUnit.module("ext_qrcode.js", {
+    before: function (assert) {
+        // setup before module
+    },
+    beforeEach: function (assert) {
+        // setup before each test
+        $(document.body).append('<div data-entity-id="eid123" data-version-id="vid234" id="ext-qrcode-test-entity" class="caosdb-entity-panel"><div class="caosdb-v-entity-header-buttons-list"></div></div>');
+    },
+    afterEach: function (assert) {
+        // teardown after each test
+        const modal = bootstrap.Modal.getInstance($(".modal")[0]);
+        if (modal) modal.dispose();
+        $("#ext-qrcode-test-entity").remove();
+        $(".modal").remove();
+    },
+    after: function (assert) {
+        // teardown after module
+    }
+});
+
+QUnit.test("init", function (assert) {
+    assert.ok(ext_qrcode.init, "init available");
+    assert.equal($(".caosdb-f-entity-qrcode-button").length, 0, "no button before.");
+    ext_qrcode.init();
+    assert.equal($(".caosdb-f-entity-qrcode-button").length, 1, "button has been added.");
+    ext_qrcode.init();
+    assert.equal($(".caosdb-f-entity-qrcode-button").length, 1, "still only one button.");
+
+    ext_qrcode.remove_qrcode_button($("#ext-qrcode-test-entity")[0]);
+    assert.equal($(".caosdb-f-entity-qrcode-button").length, 0, "no button after removal.");
+});
+
+QUnit.test("create_qrcode_button", function (assert) {
+    assert.equal(ext_qrcode.create_qrcode_button("entityid", "versionid").tagName, "BUTTON", "create_qrcode_button creates a button");
+});
+
+
+QUnit.test("qrcode_button_click_handler", function (assert) {
+    var done = assert.async();
+    assert.equal($("#qrcode-modal-entityid-versionid").length, 0, "no modal before first click");
+    ext_qrcode.qrcode_button_click_handler("entityid", "versionid")
+    $("#qrcode-modal-entityid-versionid").on("shown.bs.modal", done);
+    assert.equal($("#qrcode-modal-entityid-versionid").length, 1, "first click added the modal");
+});
+
+QUnit.test("update_qrcode", async function (assert) {
+    // create modal
+    const entity_id = "eid456";
+    const entity_version = "vid3564";
+    const modal_id = `qrcode-modal-${entity_id}-${entity_version}`;
+    const modal_element = ext_qrcode.create_qrcode_modal(modal_id, entity_id, entity_version);
+    $(document.body).append(modal_element);
+
+    assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode *").length, 0, "no qrcode.");
+    assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode-link *").length, 0, "no link.");
+
+    // update adds qrcode
+    ext_qrcode.update_qrcode(modal_element, entity_id, entity_version);
+
+    assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode-link a")[0].href, connection.getEntityUri([entity_id]), "link points to entity head.");
+    // wait until qrcode is ready
+    await sleep(500);
+    assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode canvas").length, 1, "qrcode is there.");
+
+    $("#" + modal_id).find("canvas").remove();
+    assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode canvas").length, 0, "removed qrcode canvas for next test.");
+    // select radio button for link to exact version: check both...
+    $("#" + modal_id).find("input[name=entity-qrcode-versioned]").prop("checked", true);
+    // ...then uncheck first
+    $("#" + modal_id).find("input[name=entity-qrcode-versioned]").first().prop("checked", false);
+    $("#" + modal_id).find("form").trigger("change");
+
+    // check: uri has changed
+    assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode-link a")[0].href, connection.getEntityUri([entity_id]) + "@" + entity_version, "link changed to versioned entity.");
+    // wait until qrcode is ready
+    await sleep(500);
+    assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode canvas").length, 1, "qrcode there again.");
+
+});
\ No newline at end of file
diff --git a/test/core/js/modules/webcaosdb.js.js b/test/core/js/modules/webcaosdb.js.js
index 52bf4ada52d2ce59b59d8615c89ca5796343622b..ad26e8455fb7ca16a9e6e9687606b6aba1d0e7e6 100644
--- a/test/core/js/modules/webcaosdb.js.js
+++ b/test/core/js/modules/webcaosdb.js.js
@@ -91,28 +91,6 @@ QUnit.test("injectTemplate", async function (assert) {
     assert.equal(xml2str(result_xml), "<newroot xmlns=\"http://www.w3.org/1999/xhtml\">content</newroot>");
 });
 
-QUnit.test("getEntityId", function (assert) {
-    assert.ok(getEntityId, "function available");
-    let okElem = $('<div><div class="caosdb-id">1234</div></div>')[0];
-    let notOkElem = $('<div><div class="caosdb-id">asdf</div></div>')[0];
-    let emptyElem = $('<div></div>')[0];
-
-    assert.throws(() => {
-        getEntityId();
-    }, "no parameter throws");
-    assert.throws(() => {
-        getEntityId(null);
-    }, "null parameter throws");
-    assert.throws(() => {
-        getEntityId(notOkElem);
-    }, "on-integer ID throws");
-    assert.throws(() => {
-        getEntityId(empty);
-    }, "empty elem throws");
-
-    assert.equal("1234", getEntityId(okElem), "ID found");
-});
-
 QUnit.test("asyncXslt", function (assert) {
     let xml_str = '<root/>';
     let xsl_str = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:output method="html" /><xsl:template match="root"><newroot/></xsl:template></xsl:stylesheet>';
@@ -920,7 +898,7 @@ QUnit.test("createCarouselNav", function (assert) {
         assert.equal($(carousel).find("." + preview.classNamePreviewCarouselNav).length, 1, "carousel has nav");
         assert.equal($(carousel).find(".carousel-inner").length, 1, "carousel has inner");
         for (let i = 0; i < correct_order_id.length; i++) {
-            assert.equal(getEntityId($(carousel).find('.carousel-item')[i]), correct_order_id[i], "entities ids are in order")
+            assert.equal(getEntityID($(carousel).find('.carousel-item')[i]), correct_order_id[i], "entities ids are in order")
         }
 
         assert.ok(carousel.id, "has id");