diff --git a/nomad/api.py b/nomad/api.py
index 438239c943c2efce122befc5f0c90a8e44f63053..e75833e8ac7d3253e723b892c9f63c90a7dddd65 100644
--- a/nomad/api.py
+++ b/nomad/api.py
@@ -63,6 +63,7 @@ def login_really_required(func):
             abort(401, message='Anonymous access is forbidden, authorization required')
         else:
             return func(*args, **kwargs)
+    wrapper.__name__ = func.__name__
     return wrapper
 
 
@@ -300,6 +301,53 @@ class UploadRes(Resource):
 
         return result, 200
 
+    @login_really_required
+    def post(self, upload_id):
+        """
+        Move an upload out of the staging area. This changes the visibility of the upload.
+        Clients can specify, if the calcs should be restricted.
+
+        .. :quickref: upload; Move an upload out of the staging area.
+
+        **Example request**:
+
+        .. sourcecode:: http
+
+            POST /nomadxt/api/uploads HTTP/1.1
+            Accept: application/json
+            Content-Type: application/json
+
+            {
+                operation: 'unstage'
+            }
+
+
+        :param string upload_id: the upload id
+        :resheader Content-Type: application/json
+        :status 200: upload unstaged successfully
+        :status 404: upload could not be found
+        :status 400: if the operation is not supported
+        :returns: the upload record
+        """
+        try:
+            upload = Upload.get(upload_id)
+        except KeyError:
+            abort(404, message='Upload with id %s does not exist.' % upload_id)
+
+        if upload.user_id != g.user.email:
+            abort(404, message='Upload with id %s does not exist.' % upload_id)
+
+        json_data = request.get_json()
+        if json_data is None:
+            json_data = {}
+
+        operation = json_data.get('operation')
+        if operation == 'unstage':
+            upload.unstage()
+            return upload.json_dict, 200
+
+        abort(400, message='Unsuported operation %s.' % operation)
+
     @login_really_required
     def delete(self, upload_id):
         """
diff --git a/nomad/processing/data.py b/nomad/processing/data.py
index fd7f0de64f38d57f4ecb0e792e34862645e0b3b2..ae921ffbaac40ee1005532f8ed969702e0f2ad02 100644
--- a/nomad/processing/data.py
+++ b/nomad/processing/data.py
@@ -234,7 +234,7 @@ class Upload(Proc):
     @classmethod
     def user_uploads(cls, user: User) -> List['Upload']:
         """ Returns all uploads for the given user. Currently returns all uploads. """
-        return cls.objects()
+        return cls.objects(user_id=user.email, in_staging=True)
 
     def get_logger(self, **kwargs):
         logger = super().get_logger()
@@ -263,7 +263,7 @@ class Upload(Proc):
             files.delete_archives(upload_hash=self.upload_hash)
 
             # delete repo entries
-            RepoCalc.search().query('match', upload_id=self.upload_id).delete()
+            RepoCalc.delete_upload(upload_id=self.upload_id)
 
             # delete calc processings
             Calc.objects(upload_id=self.upload_id).delete()
@@ -304,6 +304,11 @@ class Upload(Proc):
         else:
             return False
 
+    def unstage(self):
+        self.in_staging = False
+        RepoCalc.update_upload(upload_id=self.upload_id, staging=False)
+        self.save()
+
     @property
     def json_dict(self) -> dict:
         """ A json serializable dictionary representation. """
diff --git a/nomad/repo.py b/nomad/repo.py
index 32cc04a0f6c7bba0c05f6561d22f367a7cb671c6..526255caabf149fd18cb892c960144e8f80da848 100644
--- a/nomad/repo.py
+++ b/nomad/repo.py
@@ -148,6 +148,17 @@ class RepoCalc(ElasticDocument):
 
         return calc
 
+    @staticmethod
+    def delete_upload(upload_id):
+        """ Deletes all repo entries of the given upload. """
+        RepoCalc.search().query('match', upload_id=upload_id).delete()
+
+    @classmethod
+    def update_upload(cls, upload_id, **kwargs):
+        """ Update all entries of given upload with keyword args. """
+        for calc in RepoCalc.search().query('match', upload_id=upload_id):
+            calc.update(**kwargs)
+
     @staticmethod
     def es_search(body):
         """ Perform an elasticsearch and not elasticsearch_dsl search on the Calc index. """
diff --git a/tests/test_api.py b/tests/test_api.py
index a570edf7c36fce831c2e64098b5981dac0f448f9..19b8030c608e0c776d1aab4425ae53f73be68ddd 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -216,7 +216,16 @@ def test_processing(client, file, worker, mocksearch, test_user_auth):
         upload = assert_upload(rv.data)
         assert len(upload['calcs']['results']) == 1
 
-    time.sleep(1)
+    rv = client.post(
+        '/uploads/%s' % upload['upload_id'],
+        headers=test_user_auth,
+        data=json.dumps(dict(operation='unstage')),
+        content_type='application/json')
+    assert rv.status_code == 200
+
+    rv = client.get('/uploads', headers=test_user_auth)
+    assert rv.status_code == 200
+    assert_uploads(rv.data, count=0)
 
 
 def test_repo_calc(client, example_elastic_calc):
diff --git a/tests/test_repo.py b/tests/test_repo.py
index fd3347ee4a9f9c6f7dfb9bfb6e640ad76a61236c..a9058bd1a2ca07e6fe076eed263973028d05b25a 100644
--- a/tests/test_repo.py
+++ b/tests/test_repo.py
@@ -110,3 +110,13 @@ def test_delete_elastic_calc(example_elastic_calc: RepoCalc, caplog):
         assert False
     finally:
         caplog.set_level(logging.WARNING)
+
+
+def test_staging_elastic_calc(example_elastic_calc: RepoCalc):
+    assert RepoCalc.get(id='test_upload_hash/test_calc_hash').staging
+
+
+def test_unstage_elastic_calc(example_elastic_calc: RepoCalc):
+    RepoCalc.update_upload(upload_id='test_upload_id', staging=False)
+
+    assert not RepoCalc.get(id='test_upload_hash/test_calc_hash').staging