diff --git a/mapping_provider/api/network_graph.py b/mapping_provider/api/network_graph.py
index 616bef799db9f9ef943f9e2315bd7293f4a106e1..fa968b180406a431358b05b8fc90befc62ec0d4d 100644
--- a/mapping_provider/api/network_graph.py
+++ b/mapping_provider/api/network_graph.py
@@ -8,6 +8,4 @@ router = APIRouter()
 
 @router.get("/network-graph")
 def get_network_graph(config: config_dep):
-    routers = load_routers(config)
-    trunks = load_trunks(config)
-    return {"routers": routers, "trunks": trunks}
+    return {"routers": load_routers(config), "trunks": load_trunks(config)}
diff --git a/mapping_provider/services/__init__.py b/mapping_provider/services/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e2df2d776dfd9a882d21afed78dcaa384fb8cc4f
--- /dev/null
+++ b/mapping_provider/services/__init__.py
@@ -0,0 +1 @@
+"""External services for the mapping provider."""
\ No newline at end of file
diff --git a/mapping_provider/services/gap.py b/mapping_provider/services/gap.py
new file mode 100644
index 0000000000000000000000000000000000000000..42010fb58e8b5c83005dfc6551d634ee99a8090d
--- /dev/null
+++ b/mapping_provider/services/gap.py
@@ -0,0 +1,146 @@
+import logging
+from collections.abc import Callable
+from typing import Any
+
+import requests
+
+from mapping_provider.dependencies import config_dep
+
+logger = logging.getLogger(__name__)
+
+GRANT_TYPE = "client_credentials"
+SCOPE = "openid profile email aarc"
+
+
+def get_token_endpoint(discovery_endpoint_url: str, session: requests.Session) -> str:
+    """Fetch the token endpoint URL from discovery document."""
+    response = session.get(discovery_endpoint_url)
+    response.raise_for_status()
+    return response.json()["token_endpoint"]
+
+
+def get_token(config: dict, session: requests.Session) -> str:
+    """Retrieve an access token using client credentials flow."""
+    aai_config = config["aai"]
+    token_endpoint = get_token_endpoint(aai_config["discovery_endpoint_url"], session)
+    response = session.post(
+        token_endpoint,
+        data={
+            "grant_type": GRANT_TYPE,
+            "scope": SCOPE,
+            "client_id": aai_config["client_id"],
+            "client_secret": aai_config["secret"],
+        },
+    )
+    response.raise_for_status()
+    return response.json()["access_token"]
+
+
+def make_request(query: str, token: str, config: dict, session: requests.Session) -> dict:
+    """Make a GraphQL request to the orchestrator API."""
+    api_url = f"{config['orchestrator']['url']}/api/graphql"
+    headers = {"Authorization": f"Bearer {token}"}
+    response = session.post(api_url, headers=headers, json={"query": query})
+    response.raise_for_status()
+
+    data = response.json()
+    if "errors" in data:
+        logger.error(f"GraphQL query returned errors: {data['errors']}. Query: {query}")
+        raise ValueError(f"GraphQL query errors: {data['errors']}")
+
+    return data
+
+
+def extract_router(product_block_instances: list[dict]) -> str | None:
+    """Extract router FQDNs from productBlockInstances."""
+    for instance in product_block_instances:
+        for value in instance.get("productBlockInstanceValues", []):
+            if value.get("field") == "routerFqdn" and value.get("value"):
+                return value["value"]
+    return None
+
+
+def extract_trunk(product_block_instances: list[dict]) -> list[str] | None:
+    """Extract trunks from productBlockInstances."""
+    fqdns = []
+    for instance in product_block_instances:
+        for value in instance.get("productBlockInstanceValues", []):
+            if value.get("field") == "routerFqdn" and value.get("value"):
+                fqdns.append(value["value"])
+                break
+
+    if len(fqdns) >= 2:
+        return [fqdns[0], fqdns[1]]
+    return None
+
+
+def load_inventory(
+        config: dict,
+        token: str,
+        tag: str,
+        session: requests.Session,
+        extractor: Callable[[list[dict]], Any | None],
+) -> list[Any]:
+    """
+    Generic function to load inventory items based on tag and extractor function.
+
+    The extractor receives a list of productBlockInstances and returns parsed output.
+    """
+    results = []
+    end_cursor = 0
+    has_next_page = True
+
+    while has_next_page:
+        query = f"""
+           query {{
+               subscriptions(
+                   filterBy: {{field: "status", value: "PROVISIONING|ACTIVE"}},
+                   first: 100,
+                   after: {end_cursor},
+                   query: "tag:({tag})"
+               ) {{
+                   pageInfo {{
+                       hasNextPage
+                       endCursor
+                   }}
+                   page {{
+                       subscriptionId
+                       product {{
+                           tag
+                       }}
+                       productBlockInstances {{
+                           productBlockInstanceValues
+                       }}
+                   }}
+               }}
+           }}
+           """
+
+        data = make_request(query, token, config, session)
+        page_data = data.get("data", {}).get("subscriptions", {}).get("page", [])
+        page_info = data.get("data", {}).get("subscriptions", {}).get("pageInfo", {})
+
+        for item in page_data:
+            instances = item.get("productBlockInstances", [])
+            extracted = extractor(instances)
+            if extracted:
+                results.append(extracted)
+
+        has_next_page = page_info.get("hasNextPage", False)
+        end_cursor = page_info.get("endCursor", 0)
+
+    return results
+
+
+def load_routers(config: config_dep) -> list[str]:
+    """Load routers (nodes) from orchestrator."""
+    with requests.Session() as session:
+        token = get_token(config, session)
+        return load_inventory(config, token, tag="RTR", session=session, extractor=extract_router)
+
+
+def load_trunks(config: config_dep) -> list[list[str]]:
+    """Load trunks (edges) from orchestrator."""
+    with requests.Session() as session:
+        token = get_token(config, session)
+        return load_inventory(config, token, tag="IPTRUNK", session=session, extractor=extract_trunk)
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 8cedf04a6b774601a712430bfb7494d302f9fb80..2f0ae84370ebd59e199d5f203efca96587de9ed0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,6 @@
 fastapi
 uvicorn[standard]
+requests
 
 sphinx
 sphinx-rtd-theme