diff --git a/inventory_provider/routes/poller.py b/inventory_provider/routes/poller.py
index d812b0a8b1ebe34254a50752ddf615a6115db25f..1939e5d4b83e561169b046f2dd10c0d6be0bb2b2 100644
--- a/inventory_provider/routes/poller.py
+++ b/inventory_provider/routes/poller.py
@@ -36,12 +36,18 @@ These endpoints are intended for use by BRIAN.
 .. autofunction:: inventory_provider.routes.poller.gws_indirect
 
 
-/poller/services</type>
+/poller/services</service-type>
 ---------------------------------
 
 .. autofunction:: inventory_provider.routes.poller.get_services
 
 
+/poller/service-types
+---------------------------------
+
+.. autofunction:: inventory_provider.routes.poller.get_service_types
+
+
 """
 import json
 import logging
@@ -52,6 +58,7 @@ from lxml import etree
 
 from inventory_provider import juniper
 from inventory_provider.routes import common
+from inventory_provider.tasks.common import ims_sorted_service_type_key
 from inventory_provider.routes.classifier import get_ims_equipment_name, \
     get_ims_interface
 from inventory_provider.routes.common import _ignore_cache_or_retrieve
@@ -245,6 +252,13 @@ SERVICES_LIST_SCHEMA = {
 }
 
 
+STRING_LIST_SCHEMA = {
+    '$schema': 'http://json-schema.org/draft-07/schema#',
+    'type': 'array',
+    'items': {'type': 'string'}
+}
+
+
 @routes.after_request
 def after_request(resp):
     return common.after_request(resp)
@@ -706,25 +720,22 @@ def gws_direct():
     return Response(result, mimetype="application/json")
 
 
-@routes.route('/services', methods=['GET', 'POST'])
-@routes.route('/services/<service_type>', methods=['GET', 'POST'])
-@common.require_accepts_json
-def get_services(service_type=None):
+def _get_services_internal(service_type=None):
     """
-    Handler for `/poller/services/service_type`.
+    Performs the lookup and caching done for calls to
+    `/poller/services</service-type>`
+
+    This is a separate private utility so that it can be called by
+    :meth:`inventory_provider.routes.poller.get_services`
+    and :meth:`inventory_provider.routes.poller.get_service_types`
 
     The response will be formatted according to the following schema:
 
     .. asjson::
        inventory_provider.routes.poller.SERVICES_LIST_SCHEMA
 
-    A few possible values for `service_type` are (currently): gws_internal,
-    geant_ip, geant_lambda, l3_vpn, md_vpn_proxy (there are more)
-
-    If the endpoint is called with no `service_type`,
-    all services are returned
-
-    :return:
+    :param service_type: a service type, or None to return all
+    :return: service list, json-serialized to a string
     """
     def _services():
         key_pattern = f'ims:services:{service_type}:*' \
@@ -765,18 +776,51 @@ def get_services(service_type=None):
         result = list(result)
 
         if not result:
-            message = f'no {service_type} services found' \
-                    if service_type else 'no services found'
-            return Response(
-                response=message,
-                status=404,
-                mimetype='text/html')
+            return None
 
         # cache this data for the next call
         result = json.dumps(result)
-        redis.set(cache_key, result.encode('utf-8'))
+        redis.set(cache_key, result)
+
+    return result
+
+@routes.route('/services', methods=['GET', 'POST'])
+@routes.route('/services/<service_type>', methods=['GET', 'POST'])
+@common.require_accepts_json
+def get_services(service_type=None):
+    """
+    Handler for `/poller/services</service-type>`
+
+    Use `/poller/service-types` for possible values of `service-type`.
+
+    If the endpoint is called with no `service-type`,
+    all services are returned.
+
+    The response will be formatted according to the following schema:
 
-    return Response(result, mimetype='application/json')
+    .. asjson::
+       inventory_provider.routes.poller.SERVICES_LIST_SCHEMA
+
+    A few possible values for `service_type` are (currently): gws_internal,
+    geant_ip, geant_lambda, l3_vpn, md_vpn_proxy (there are more)
+
+    If the endpoint is called with no `service_type`,
+    all services are returned
+
+    :return:
+    """
+
+    services_json_str = _get_services_internal(service_type)
+
+    if not services_json_str:
+        message = f'no {service_type} services found' \
+            if service_type else 'no services found'
+        return Response(
+            response=message,
+            status=404,
+            mimetype='text/html')
+
+    return Response(services_json_str, mimetype='application/json')
 
 
 @routes.route("/gws/indirect", methods=['GET', 'POST'])
@@ -791,3 +835,45 @@ def gws_indirect():
     :return:
     """
     return get_services(service_type='gws_indirect')
+
+
+@routes.route('/service-types', methods=['GET', 'POST'])
+@common.require_accepts_json
+def get_service_types():
+    """
+    Handler for `/poller/service-types`
+
+    This method returns a list of all values of `service_type` that
+    can be used with `/poller/services/service-type` to return
+    a non-empty list of services.
+
+    The response will be formatted according to the following schema:
+
+    .. asjson::
+       inventory_provider.routes.poller.STRING_LIST_SCHEMA
+
+    """
+    cache_key = f'classifier-cache:poller:service-types'
+
+    redis = common.get_current_redis()
+    service_types = _ignore_cache_or_retrieve(request, cache_key, redis)
+
+    if not service_types:
+        all_services = json.loads(_get_services_internal())
+        service_types = {
+            ims_sorted_service_type_key(s['type'])
+            for s in all_services
+        }
+
+        if not service_types:
+            return Response(
+                response='no service types found',
+                status=404,
+                mimetype='text/html')
+
+        # cache this data for the next call
+        service_types = sorted(list(service_types))
+        service_types = json.dumps(service_types)
+        redis.set(cache_key, service_types)
+
+    return Response(service_types, mimetype='application/json')
diff --git a/inventory_provider/tasks/common.py b/inventory_provider/tasks/common.py
index 427d51d8a18e4128f3dd3b174274e65fa17f5f8f..f7942d2f4e28826b4723dd9b7e92521d7c6815de 100644
--- a/inventory_provider/tasks/common.py
+++ b/inventory_provider/tasks/common.py
@@ -1,5 +1,6 @@
 import json
 import logging
+import re
 import time
 
 import jsonschema
@@ -218,3 +219,23 @@ def get_next_redis(config):
             'derived next id: {}'.format(next_id))
 
     return _get_redis(config, next_id)
+
+
+def ims_sorted_service_type_key(service_type):
+    """
+    special-purpose function used for mapping IMS service type strings
+    to the key used in redis ('ims:services:*')
+    
+    this method is only used by
+    :meth:`inventory_provider:tasks:worker.update_circuit_hierarchy_and_port_id_services
+    and :meth:`inventory_provider:routes.poller.get_service_types
+
+    :param service_type: IMS-formatted service_type string
+    :return: ims sorted service type redis key
+    """  # noqa: E501
+
+    service_type = re.sub(
+        r'[^a-zA-Z0-9]+', '_', service_type.lower())
+    # prettification ... e.g. no trailing _ for 'MD-VPN (NATIVE)'
+    service_type = re.sub('_+$', '', service_type)
+    return re.sub('^_+', '', service_type)
diff --git a/inventory_provider/tasks/worker.py b/inventory_provider/tasks/worker.py
index 58bccc8252ffda083a0243fa4def71477e6f7200..4d7e33c506390db0a5b76b129dabe06d02156ffd 100644
--- a/inventory_provider/tasks/worker.py
+++ b/inventory_provider/tasks/worker.py
@@ -29,7 +29,8 @@ from inventory_provider.db.ims import IMS
 from inventory_provider.tasks.app import app
 from inventory_provider.tasks.common \
     import get_next_redis, get_current_redis, \
-    latch_db, get_latch, set_latch, update_latch_status
+    latch_db, get_latch, set_latch, update_latch_status, \
+    ims_sorted_service_type_key
 from inventory_provider.tasks import monitor
 from inventory_provider import config
 from inventory_provider import environment
@@ -762,14 +763,8 @@ def update_circuit_hierarchy_and_port_id_services(self, use_current=False):
                 circ['calculated-speed'] = _get_speed(circ['id'])
                 _format_service(circ)
 
-                service_type_key = re.sub(
-                    r'[^a-zA-Z0-9]+', '_', circ['service_type'].lower())
-                # prettification ... e.g. no trailing _ for 'MD-VPN (NATIVE)'
-                service_type_key = re.sub('_+$', '', service_type_key)
-                service_type_key = re.sub('^_+', '', service_type_key)
-
                 type_services = services_by_type.setdefault(
-                    service_type_key, dict())
+                    ims_sorted_service_type_key(circ['service_type']), dict())
                 type_services[circ['id']] = circ
 
             interface_services[k].extend(circuits)
diff --git a/test/test_general_poller_routes.py b/test/test_general_poller_routes.py
index b2179230221b5d5b2cc2c4fbeec8196512016b12..f9359a367183ab093ab47a5e8fdef7280399663b 100644
--- a/test/test_general_poller_routes.py
+++ b/test/test_general_poller_routes.py
@@ -112,3 +112,16 @@ def test_all_services_by_type(client, uri_type, expected_type):
 
     assert response_data  # test data is non-empty
     assert all(s['type'] == expected_type for s in response_data)
+
+
+def test_get_service_types(client):
+    rv = client.get(
+        f'/poller/service-types',
+        headers=DEFAULT_REQUEST_HEADERS)
+    assert rv.status_code == 200
+    assert rv.is_json
+    response_data = json.loads(rv.data.decode('utf-8'))
+    jsonschema.validate(response_data, poller.STRING_LIST_SCHEMA)
+
+    assert response_data  # test data is non-empty
+