diff --git a/.gitignore b/.gitignore
index abf174d05b187a9c365e34e176ef2cc7a03c8913..fb36086fbf534507b984754dbcff14f4947b22c4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,45 +1,13 @@
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-build/
-dist/
-*.egg-info/
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Virtual environments
-venv/
-.env
-.venv/
-
-# PyInstaller
-*.manifest
-*.spec
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
-.coverage
+.tox
+bom.json
 coverage.xml
-.cache
-.pytest_cache/
-
-# Sphinx documentation
-docs/_build/
-
-# Editor / OS junk
+.coverage
+htmlcov
+*.egg-info
+__pycache__
+.vscode
+docs/build
 .DS_Store
-Thumbs.db
-.idea/
-.vscode/
-*.swp
-*.swo
\ No newline at end of file
+
+# drawio tmp files
+*.bkp
diff --git a/docs/source/conf.py b/docs/source/conf.py
index d52fe2040888b5fcd07aa1b3c59ca5e1e0f15b6b..8a6ca804f98730651c57120454e1b8622ad2381a 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -7,6 +7,7 @@ from datetime import datetime
 import json
 import os
 import sys
+import tempfile
 
 sys.path.insert(
     0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
@@ -28,7 +29,7 @@ release = '0.0'
 
 extensions = [
     "sphinx_rtd_theme",
-    # "sphinx.ext.autodoc",
+    "sphinx.ext.autodoc",
     "sphinx.ext.coverage",
     "sphinxcontrib.plantuml",
     "sphinxcontrib.openapi",
@@ -53,7 +54,19 @@ html_theme = 'sphinx_rtd_theme'
 html_static_path = ['_static']
 
 
-api_schema = create_app().openapi()
+# we need a minimal/parseable config file in order to
+# start the server and dump the schema
+with tempfile.NamedTemporaryFile(delete=False) as f:
+
+    bogus_config = {'inventory': 'http://bogus'}
+    with open(f.name, 'w') as f:
+        json.dump(bogus_config, f)
+        f.flush()
+        os.environ['SETTINGS_FILENAME'] = f.name
+
+    api_schema = create_app().openapi()
+
 openapi_filename = os.path.join(os.path.dirname(__file__), "openapi.json")
 with open(openapi_filename, 'w') as f:
     json.dump(api_schema, f, indent=4)
+
diff --git a/docs/source/mapping_provider.rst b/docs/source/mapping_provider.rst
deleted file mode 100644
index dad01fb843acad3b5ee1364bea300af8dc375ecd..0000000000000000000000000000000000000000
--- a/docs/source/mapping_provider.rst
+++ /dev/null
@@ -1,22 +0,0 @@
-mapping_provider package
-=========================
-
-Submodules
-----------
-
-mapping\_provider.main module
------------------------------
-
-.. automodule:: mapping_provider.main
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: mapping_provider
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
diff --git a/docs/source/modules.rst b/docs/source/modules.rst
deleted file mode 100644
index 823c103fbb5633a3b8f2ac6452ff7ab5c1c8d67d..0000000000000000000000000000000000000000
--- a/docs/source/modules.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-mapping_provider
-================
-
-.. toctree::
-   :maxdepth: 4
-
-   mapping_provider
-
diff --git a/mapping_provider/__init__.py b/mapping_provider/__init__.py
index aa309f9cfa78136a8ef621a23d83e13d37b4dffa..1000f2af4fa33a441d0a75abe1a8ccbc64525f64 100644
--- a/mapping_provider/__init__.py
+++ b/mapping_provider/__init__.py
@@ -1,15 +1,27 @@
-"""Initializes the FastAPI application."""
+"""
+Default entry point for the FastAPI application.
+"""
 
 from fastapi import FastAPI
 
-from mapping_provider.api.common import router as version_router
+from mapping_provider import config, environment
+from mapping_provider.api import common, map
 
 
 def create_app() -> FastAPI:
-    """Create a FastAPI application."""
+    """
+    Creates the FastAPI application instance, with routers attached.
+    """
+    environment.setup_logging()
+
+    app_config = config.load()
+    if app_config.sentry:
+        environment.setup_sentry(app_config.sentry)
+
     app = FastAPI(
         title="Mapping provider",
         description="Mapping provider endpoints for GÉANT maps",
     )
-    app.include_router(version_router)
+    app.include_router(common.router)
+    app.include_router(map.router, prefix='/map')
     return app
diff --git a/mapping_provider/api/common.py b/mapping_provider/api/common.py
index 9c528b0e71b78ccb6f8a8f607359a2013ff60c50..3712b8e96d5cd7a76724cbd7bdbd0f5c0515ef37 100644
--- a/mapping_provider/api/common.py
+++ b/mapping_provider/api/common.py
@@ -1,14 +1,22 @@
-from importlib.metadata import PackageNotFoundError, version
+from importlib.metadata import version
 
 from fastapi import APIRouter
+from pydantic import BaseModel
 
 router = APIRouter()
 
 
+class Version(BaseModel):
+    module: str
+    api: str
+
+
 @router.get("/version")
-def get_version() -> dict[str, str]:
-    """Get the version of the package."""
-    try:
-        return {"version": version("mapping_provider")}
-    except PackageNotFoundError:
-        return {"version": "unknown"}
+def get_version() -> Version:
+    """
+    handler for /version
+    """
+    return Version(
+        module = version('mapping_provider'),
+        api = '0.1'
+    )
diff --git a/mapping_provider/api/map.py b/mapping_provider/api/map.py
new file mode 100644
index 0000000000000000000000000000000000000000..a2b8461b13323a022fd89729faf36cc10a5daf7e
--- /dev/null
+++ b/mapping_provider/api/map.py
@@ -0,0 +1,218 @@
+from typing import Any
+
+import jsonschema
+import requests
+from fastapi import APIRouter
+from pydantic import BaseModel
+
+from mapping_provider import config
+
+router = APIRouter()
+
+
+class Site(BaseModel):
+    latitude: float
+    longitude: float
+    name: str
+
+    @classmethod
+    def from_inprov_site(cls, site: dict[str, Any]) -> 'Site':
+        return cls(
+            latitude=site['latitude'],
+            longitude=site['longitude'],
+            name=site['name']
+        )
+
+class SiteList(BaseModel):
+    sites: list[Site]
+
+class Router(BaseModel):
+    fqdn: str
+    site: str
+
+    @classmethod
+    def from_inprov_router(cls, router: dict[str, Any]) -> 'Router':
+        return cls(
+            fqdn = router['fqdn'],
+            site = router['site']
+        )
+
+class RouterList(BaseModel):
+    routers: list[Router]
+
+
+class Endpoint(BaseModel):
+    hostname: str
+    interface: str
+
+    @classmethod
+    def from_inprov_endpoint(cls, endpoint: dict[str, Any]) -> 'Endpoint':
+        return cls(
+            hostname = endpoint['hostname'],
+            interface = endpoint['interface'],
+        )
+
+class Overlays(BaseModel):
+    speed: int
+
+    @classmethod
+    def from_inprov_overlays(cls, overlays: dict[str, Any]) -> 'Overlays':
+        return cls(
+            speed = overlays['speed'],
+        )
+
+class Service(BaseModel):
+    sid: str
+    name: str
+    type: str
+    endpoints: list[Endpoint]
+    overlays: Overlays
+
+    @classmethod
+    def from_inprov_service(cls, service: dict[str, Any]) -> 'Service':
+        return cls(
+            sid = service['sid'],
+            name = service['name'],
+            type = service['type'],
+            endpoints = list(map(Endpoint.from_inprov_endpoint, service['endpoints'])),
+            overlays = Overlays.from_inprov_overlays(service['overlays']),
+        )
+
+class ServiceList(BaseModel):
+    services: list[Service]
+
+
+INPROV_SITE_LIST_SCHEMA = {
+    '$schema': 'https://json-schema.org/draft/2020-12/schema',
+
+    'definitions': {
+        'site': {
+            'type': 'object',
+            'properties': {
+                'name': {'type': 'string'},
+                'latitude': {'type': 'number'},
+                'longitude': {'type': 'number'},
+            },
+            'required': ['name', 'latitude', 'longitude'],
+            'additionalProperties': True,
+        },
+    },
+
+    'type': 'array',
+    'items': {'$ref': '#/definitions/site'}
+}
+
+INPROV_ROUTER_LIST_SCHEMA = {
+    '$schema': 'https://json-schema.org/draft/2020-12/schema',
+
+    'definitions': {
+        'router': {
+            'type': 'object',
+            'properties': {
+                'fqdn': {'type': 'string'},
+                'site': {'type': 'string'},
+            },
+            'required': ['fqdn', 'site'],
+        },
+    },
+
+    'type': 'array',
+    'items': {'$ref': '#/definitions/router'}
+}
+
+INPROV_SERVICE_LIST_SCHEMA = {
+    '$schema': 'https://json-schema.org/draft/2020-12/schema',
+
+    'definitions': {
+        'endpoint': {
+            'type': 'object',
+            'properties': {
+                'hostname': {'type': 'string'},
+                'interface': {'type': 'string'},
+            },
+        },
+        'service': {
+            'type': 'object',
+            'properties': {
+                'sid': {'type': 'string'},
+                'name': {'type': 'string'},
+                'type': {'type': 'string'},
+                'endpoints': {
+                    'type': 'array',
+                    'items': {'$ref': '#/definitions/endpoint'},
+                    'minItems': 1,
+                },
+                'overlays': {
+                    'type': 'object', 'properties': {
+                        'speed': {'type': 'number'},
+                    },
+                    'required': ['speed'],
+                },
+            },
+            'required': ['sid', 'name', 'type', 'endpoints', 'overlays'],
+        },
+    },
+
+    'type': 'array',
+    'items': {'$ref': '#/definitions/service'}
+}
+
+INPROV_API_URL_TODO = 'https://test-inprov01.geant.org'
+@router.get("/sites")
+def get_sites() -> SiteList:
+    """
+    handler for /sites
+    """
+    # TODO: catch/handle the usual exceptions
+
+    app_params = config.load()
+    rv = requests.get(
+        f'{app_params.inventory}/map/sites',
+        headers={'Accept': 'application/json'})
+    rv.raise_for_status()
+    site_list_json = rv.json()
+    jsonschema.validate(site_list_json, INPROV_SITE_LIST_SCHEMA)
+
+    rsp_sites = map(Site.from_inprov_site, site_list_json)
+    return SiteList(sites=list(rsp_sites))
+
+
+@router.get("/routers")
+def get_routers() -> RouterList:
+    """
+    handler for /sites
+    """
+
+    # TODO: catch/handle the usual exceptions
+
+    app_params = config.load()
+    rv = requests.get(
+        f'{app_params.inventory}/map/routers',
+        headers={'Accept': 'application/json'})
+    rv.raise_for_status()
+    router_list_json = rv.json()
+    jsonschema.validate(router_list_json, INPROV_ROUTER_LIST_SCHEMA)
+
+    rsp_routers = map(Router.from_inprov_router, router_list_json)
+    return RouterList(routers=list(rsp_routers))
+
+
+@router.get("/trunks")
+def get_trunks() -> ServiceList:
+    """
+    handler for /trunks
+    """
+
+    # TODO: catch/handle the usual exceptions
+
+    app_params = config.load()
+    rv = requests.get(
+        f'{app_params.inventory}/map/services/IP TRUNK',
+        headers={'Accept': 'application/json'})
+    rv.raise_for_status()
+    service_list_json = rv.json()
+    jsonschema.validate(service_list_json, INPROV_SERVICE_LIST_SCHEMA)
+
+    rsp_services = map(Service.from_inprov_service, service_list_json)
+    return ServiceList(services=list(rsp_services))
+
diff --git a/mapping_provider/config.py b/mapping_provider/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..cf67282db6fa7984c799f83a9dd85b9383838dad
--- /dev/null
+++ b/mapping_provider/config.py
@@ -0,0 +1,34 @@
+import os
+
+from pydantic import BaseModel, Field, HttpUrl
+
+
+class SentryConfig(BaseModel):
+    dsn: str
+    environment: str
+    level: str = Field(
+        default='error',
+        pattern="^(debug|info|warning|error)$")
+
+    # @field_validator('level')
+    # @classmethod
+    # def validate_level(cls, v):
+    #     if v not in ['debug', 'info', 'warning', 'error']:
+    #         raise ValueError('level must be one of: debug, info, warning, error')
+    #     return v
+
+class Configuration(BaseModel):
+    sentry: SentryConfig | None = None
+    inventory: HttpUrl
+
+
+def load() -> Configuration:
+    """
+    Loads, validates and returns configuration parameters from
+    the file named in the environment variable 'SETTINGS_FILENAME'.
+
+    :return:
+    """
+    assert 'SETTINGS_FILENAME' in os.environ
+    with open(os.environ['SETTINGS_FILENAME']) as f:
+        return Configuration.model_validate_json(f.read())
diff --git a/mapping_provider/environment.py b/mapping_provider/environment.py
new file mode 100644
index 0000000000000000000000000000000000000000..2319555abc09ab5ca98794c7684a3514d3b797b4
--- /dev/null
+++ b/mapping_provider/environment.py
@@ -0,0 +1,96 @@
+"""
+Environment setup
+===================
+
+.. autofunction:: mapping_provider.environment.setup_logging
+
+.. autofunction:: mapping_provider.environment.setup_sentry
+
+"""
+import json
+import logging
+import logging.config
+import os
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from mapping_provider.config import SentryConfig
+
+import sentry_sdk
+from sentry_sdk.integrations.logging import LoggingIntegration
+
+LOGGING_DEFAULT_CONFIG = {
+    'version': 1,
+    'disable_existing_loggers': False,
+    'formatters': {
+        'simple': {
+            'format': '%(asctime)s - %(name)s '
+                      '(%(lineno)d) - %(levelname)s - %(message)s'
+        }
+    },
+
+    'handlers': {
+        'console': {
+            'class': 'logging.StreamHandler',
+            'level': 'DEBUG',
+            'formatter': 'simple',
+            'stream': 'ext://sys.stdout'
+        }
+
+    },
+
+    'loggers': {
+        'mapping_provider': {
+            'level': 'DEBUG',
+            'handlers': ['console'],
+            'propagate': False
+        }
+    },
+
+    'root': {
+        'level': 'WARNING',
+        'handlers': ['console']
+    }
+}
+
+
+def setup_logging() -> None:
+    """
+    Sets up logging using the configured filename.
+
+    if LOGGING_CONFIG is defined in the environment, use this for
+    the filename, otherwise use LOGGING_DEFAULT_CONFIG
+    """
+    logging_config = LOGGING_DEFAULT_CONFIG
+    if 'LOGGING_CONFIG' in os.environ:
+        filename = os.environ['LOGGING_CONFIG']
+        with open(filename) as f:
+            logging_config = json.load(f)
+
+    logging.config.dictConfig(logging_config)
+
+
+def setup_sentry(sentry_config_params: 'SentryConfig') -> None:
+    """
+    Sets up the sentry instrumentation based on the Configuration.sentry params.
+    """
+    match str(sentry_config_params.level):
+        case 'debug':
+            level = logging.DEBUG
+        case 'info':
+            level = logging.INFO
+        case 'warning':
+            level = logging.WARNING
+        case 'error':
+            level = logging.ERROR
+
+    sentry_sdk.init(
+        dsn=sentry_config_params.dsn,
+        environment=sentry_config_params.environment,
+        integrations=[
+            LoggingIntegration(
+                level=logging.INFO,  # Capture info and above as breadcrumbs
+                event_level=level  # Send records as events
+            ),
+        ],
+    )
diff --git a/pyproject.toml b/pyproject.toml
index cdeac565c37fdc9a4f47530c7a09f6413028ef66..5962c7c74f4fddd876eb57b2d80c771c0b39a4d2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,12 +1,12 @@
 [tool.ruff]
 line-length = 120
 target-version = "py313"
-select = ["E", "F", "I", "B", "UP", "N"]
-fixable = ["ALL"]
+lint.select = ["E", "F", "I", "B", "UP", "N"]
+lint.fixable = ["ALL"]
 exclude = ["tests", "docs", "build"]
 
 [tool.mypy]
 python_version = "3.13"
 strict = true
 warn_unused_ignores = true
-warn_return_any = true
\ No newline at end of file
+warn_return_any = true
diff --git a/requirements.txt b/requirements.txt
index 8cedf04a6b774601a712430bfb7494d302f9fb80..0c99b1de0cb6985c5f38d93aa260d46242f3c856 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,13 +1,13 @@
 fastapi
 uvicorn[standard]
+requests
+jsonschema
+sentry_sdk
 
-sphinx
-sphinx-rtd-theme
-sphinxcontrib-plantuml
-sphinxcontrib-drawio
-sphinxcontrib-openapi
+httpx  # required for fastapi TestClient
+pytest
+responses
 
 ruff
 mypy
-tox
 pre-commit
diff --git a/setup.py b/setup.py
index 5e5209c7d2b398fa6ee6b7e28f954dc397cc8133..b0fd259282caceba9920789bbe3e94fc7259e70c 100644
--- a/setup.py
+++ b/setup.py
@@ -13,7 +13,10 @@ setup(
     python_requires=">=3.10",
     install_requires=[
         "fastapi",
-        "uvicorn[standard]"
+        "uvicorn[standard]",
+        "requests",
+        "jsonschema",
+        "sentry_sdk",        
     ],
     long_description=open("README.md", encoding="utf-8").read(),
     long_description_content_type="text/markdown",
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/test/conftest.py b/test/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..4e7c4f211775a3d0ff490a372c3df867dec0fc8d
--- /dev/null
+++ b/test/conftest.py
@@ -0,0 +1,35 @@
+import json
+import os
+import tempfile
+from unittest.mock import patch
+
+import pytest
+from fastapi.testclient import TestClient
+
+from mapping_provider import create_app
+
+
+@pytest.fixture
+def dummy_config():
+    return {
+        'sentry': {
+            'dsn': 'https://token@hostname.geant.org:1111/a/b',
+            'environment': 'unit tests'
+        },
+        'inventory': 'https://dummy-hostname.dummy.domain'
+    }
+
+
+@pytest.fixture
+def dummy_config_filename(dummy_config):
+    with tempfile.NamedTemporaryFile(delete=False) as f:
+        f.write(json.dumps(dummy_config).encode('utf-8'))
+        f.flush()
+        yield f.name
+
+
+@pytest.fixture
+def client(dummy_config_filename):
+    os.environ['SETTINGS_FILENAME'] = dummy_config_filename
+    with patch('sentry_sdk.init') as _mock_sentry_init:
+        return TestClient(create_app())
diff --git a/test/data/inprov-routers.json b/test/data/inprov-routers.json
new file mode 100644
index 0000000000000000000000000000000000000000..a40dfe2c20a7f25005d3099e1ed92c664e6f7544
--- /dev/null
+++ b/test/data/inprov-routers.json
@@ -0,0 +1,246 @@
+[
+  {
+    "fqdn": "qfx.par.fr.geant.net",
+    "site": "PAR"
+  },
+  {
+    "fqdn": "mx2.lis.pt.geant.net",
+    "site": "LIS"
+  },
+  {
+    "fqdn": "mx1.vie.at.geant.net",
+    "site": "VIE"
+  },
+  {
+    "fqdn": "qfx.fra.de.geant.net",
+    "site": "FRA"
+  },
+  {
+    "fqdn": "qfx.lon2.uk.geant.net",
+    "site": "LON2"
+  },
+  {
+    "fqdn": "mx1.poz.pl.geant.net",
+    "site": "POZ"
+  },
+  {
+    "fqdn": "rt1.bil.es.geant.net",
+    "site": "BIL"
+  },
+  {
+    "fqdn": "rt1.chi.md.geant.net",
+    "site": "CHI"
+  },
+  {
+    "fqdn": "rt0.ath2.gr.geant.net",
+    "site": "ATH2"
+  },
+  {
+    "fqdn": "rt1.ams.nl.geant.net",
+    "site": "AMS"
+  },
+  {
+    "fqdn": "rt1.buc.ro.geant.net",
+    "site": "BUC"
+  },
+  {
+    "fqdn": "rt1.bra.sk.geant.net",
+    "site": "BRA"
+  },
+  {
+    "fqdn": "rt0.lon.uk.geant.net",
+    "site": "LON"
+  },
+  {
+    "fqdn": "mx2.zag.hr.geant.net",
+    "site": "ZAG"
+  },
+  {
+    "fqdn": "srx1.am.office.geant.net",
+    "site": "AMO"
+  },
+  {
+    "fqdn": "mx1.bud.hu.geant.net",
+    "site": "BUD"
+  },
+  {
+    "fqdn": "mx1.buc.ro.geant.net",
+    "site": "BUC"
+  },
+  {
+    "fqdn": "rt0.mil2.it.geant.net",
+    "site": "MIL2"
+  },
+  {
+    "fqdn": "rt0.mar.fr.geant.net",
+    "site": "MAR"
+  },
+  {
+    "fqdn": "mx1.lon2.uk.geant.net",
+    "site": "LON2"
+  },
+  {
+    "fqdn": "mx1.gen.ch.geant.net",
+    "site": "GEN"
+  },
+  {
+    "fqdn": "mx1.mad.es.geant.net",
+    "site": "MAD"
+  },
+  {
+    "fqdn": "mx1.par.fr.geant.net",
+    "site": "PAR"
+  },
+  {
+    "fqdn": "mx1.lon.uk.geant.net",
+    "site": "LON"
+  },
+  {
+    "fqdn": "srx1.ch.office.geant.net",
+    "site": "CCH"
+  },
+  {
+    "fqdn": "srx2.am.office.geant.net",
+    "site": "AMO"
+  },
+  {
+    "fqdn": "srx2.ch.office.geant.net",
+    "site": "CCH"
+  },
+  {
+    "fqdn": "rt1.kie.ua.geant.net",
+    "site": "KIE"
+  },
+  {
+    "fqdn": "rt1.fra.de.geant.net",
+    "site": "FRA"
+  },
+  {
+    "fqdn": "rt1.mar.fr.geant.net",
+    "site": "MAR"
+  },
+  {
+    "fqdn": "rt1.mil2.it.geant.net",
+    "site": "MIL2"
+  },
+  {
+    "fqdn": "rt1.por.pt.geant.net",
+    "site": "POR"
+  },
+  {
+    "fqdn": "rt1.pra.cz.geant.net",
+    "site": "PRA"
+  },
+  {
+    "fqdn": "rt0.poz.pl.geant.net",
+    "site": "POZ"
+  },
+  {
+    "fqdn": "rt2.ams.nl.geant.net",
+    "site": "AMS"
+  },
+  {
+    "fqdn": "rt0.gen.ch.geant.net",
+    "site": "GEN"
+  },
+  {
+    "fqdn": "rt2.kie.ua.geant.net",
+    "site": "KIE"
+  },
+  {
+    "fqdn": "rt2.chi.md.geant.net",
+    "site": "CHI"
+  },
+  {
+    "fqdn": "rt0.zag.hr.geant.net",
+    "site": "ZAG"
+  },
+  {
+    "fqdn": "rt2.bra.sk.geant.net",
+    "site": "BRA"
+  },
+  {
+    "fqdn": "rt0.rig.lv.geant.net",
+    "site": "RIG"
+  },
+  {
+    "fqdn": "rt0.mad.es.geant.net",
+    "site": "MAD"
+  },
+  {
+    "fqdn": "rt0.kau.lt.geant.net",
+    "site": "KAU"
+  },
+  {
+    "fqdn": "rt0.the.gr.geant.net",
+    "site": "THE"
+  },
+  {
+    "fqdn": "rt0.buc.ro.geant.net",
+    "site": "BUC"
+  },
+  {
+    "fqdn": "rt0.tar.ee.geant.net",
+    "site": "TAR"
+  },
+  {
+    "fqdn": "rt0.par.fr.geant.net",
+    "site": "PAR"
+  },
+  {
+    "fqdn": "rt0.bud.hu.geant.net",
+    "site": "BUD"
+  },
+  {
+    "fqdn": "rt0.bra.sk.geant.net",
+    "site": "BRA"
+  },
+  {
+    "fqdn": "rt0.pra.cz.geant.net",
+    "site": "PRA"
+  },
+  {
+    "fqdn": "rt0.lon2.uk.geant.net",
+    "site": "LON2"
+  },
+  {
+    "fqdn": "rt0.por.pt.geant.net",
+    "site": "POR"
+  },
+  {
+    "fqdn": "rt0.vie.at.geant.net",
+    "site": "VIE"
+  },
+  {
+    "fqdn": "rt0.cor.ie.geant.net",
+    "site": "COR"
+  },
+  {
+    "fqdn": "rt0.dub.ie.geant.net",
+    "site": "DUB"
+  },
+  {
+    "fqdn": "rt0.bru.be.geant.net",
+    "site": "BRU"
+  },
+  {
+    "fqdn": "rt0.fra.de.geant.net",
+    "site": "FRA"
+  },
+  {
+    "fqdn": "rt0.ams.nl.geant.net",
+    "site": "AMS"
+  },
+  {
+    "fqdn": "rt0.lju.si.geant.net",
+    "site": "LJU"
+  },
+  {
+    "fqdn": "rt0.sof.bg.geant.net",
+    "site": "SOF"
+  },
+  {
+    "fqdn": "rt0.ham.de.geant.net",
+    "site": "HAM"
+  }
+]
diff --git a/test/data/inprov-services.json b/test/data/inprov-services.json
new file mode 100644
index 0000000000000000000000000000000000000000..017bd5b7669250c6e94252f961d55f119c58fdf4
--- /dev/null
+++ b/test/data/inprov-services.json
@@ -0,0 +1,284 @@
+[
+  {
+    "endpoints": [
+      {
+        "hostname": "rt0.dub.ie.geant.net",
+        "interface": "lag-7.0"
+      }
+    ],
+    "name": "BIL-DUB-IPTRUNK",
+    "overlays": {
+      "speed": 536870912000
+    },
+    "sid": "LGS-00044",
+    "type": "IP TRUNK"
+  },
+  {
+    "endpoints": [
+      {
+        "hostname": "rt0.lon.uk.geant.net",
+        "interface": "lag-1"
+      }
+    ],
+    "name": "LONDON-LONDON-LAG-005(UNKNOWN)",
+    "overlays": {
+      "speed": 214748364800
+    },
+    "sid": "GA-000101",
+    "type": "ETHERNET"
+  },
+  {
+    "endpoints": [
+      {
+        "hostname": "rt0.lon.uk.geant.net",
+        "interface": "lag-3"
+      }
+    ],
+    "name": "LONDON-LONDON-LAG-003(UNKNOWN)",
+    "overlays": {
+      "speed": 429496729600
+    },
+    "sid": "LGA-000222",
+    "type": "ETHERNET"
+  },
+  {
+    "endpoints": [
+      {
+        "hostname": "rt0.lon.uk.geant.net",
+        "interface": "lag-2"
+      }
+    ],
+    "name": "ATHENS-LONDON-LAG-001(UNKNOWN)",
+    "overlays": {
+      "speed": 429496729600
+    },
+    "sid": "GA-000662",
+    "type": "ETHERNET"
+  },
+  {
+    "endpoints": [
+      {
+        "addresses": {
+          "v4": "62.40.98.60/31",
+          "v6": "2001:798:cc::65/126"
+        },
+        "hostname": "rt0.lon.uk.geant.net",
+        "interface": "lag-1.0"
+      }
+    ],
+    "name": "LON-LON-IPTRUNK",
+    "overlays": {
+      "speed": 214748364800
+    },
+    "sid": "GS-00010",
+    "type": "IP TRUNK"
+  },
+  {
+    "endpoints": [
+      {
+        "hostname": "rt0.lon.uk.geant.net",
+        "interface": "lag-3.0"
+      }
+    ],
+    "name": "DUB-LON-IPTRUNK",
+    "overlays": {
+      "speed": 429496729600
+    },
+    "sid": "LGS-00022",
+    "type": "IP TRUNK"
+  },
+  {
+    "endpoints": [
+      {
+        "hostname": "rt0.lon.uk.geant.net",
+        "interface": "lag-8"
+      }
+    ],
+    "name": "AMSTERDAM-LONDON-LAG-001(UNKNOWN)",
+    "overlays": {
+      "speed": 429496729600
+    },
+    "sid": "LGA-00771",
+    "type": "ETHERNET"
+  },
+  {
+    "endpoints": [
+      {
+        "hostname": "rt0.lon.uk.geant.net",
+        "interface": "lag-20"
+      }
+    ],
+    "name": "JISC-AP2-LAG",
+    "overlays": {
+      "speed": 10737418240
+    },
+    "sid": "GA-01760",
+    "type": "ETHERNET"
+  },
+  {
+    "endpoints": [
+      {
+        "hostname": "rt0.lon.uk.geant.net",
+        "interface": "lag-20.2501"
+      }
+    ],
+    "name": "JISC-AP2",
+    "overlays": {
+      "speed": 10737418240
+    },
+    "sid": "GS-00480",
+    "type": "GEANT IP"
+  },
+  {
+    "endpoints": [
+      {
+        "hostname": "rt0.lon.uk.geant.net",
+        "interface": "lag-20.2522"
+      }
+    ],
+    "name": "JISC-AP2-LHCONE",
+    "overlays": {
+      "speed": 10737418240
+    },
+    "sid": "GS-02474",
+    "type": "L3-VPN"
+  },
+  {
+    "endpoints": [
+      {
+        "hostname": "rt0.dub.ie.geant.net",
+        "interface": "lag-1"
+      }
+    ],
+    "name": "UNKNOWN_NOKIA-UNKNOWN_NOKIA-LAG-001(UNKNOWN)",
+    "overlays": {
+      "speed": 214748364800
+    },
+    "sid": "LGS-000991",
+    "type": "ETHERNET"
+  },
+  {
+    "endpoints": [
+      {
+        "hostname": "rt0.dub.ie.geant.net",
+        "interface": "lag-7"
+      }
+    ],
+    "name": "UNKNOWN_NOKIA-UNKNOWN_NOKIA-LAG-002(UNKNOWN)",
+    "overlays": {
+      "speed": 429496729600
+    },
+    "sid": "LGA-000442",
+    "type": "ETHERNET"
+  },
+  {
+    "endpoints": [
+      {
+        "hostname": "rt0.dub.ie.geant.net",
+        "interface": "lag-20"
+      }
+    ],
+    "name": "DUBLIN-DUBLIN-LAG-004(UNKNOWN)",
+    "overlays": {
+      "speed": 10737418240
+    },
+    "sid": "GA-50002",
+    "type": "ETHERNET"
+  },
+  {
+    "endpoints": [
+      {
+        "hostname": "rt0.dub.ie.geant.net",
+        "interface": "lag-20.3333"
+      }
+    ],
+    "name": "BELNET-AP1-IAS",
+    "overlays": {
+      "speed": 10737418240
+    },
+    "sid": "GS-00524",
+    "type": "GEANT PEERING"
+  },
+  {
+    "endpoints": [
+      {
+        "hostname": "rt0.dub.ie.geant.net",
+        "interface": "lag-20.2433"
+      }
+    ],
+    "name": "MARNET-AP1-IAS",
+    "overlays": {
+      "speed": 10737418240
+    },
+    "sid": "GS-00536",
+    "type": "GWS - INDIRECT"
+  },
+  {
+    "endpoints": [
+      {
+        "hostname": "rt0.dub.ie.geant.net",
+        "interface": "lag-20.2410"
+      }
+    ],
+    "name": "MARNET-AP1",
+    "overlays": {
+      "speed": 10737418240
+    },
+    "sid": "GS-12489",
+    "type": "GEANT IP"
+  },
+  {
+    "endpoints": [
+      {
+        "hostname": "rt0.dub.ie.geant.net",
+        "interface": "lag-20:765"
+      },
+      {
+        "hostname": "rt0.dub.ie.geant.net",
+        "interface": "pwe-34784"
+      }
+    ],
+    "name": "$GS-50008",
+    "overlays": {
+      "speed": 10737418240
+    },
+    "sid": "GS-50008",
+    "type": "GEANT PLUS"
+  },
+  {
+    "endpoints": [
+      {
+        "hostname": "rt0.dub.ie.geant.net",
+        "interface": "lag-24.0"
+      }
+    ],
+    "name": "IX-PEERINGS-IN-INEX",
+    "overlays": {
+      "speed": 10737418240
+    },
+    "sid": "GS-00950",
+    "type": "IP PEERING - NON R&E (PUBLIC)"
+  },
+  {
+    "endpoints": [
+      {
+        "hostname": "rt0.dub.ie.geant.net",
+        "interface": "lag-20:790"
+      },
+      {
+        "hostname": "rt0.dub.ie.geant.net",
+        "interface": "pwe-31757"
+      },
+      {
+        "hostname": "rt0.lon.uk.geant.net",
+        "interface": "pwe-31757"
+      }
+    ],
+    "name": "DUBLIN-LONDON-ETHS-001(BELNET)",
+    "overlays": {
+      "speed": 10737418240
+    },
+    "sid": "GS-50035",
+    "type": "ETHERNET"
+  }
+]
diff --git a/test/data/inprov-sites.json b/test/data/inprov-sites.json
new file mode 100644
index 0000000000000000000000000000000000000000..b6f5f56d3a852afd5f889c141591a7f99ece4052
--- /dev/null
+++ b/test/data/inprov-sites.json
@@ -0,0 +1,261 @@
+[
+  {
+    "city": "AMSTERDAM",
+    "country": "NETHERLANDS",
+    "latitude": 52.34638889,
+    "longitude": 4.93861111,
+    "name": "AMS"
+  },
+  {
+    "city": "BILBAO",
+    "country": "SPAIN",
+    "latitude": 43.3250881,
+    "longitude": -2.9804526,
+    "name": "BIL"
+  },
+  {
+    "city": "ATTIKI",
+    "country": "GREECE",
+    "latitude": 37.98,
+    "longitude": 23.73,
+    "name": "ATH2"
+  },
+  {
+    "city": "CAMBRIDGE",
+    "country": "UNITED KINGDOM",
+    "latitude": 52.19152778,
+    "longitude": 0.13383333,
+    "name": "CCH"
+  },
+  {
+    "city": "ATHENS",
+    "country": "GREECE",
+    "latitude": 37.97308611,
+    "longitude": 23.74555556,
+    "name": "ATH"
+  },
+  {
+    "city": "BRATISLAVA",
+    "country": "SLOVAKIA",
+    "latitude": 48.116833,
+    "longitude": 17.094389,
+    "name": "BRA"
+  },
+  {
+    "city": "BRUSSELS",
+    "country": "BELGIUM",
+    "latitude": 50.85694444,
+    "longitude": 4.41138889,
+    "name": "BRU"
+  },
+  {
+    "city": "BUCHAREST",
+    "country": "ROMANIA",
+    "latitude": 44.44474167,
+    "longitude": 26.09642222,
+    "name": "BUC"
+  },
+  {
+    "city": "DUBLIN",
+    "country": "IRELAND",
+    "latitude": 53.29198056,
+    "longitude": -6.41473333,
+    "name": "DUB"
+  },
+  {
+    "city": "CORK",
+    "country": "IRELAND",
+    "latitude": 51.90362538,
+    "longitude": -8.512314371,
+    "name": "COR"
+  },
+  {
+    "city": "BUDAPEST",
+    "country": "HUNGARY",
+    "latitude": 47.51777778,
+    "longitude": 19.05527778,
+    "name": "BUD"
+  },
+  {
+    "city": "CHISINAU",
+    "country": "MOLDOVA, REPUBLIC OF",
+    "latitude": 47.03072498,
+    "longitude": 28.82379273,
+    "name": "CHI"
+  },
+  {
+    "city": "KAUNAS",
+    "country": "LITHUANIA",
+    "latitude": 54.94067222,
+    "longitude": 24.01801389,
+    "name": "KAU"
+  },
+  {
+    "city": "KIEV",
+    "country": "UKRAINE",
+    "latitude": 50.44909773,
+    "longitude": 30.46527006,
+    "name": "KIE"
+  },
+  {
+    "city": "GENEVA",
+    "country": "SWITZERLAND",
+    "latitude": 46.2355997,
+    "longitude": 6.0553345,
+    "name": "GEN"
+  },
+  {
+    "city": "HAMBURG",
+    "country": "GERMANY",
+    "latitude": 53.55090278,
+    "longitude": 10.04648611,
+    "name": "HAM"
+  },
+  {
+    "city": "AMSTERDAM - Office",
+    "country": "NETHERLANDS",
+    "latitude": 52.31330556,
+    "longitude": 4.94911111,
+    "name": "AMO"
+  },
+  {
+    "city": "FRANKFURT",
+    "country": "GERMANY",
+    "latitude": 50.12029444,
+    "longitude": 8.73584444,
+    "name": "FRA"
+  },
+  {
+    "city": "LONDON",
+    "country": "UNITED KINGDOM",
+    "latitude": 51.4981657,
+    "longitude": -0.0152639,
+    "name": "LON"
+  },
+  {
+    "city": "ALCOBENDAS",
+    "country": "SPAIN",
+    "latitude": 40.53647778,
+    "longitude": -3.64880278,
+    "name": "MAD"
+  },
+  {
+    "city": "LISBON",
+    "country": "PORTUGAL",
+    "latitude": 38.759325,
+    "longitude": -9.142339,
+    "name": "LIS"
+  },
+  {
+    "city": "MARSEILLE",
+    "country": "FRANCE",
+    "latitude": 43.338019,
+    "longitude": 5.347666,
+    "name": "MAR"
+  },
+  {
+    "city": "LJUBLJANA",
+    "country": "SLOVENIA",
+    "latitude": 46.050171,
+    "longitude": 14.46033,
+    "name": "LJU"
+  },
+  {
+    "city": "SLOUGH",
+    "country": "UNITED KINGDOM",
+    "latitude": 51.5231433,
+    "longitude": -0.6224058,
+    "name": "LON2"
+  },
+  {
+    "city": "AUBERVILLIERS",
+    "country": "FRANCE",
+    "latitude": 48.90444444,
+    "longitude": 2.37111111,
+    "name": "PAR"
+  },
+  {
+    "city": "MILAN",
+    "country": "ITALY",
+    "latitude": 45.47527778,
+    "longitude": 9.10305556,
+    "name": "MIL2"
+  },
+  {
+    "city": "RIGA",
+    "country": "LATVIA",
+    "latitude": 56.94834444,
+    "longitude": 24.118,
+    "name": "RIG"
+  },
+  {
+    "city": "PORTO",
+    "country": "PORTUGAL",
+    "latitude": 41.177904,
+    "longitude": -8.594972,
+    "name": "POR"
+  },
+  {
+    "city": "PRAGUE",
+    "country": "CZECH REPUBLIC",
+    "latitude": 50.10166667,
+    "longitude": 14.39166667,
+    "name": "PRA"
+  },
+  {
+    "city": "POZNAN",
+    "country": "POLAND",
+    "latitude": 52.411775,
+    "longitude": 16.91756111,
+    "name": "POZ"
+  },
+  {
+    "city": "SOFIA",
+    "country": "BULGARIA",
+    "latitude": 42.67575833,
+    "longitude": 23.37098611,
+    "name": "SOF"
+  },
+  {
+    "city": "ZAGREB",
+    "country": "CROATIA",
+    "latitude": 45.79194444,
+    "longitude": 15.96944444,
+    "name": "ZAG"
+  },
+  {
+    "city": "TARTU",
+    "country": "ESTONIA",
+    "latitude": 58.383146,
+    "longitude": 26.71986,
+    "name": "TAR"
+  },
+  {
+    "city": "VIENNA",
+    "country": "AUSTRIA",
+    "latitude": 48.26888889,
+    "longitude": 16.41019444,
+    "name": "VIE"
+  },
+  {
+    "city": "BELGRADE",
+    "country": "SERBIA",
+    "latitude": 44.81141383,
+    "longitude": 20.39871378,
+    "name": "BEL"
+  },
+  {
+    "city": "THESSALONIKI",
+    "country": "GREECE",
+    "latitude": 40.662632,
+    "longitude": 22.860255,
+    "name": "THE"
+  },
+  {
+    "city": "GENEVA",
+    "country": "FR",
+    "latitude": 46.255952,
+    "longitude": 6.055262,
+    "name": "GEN2"
+  }
+]
diff --git a/test/test_map_endpoints.py b/test/test_map_endpoints.py
new file mode 100644
index 0000000000000000000000000000000000000000..e1befeb97c6ff7a82b27d3a2bff296877c0d9384
--- /dev/null
+++ b/test/test_map_endpoints.py
@@ -0,0 +1,67 @@
+import json
+import os
+import re
+
+import responses
+
+from mapping_provider.api.map import RouterList, ServiceList, SiteList
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
+
+
+def _load_test_data(filename: str) -> dict:
+    with open(os.path.join(DATA_DIR, filename)) as f:
+        return json.load(f)
+
+# @responses.activate
+# def test_inventory_uri_validation():
+#     responses.add(
+#         method=responses.GET, url=re.compile(r".*/version$"), json={"api": "0.9"}
+#     )
+#     assert (
+#         classifier.verify_inventory_provider_uri(None, None, "http://a.b.c:9999")
+#         == "http://a.b.c:9999/"
+#     )
+
+@responses.activate
+def test_get_sites(client):
+
+    responses.add(
+        method=responses.GET,
+        url=re.compile(r'.*/map/sites$'),
+        json=_load_test_data('inprov-sites.json')
+    )
+
+    rv = client.get("/map/sites")
+    assert rv.status_code == 200
+    assert rv.json()
+    SiteList.model_validate(rv.json())
+
+
+@responses.activate
+def test_get_routers(client):
+
+    responses.add(
+        method=responses.GET,
+        url=re.compile(r'.*/map/routers$'),
+        json=_load_test_data('inprov-routers.json')
+    )
+
+    rv = client.get("/map/routers")
+    assert rv.status_code == 200
+    assert rv.json()
+    RouterList.model_validate(rv.json())
+
+@responses.activate
+def test_get_trunks(client):
+
+    responses.add(
+        method=responses.GET,
+        url=re.compile(r'.*/map/services.*'),
+        json=_load_test_data('inprov-services.json')
+    )
+
+    rv = client.get("/map/trunks")
+    assert rv.status_code == 200
+    assert rv.json()
+    ServiceList.model_validate(rv.json())
diff --git a/test/test_version.py b/test/test_version.py
new file mode 100644
index 0000000000000000000000000000000000000000..863570b83cc17851e160c1df9ce446668096a40a
--- /dev/null
+++ b/test/test_version.py
@@ -0,0 +1,8 @@
+from mapping_provider.api.common import Version
+
+
+def test_version(client):
+    rv = client.get("/version")
+    assert rv.status_code == 200
+    assert rv.json()
+    Version.model_validate(rv.json())
diff --git a/tox.ini b/tox.ini
index c34930bd56b4c60894c3bcfb52180adfcf5447dc..936301481e2e1807b4a787019b0f54221957b3f1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,16 +1,35 @@
 [tox]
 envlist = lint, typecheck, docs
 
+[testenv:coverage]
+description = Run unit tests and save coverage
+deps =
+  pytest
+  pytest-cov
+  httpx  # required for fastapi TestClient
+  responses
+commands =
+  coverage erase
+  pytest --cov mapping_provider --cov-fail-under=80 --cov-report html --cov-report xml --cov-report term -p no:checkdocs
+
 [testenv:lint]
 description = Lint code with Ruff
 deps = ruff
-commands = ruff check mapping_provider
+commands = ruff check mapping_provider test
 
 [testenv:typecheck]
 description = Type-check code with mypy
-deps = mypy
+deps =
+    mypy
+    types-jsonschema
+    types-requests
 commands = mypy mapping_provider
 
+[testenv:sbom]
+description = Create SBOM for dependency analysis
+deps = cyclonedx-py
+commands = cyclonedx-py environment --output-format json -o bom.json
+
 [testenv:docs]
 description = Build docs
 deps =