diff --git a/eossr/api/apizenodo.py b/eossr/api/apizenodo.py
new file mode 100644
index 0000000000000000000000000000000000000000..7eea1f13bca82099fd5521a3e58e010f45f39a3e
--- /dev/null
+++ b/eossr/api/apizenodo.py
@@ -0,0 +1,403 @@
+#!/usr/bin/env python
+
+import sys
+import json
+import pprint
+import requests
+from os.path import abspath
+from pathlib import Path
+from ..metadata.codemeta2zenodo import parse_codemeta_and_write_zenodo_metadata_file
+
+
+class ZenodoAPI:
+    def __init__(self, access_token, sandbox=True, proj_root_dir='./'):
+        """
+        Manages the communication with the (sandbox.)zenodo REST API through the Python request library.
+        This class is **EXCLUSIVELY** developed to be used within a CI/CD pipeline and to **EXCLUSIVELY PERFORM**
+         the following tasks within the (sandbox.)zenodo api environment:
+
+          - Fetches a user's published entries,
+          - Creates a new deposit,
+          - Fetches any published record,
+          - Creates a new version of an existing deposit,
+          - Uploads files to a specific Zenodo entry,
+          - Erases a non-published entry / new version draft,
+          - Erases (old version) files from an entry (when creating a new_version entry and uploading
+            new_version files),
+          - Uploads information to the entry (Zenodo compulsory deposit information),
+          - Publishes an entry
+          - Find all the published community entries
+            * per title
+            * per entry_id
+
+
+        :param access_token: str
+            Personal access token to (sandbox.)zenodo.org/api
+        :param sandbox: bool
+            Communicates with either zenodo or sandbox.zenodo api
+        """
+
+        if sandbox:
+            zenodo_api_url = "https://sandbox.zenodo.org/api"
+        else:
+            zenodo_api_url = "https://zenodo.org/api"
+
+        self.zenodo_api_url = zenodo_api_url
+        self.access_token = access_token
+        self.parameters = {'access_token': self.access_token}
+
+        self.proj_root_dir = Path(proj_root_dir)
+        self.exist_codemeta_file = False
+        self.path_codemeta_file = self.proj_root_dir
+        self.exist_zenodo_metadata_file = False
+        self.path_zenodo_metadata_file = self.proj_root_dir
+
+    def fetch_user_entries(self):
+        """
+        Fetch the published entries of an user. Works to tests connection to Zenodo too.
+
+        GET method to {zenodo_api_url}/deposit/depositions
+
+        :return: request.get method
+        """
+        url = f"{self.zenodo_api_url}/deposit/depositions"
+
+        return requests.get(url, params=self.parameters)
+
+    def create_new_entry(self):
+        """
+        Create a new entry / deposition in (sandbox.)zenodo
+
+        POST method to {zenodo_api_url}/deposit/depositions
+
+        :return: request.put method
+        """
+        url = f"{self.zenodo_api_url}/deposit/depositions"
+        headers = {"Content-Type": "application/json"}
+
+        return requests.post(url, json={}, headers=headers, params=self.parameters)
+
+    def fetch_entry(self, entry_id):
+        """
+        Fetches (recovers all the existing information, as well as links) of an existing Zenodo entry.
+
+        GET method to {zenodo_api_url}/deposit/depositions/{entry_id}
+
+        :param entry_id: str
+            entry_id of the entry to fetch
+
+        :return: request.get method
+        """
+        # In case of entries created by oneself, or entries in the process of being created, the method to fetch
+        # a record is request.get('api/deposit/deposition/{entry_id}') - see also the upload_file_entry method.
+
+        # To fetch any other entry, already published, use:
+        url = f"{self.zenodo_api_url}/records/{entry_id}"
+        return requests.get(url, params=self.parameters)
+
+    def upload_file_entry(self, entry_id, name_file, path_file):
+        """
+        Upload a file to a Zenodo entry. If first retrieve the entry by a GET method to the
+            {zenodo_api_url}/deposit/depositions/{entry_id}.
+
+        PUT method to {bucket_url}/{filename}. The full api url is recovered when the entry is firstly retrieved.
+
+        :param entry_id: str
+            deposition_id of the Zenodo entry
+        :param name_file: str
+            File name of the file when uploaded
+        :param path_file: str
+            Path to the file to be uploaded
+
+        :return: request.put method
+        """
+        # 1 - Retrieve and recover information of a record that is in process of being published
+        fetch = requests.get(f"{self.zenodo_api_url}/deposit/depositions/{entry_id}",
+                             params=self.parameters)
+
+        # 2 - Upload the files
+        bucket_url = fetch.json()['links']['bucket']  # full url is recovered from previous GET method
+        url = f"{bucket_url}/{name_file}"
+
+        with open(path_file, 'rb') as upload_file:
+            upload = requests.put(url, data=upload_file, params=self.parameters)
+
+        return upload.json()
+
+    def update_metadata_entry(self, entry_id, json_metadata):
+        """
+        Update an entry resource. Data should be the entry information that will be shown when a deposition is visited
+        at the Zenodo site.
+
+        PUT method to {zenodo_api_url}/deposit/depositions/{entry_id}. `data` MUST be included as json.dump(data)
+
+        :param entry_id: str
+            deposition_id of the Zenodo entry
+        :param json_metadata: object
+            json object containing the metadata (compulsory fields) that are enclosed when a new entry is created.
+
+        :return: request.put method
+        """
+        url = f"{self.zenodo_api_url}/deposit/depositions/{entry_id}"
+        headers = {"Content-Type": "application/json"}
+
+        # The metadata field is already created, just need to be updated.
+        # Thus the root 'metadata' key need to be kept, to indicate the field to be updated.
+        data = {"metadata": json_metadata}
+
+        return requests.put(url, data=json.dumps(data),
+                            headers=headers, params=self.parameters)
+
+    def erase_entry(self, entry_id):
+        """
+        Erase an entry/new version of an entry that HAS NOT BEEN published yet.
+        Any new upload/version will be first saved as 'draft' and not published until confirmation (i.e, requests.post)
+
+        DELETE method to {zenodo_api_url}/deposit/depositions/{entry_id}.
+
+        :param entry_id: str
+            deposition_id of the Zenodo entry to be erased
+
+        :return: request.delete method
+        """
+        url = f"{self.zenodo_api_url}/deposit/depositions/{entry_id}"
+        return requests.delete(url, params=self.parameters)
+
+    def erase_file_entry(self, entry_id, file_id):
+        """
+        Erase a file from an entry resource.
+        This method is intended to be used for substitution of files (deletion) within an entry by their correspondent
+         new versions.
+        DELETE method to {zenodo_api_url}/deposit/depositions/{entry_id}/files/{file_id}
+
+        :param entry_id: str
+            deposition_id of the Zenodo entry
+        :param file_id: str
+            Id of the files stored in Zenodo
+
+        :return: requests.delete method
+        """
+        url = f"{self.zenodo_api_url}/deposit/depositions/{entry_id}/files/{file_id}"
+        return requests.delete(url, params=self.parameters)
+
+    def publish_entry(self, entry_id):
+        """
+        Publishes an entry in (sandbox.)zenodo
+        POST method to {zenodo_api_url}/deposit/depositions/{entry_id}/actions/publish
+
+        :param entry_id: str
+            deposition_id of the Zenodo entry
+
+        :return: requests.put method
+        """
+        url = f"{self.zenodo_api_url}/deposit/depositions/{entry_id}/actions/publish"
+        return requests.post(url, params=self.parameters)
+
+    def new_version_entry(self, entry_id):
+        """
+        Creates a new version of AN EXISTING entry resource.
+        POST method to {zenodo_api_url}/deposit/depositions/{entry_id}/actions/newversion
+
+        :param entry_id: str
+            deposition_id of the Zenodo entry
+
+        :return: requests.post method
+        """
+        url = f"{self.zenodo_api_url}/deposit/depositions/{entry_id}/actions/newversion"
+        parameters = {'access_token': self.access_token}
+
+        return requests.post(url, params=parameters)
+
+    def fetch_community_entries(self, community_name='escape2020', results_per_query=100):
+        """
+        Query the entries within a community.
+        GET method, previous modification of the query arguments, to {zenodo_api_url}/records
+
+
+        :param community_name: str
+            Community name. DEFAULT='escape2020'
+        :param results_per_query: int
+            Number of entries returned per call to the REST API. DEFAULT=100.
+
+        :return: requests.get method
+        """
+        # https://developers.zenodo.org/#list36
+        update_query_args = {'communities': str(community_name),
+                             'size': int(results_per_query)
+                             }
+        self.parameters.update(update_query_args)
+
+        # Full answer
+        #   content = requests.post(url, params=self.parameters)
+        # Answer items
+        #   content.json().keys()
+        # Stats
+        #   content.json()['aggregations']
+        # Total num of entries
+        #   content.json()['hits']['total']
+        # Zenodo metadata of each entry
+        #   [item['metadata'] for item in content.json()['hits']['hits']]
+
+        return requests.get(f"{self.zenodo_api_url}/records", params=self.parameters)
+
+    def fetch_community_entries_per_id(self, community_name='escape2020', results_per_query=100):
+        """
+        Query the `entries ids` of all the entries within a community
+
+        :param community_name: str
+            Community name. DEFAULT='escape2020'
+        :param results_per_query: int
+            Number of entries returned per call to the REST API. DEFAULT=100.
+
+        :return: list
+            List containing the `id`s of each community entry
+        """
+        return [entry['id'] for entry in
+                self.fetch_community_entries(community_name, results_per_query).json()['hits']['hits']]
+
+    def fetch_community_entries_per_title(self, community_name='escape2020', results_per_query=100):
+        """
+        Query the title of all the entries within a community
+
+        :param community_name: str
+            Community name. DEFAULT='escape2020'
+        :param results_per_query: int
+            Number of entries returned per call to the REST API. DEFAULT=100.
+
+        :return: list
+            List containing the title of each community entry
+        """
+        return [entry['metadata']['title'] for entry in
+                self.fetch_community_entries(community_name, results_per_query).json()['hits']['hits']]
+
+    def search_codemeta_file(self):
+        """Check if a `codemeta.json` files exists in the ROOT directory of the project"""
+
+        # root_dir = find_root_directory()
+        root_dir = self.proj_root_dir
+        print(f'\nProject root directory {abspath(root_dir)}')  # DEBUG
+
+        codemeta_file = root_dir / 'codemeta.json'
+
+        if codemeta_file.exists():
+            print("\n * Found codemeta.json file within the project !")
+            self.exist_codemeta_file = True
+            self.path_codemeta_file = codemeta_file
+        else:
+            print("\n ! codemeta.json file NOT found in the root directory of the  project !")
+
+    def search_zenodo_json_file(self):
+        """Check if a `.zenodo.json` files exists in the ROOT directory of the project"""
+
+        # root_dir = find_root_directory()
+        root_dir = self.proj_root_dir
+
+        zenodo_metadata_file = root_dir / '.zenodo.json'
+
+        if zenodo_metadata_file.exists():
+            print("\n * Found .zenodo.json file within the project !")
+            self.exist_zenodo_metadata_file = True
+            self.path_zenodo_metadata_file = zenodo_metadata_file
+        else:
+            print("\n ! .zenodo.json file NOT found in the root directory of the  project !")
+
+    def conversion_codemeta2zenodo(self):
+        """Perform the codemeta2zenodo conversion if a codemeta.json file is found"""
+
+        if self.exist_codemeta_file:
+            print("\n * Creating a .zenodo.json file from your codemeta.json file...")
+            self.path_zenodo_metadata_file = self.path_codemeta_file.parent / '.zenodo.json'
+
+            parse_codemeta_and_write_zenodo_metadata_file(self.path_codemeta_file,
+                                                          self.path_zenodo_metadata_file)
+        else:
+            pass
+
+    def test_upload_to_zenodo(self):
+        """
+        `Tests` the different stages of the GitLab-Zenodo connection and that the status_code returned by every
+        stage is the correct one.
+
+        Checks:
+         - The existence of a `.zenodo.json` file in the ROOT dir of the project
+            - If not, it checks if it exists a `codemeta.json` file
+               - If it exists it performs the codemeta2zenodo conversion
+               - If not, it exits the program
+
+         - The communication with Zenodo through its API to verify that:
+            - You can fetch an user entries
+            - You can create a new entry
+            - The provided zenodo metadata can be digested, and not errors appear
+            - Finally erases the test entry - because IT HAS NOT BEEN PUBLISHED !
+        """
+        # Search for the codemeta.json and the .zenodo.json files within the project
+        self.search_codemeta_file()
+        self.search_zenodo_json_file()
+
+        if not self.exist_zenodo_metadata_file:
+
+            if self.exist_codemeta_file:
+                self.conversion_codemeta2zenodo()
+            else:
+                print("\n ! NO codemeta.json NOR .zenodo.json file found. "
+                      "Please add one to the ROOT directory of your project.")
+                sys.exit(-1)
+
+        print("\n * Using the .zenodo.json file to simulate a new upload to Zenodo... \n")
+
+        # 1 - Test connection
+        print("1 --> Testing communication with Zenodo...")
+
+        test_connection = self.fetch_user_entries()
+        if test_connection.status_code == 200:
+            print("  * Test connection status OK !")
+        else:
+            print("  ! ERROR while testing connection status\n", test_connection.json())
+
+        # 2 - Test new entry
+        print("2 --> Testing the creation of a dummy entry to (sandbox)Zenodo...")
+
+        new_entry = self.create_new_entry()
+        if new_entry.status_code == 201:
+            print("  * Test new entry status OK !")
+        else:
+            print("  ! ERROR while testing the creation of new entry\n", new_entry.json())
+
+        # 3 - Test upload metadata
+        print("3 --> Testing the ingestion of the Zenodo metadata...")
+
+        test_entry_id = new_entry.json()['id']
+        with open(self.path_zenodo_metadata_file) as file:
+            metadata_entry = json.load(file)
+        update_metadata = self.update_metadata_entry(test_entry_id,
+                                                     json_metadata=metadata_entry)
+
+        if update_metadata.status_code == 200:
+            print("  * Update metadata status OK !")
+            pprint.pprint(metadata_entry)
+        else:
+            print("  ! ERROR while testing update of metadata\n",
+                  update_metadata.json(), "\n", metadata_entry)
+            print("  ! Erasing dummy test entry...\n")
+            erase_error = self.erase_entry(test_entry_id)
+            if erase_error.status_code != 204:
+                print(f" !! ERROR erasing dummy test entry. Please erase it manually at\n "
+                      f"{self.zenodo_api_url[:-4]}/deposit")
+            else:
+                print("    - Done.\n")
+            sys.exit(-1)
+
+        # 4 - Test delete entry
+        print("4 --> Testing the deletion of the dummy entry...")
+
+        delete_test_entry = self.erase_entry(test_entry_id)
+        if delete_test_entry.status_code == 204:
+            print("  * Delete test entry status OK !")
+        else:
+            print("  ! ERROR while deleting test entry\n", delete_test_entry.json())
+
+        print("\n\tYAY ! Successful testing of the connection to Zenodo ! \n\n"
+              "You should not face any trouble when uploading a project to Zenodo - if you followed the "
+              "`OSSR how to publish tutorial`:\n"
+              "\t https://escape2020.pages.in2p3.fr/wp3/ossr-pages/page/contribute/publish_tutorial/#3-add-the-following-code-snippet \n"
+              "In case you do, please contact us !\n")
diff --git a/eossr/api/tests/__init__.py b/eossr/api/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/eossr/api/tests/test_apizenodo.py b/eossr/api/tests/test_apizenodo.py
new file mode 100644
index 0000000000000000000000000000000000000000..1f7712fbb155a7273eba21e224446cbb8889892e
--- /dev/null
+++ b/eossr/api/tests/test_apizenodo.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python
+
+import json
+import unittest
+import requests
+from pathlib import Path
+from os import remove
+from os.path import dirname, realpath, join
+from ..apizenodo import ZenodoAPI
+
+ROOT_DIR = dirname(realpath("codemeta.json"))
+
+
+class TestZenodoApiSandbox(unittest.TestCase):
+    def test_initialization_sandbox(self):
+        token = 'FakeToken'
+        z = ZenodoAPI(access_token=token,
+                      sandbox=True,
+                      proj_root_dir=ROOT_DIR)
+
+        assert isinstance(z, ZenodoAPI)
+        assert z.zenodo_api_url == 'https://sandbox.zenodo.org/api'
+        assert z.access_token == token
+        assert type(z.exist_codemeta_file) == bool
+        assert type(z.exist_zenodo_metadata_file) == bool
+        assert z.proj_root_dir == Path(ROOT_DIR)
+        assert z.path_codemeta_file == z.proj_root_dir
+        assert z.path_zenodo_metadata_file == z.proj_root_dir
+        assert isinstance(z.proj_root_dir, (Path, str))
+
+
+class TestZenodoAPI(unittest.TestCase):
+    def test_initialization(self):
+        token = 'FakeToken'
+        z = ZenodoAPI(access_token=token,
+                      sandbox=False,
+                      proj_root_dir=ROOT_DIR)
+
+        assert isinstance(z, ZenodoAPI)
+        assert z.zenodo_api_url == 'https://zenodo.org/api'
+        assert z.access_token == token
+        assert type(z.exist_codemeta_file) == bool
+        assert type(z.exist_zenodo_metadata_file) == bool
+        assert z.proj_root_dir == Path(ROOT_DIR)
+        assert z.path_codemeta_file == z.proj_root_dir
+        assert z.path_zenodo_metadata_file == z.proj_root_dir
+        assert isinstance(z.proj_root_dir, (Path, str))
+
+    def test_zenodo_api_methods(self):
+        token = 'FakeToken'
+        z = ZenodoAPI(access_token=token,
+                      sandbox=False,
+                      proj_root_dir=ROOT_DIR)
+
+        test_id = '42'
+        z.search_codemeta_file()
+        test_filename = join(ROOT_DIR, z.path_codemeta_file)
+        path_test_filename = './'
+
+        fetch_user_entry = z.fetch_user_entries()
+        create_new_entry = z.create_new_entry()
+        fetch_single_entry = z.fetch_entry(
+            entry_id=test_id
+        )
+        # upload_file_entry = z.upload_file_entry(
+        #     entry_id=test_id,
+        #     name_file=test_filename,
+        #     path_file=path_test_filename
+        # )
+        upload_metadata_entry = z.update_metadata_entry(
+            entry_id=test_id,
+            json_metadata=test_filename
+        )
+        erase_entry = z.erase_entry(
+            entry_id=test_id
+        )
+        erase_file_entry = z.erase_file_entry(
+            entry_id=test_id,
+            file_id=test_id
+        )
+        publish_entry = z.publish_entry(
+            entry_id=test_id
+        )
+        new_version_entry = z.new_version_entry(
+            entry_id=test_id
+        )
+        community_entries = z.fetch_community_entries()
+        fetch_ids = z.fetch_community_entries_per_id()
+        fetch_filenames = z.fetch_community_entries_per_title()
+
+        assert isinstance(fetch_user_entry, requests.models.Response)
+        assert isinstance(create_new_entry, requests.models.Response)
+        assert isinstance(fetch_single_entry, requests.models.Response)
+        # assert isinstance(upload_file_entry, requests.models.Response)
+        assert isinstance(upload_metadata_entry, requests.models.Response)
+        assert isinstance(erase_entry, requests.models.Response)
+        assert isinstance(erase_file_entry, requests.models.Response)
+        assert isinstance(publish_entry, requests.models.Response)
+        assert isinstance(new_version_entry, requests.models.Response)
+        assert isinstance(community_entries, requests.models.Response)
+        assert isinstance(fetch_ids, list)
+        assert isinstance(fetch_filenames, list)
+
+    def test_search_codemeta_file(self):
+        token = 'FakeToken'
+        z = ZenodoAPI(access_token=token,
+                      sandbox=False,
+                      proj_root_dir=ROOT_DIR)
+
+        assert z.exist_codemeta_file is False
+        z.search_codemeta_file()
+        assert z.exist_codemeta_file is True
+
+        codemeta_file_path = Path(ROOT_DIR) / 'codemeta.json'
+        assert z.path_codemeta_file == codemeta_file_path
+        assert codemeta_file_path.is_file()
+        print(z.path_codemeta_file, type(z.path_codemeta_file))
+        with open(z.path_codemeta_file) as f:
+            json.load(f)
+
+    def test_search_zenodo_json_file(self):
+        token = 'FakeToken'
+        z = ZenodoAPI(access_token=token,
+                      sandbox=False,
+                      proj_root_dir=ROOT_DIR)
+
+        assert z.exist_zenodo_metadata_file is False
+        z.search_zenodo_json_file()
+        assert z.exist_zenodo_metadata_file is False
+
+    def test_conversion_codemeta2zenodo_and_search_zenodo_file(self):
+        token = 'FakeToken'
+        z = ZenodoAPI(access_token=token,
+                      sandbox=False,
+                      proj_root_dir=ROOT_DIR)
+
+        z.search_codemeta_file()
+        z.conversion_codemeta2zenodo()
+
+        z.search_zenodo_json_file()
+        assert z.exist_zenodo_metadata_file is True
+
+        zenodo_file_path = Path(ROOT_DIR) / '.zenodo.json'
+        assert z.path_zenodo_metadata_file == zenodo_file_path
+        assert zenodo_file_path.is_file()
+        with open(z.path_zenodo_metadata_file) as f:
+            json.load(f)
+
+        remove(z.path_zenodo_metadata_file)
diff --git a/setup.py b/setup.py
index 5a3b1bcce2e80aa72dd9d3bac15d70587c993ed5..1a5f5f5cdc94cdb32dff4c42e07199ef74057fc3 100644
--- a/setup.py
+++ b/setup.py
@@ -19,10 +19,12 @@ setup(
     name='eossr',
     version=get_property('__version__', 'eossr'),
     description="ESCAPE OSSR library",
-    # install_requires=[],
+    install_requires=[
+        "requests"
+    ],
     packages=find_packages(),
     # scripts=[],
-    # tests_require=['pytest'],
+    tests_require=['pytest'],
     author='Thomas Vuillaume & Enrique Garcia',
     author_email='vuillaume<at>lapp.in2p3.fr',
     url='https://gitlab.in2p3.fr/escape2020/wp3/eossr',