diff --git a/SAS/TMSS/services/scheduling/lib/constraints/template_constraints_v1.py b/SAS/TMSS/services/scheduling/lib/constraints/template_constraints_v1.py
index 27c764a783f8fdf149ef3d961a7ef7a532673191..dd4f5451a7e844d41a5070b3e083d2cbe4f3fca2 100644
--- a/SAS/TMSS/services/scheduling/lib/constraints/template_constraints_v1.py
+++ b/SAS/TMSS/services/scheduling/lib/constraints/template_constraints_v1.py
@@ -29,9 +29,11 @@ import logging
 logger = logging.getLogger(__name__)
 from datetime import datetime, timedelta
 from dateutil import parser
+from astropy.coordinates import Angle
+import astropy.units
 
 from lofar.sas.tmss.tmss.tmssapp import models
-from lofar.sas.tmss.tmss.tmssapp.conversions import create_astroplan_observer_for_station, Time, timestamps_and_stations_to_sun_rise_and_set, coordinates_and_timestamps_to_separation_from_bodies
+from lofar.sas.tmss.tmss.tmssapp.conversions import create_astroplan_observer_for_station, Time, timestamps_and_stations_to_sun_rise_and_set, coordinates_and_timestamps_to_separation_from_bodies, coordinates_timestamps_and_stations_to_target_rise_and_set
 from lofar.sas.tmss.tmss.exceptions import TMSSException
 
 from . import ScoredSchedulingUnit
@@ -194,6 +196,9 @@ def can_run_anywhere_within_timewindow_with_sky_constraints(scheduling_unit: mod
     :return: True if all sky constraints are met over the entire time window, else False.
     """
     constraints = scheduling_unit.draft.scheduling_constraints_doc
+    if not "sky" in constraints:
+        return True
+
     for task in scheduling_unit.requirements_doc['tasks'].values():
         if 'specifications_doc' in task:
             if 'tile_beam' in task['specifications_doc']:
@@ -201,7 +206,8 @@ def can_run_anywhere_within_timewindow_with_sky_constraints(scheduling_unit: mod
                 angle1 = beam['angle1']
                 angle2 = beam['angle2']
                 direction_type = beam['direction_type']
-                if "sky" in constraints and 'min_distance' in constraints['sky']:
+
+                if 'min_distance' in constraints['sky']:
                     # currently we only check at bounds, we probably want to add some more samples in between later on
                     distances = coordinates_and_timestamps_to_separation_from_bodies(angle1=angle1, angle2=angle2, direction_type=direction_type, timestamps=(lower_bound, upper_bound), bodies=tuple(constraints['sky']['min_distance'].keys()))
                     for body, min_distance in constraints['sky']['min_distance'].items():
@@ -211,6 +217,25 @@ def can_run_anywhere_within_timewindow_with_sky_constraints(scheduling_unit: mod
                                 logger.info('Distance=%s from body=%s does not meet min_distance=%s constraint at timestamp=%s' % (angle.rad, body, min_distance, timestamp))
                                 return False
 
+                if 'min_target_elevation' in constraints['sky'] and task['specifications_template'] == 'target observation' or \
+                   'min_calibrator_elevation' in constraints['sky'] and task['specifications_template'] == 'calibrator observation':
+                    if task['specifications_template'] == 'calibrator observation':
+                        min_elevation = Angle(constraints['sky']['min_calibrator_elevation'], unit=astropy.units.rad)
+                    else:
+                        min_elevation = Angle(constraints['sky']['min_target_elevation'], unit=astropy.units.rad)
+                    timestamps = (lower_bound, upper_bound)
+                    stations = task['specifications_doc']['stations']
+                    # currently we only check at bounds, we probably want to add some more samples in between later on
+                    target_rise_and_set_times = coordinates_timestamps_and_stations_to_target_rise_and_set(angle1=angle1, angle2=angle2, direction_type=direction_type, timestamps=timestamps, stations=tuple(stations), angle_to_horizon=min_elevation)
+                    for station, times in target_rise_and_set_times.items():
+                        for i in range(len(timestamps)):
+                            if not (timestamps[i] > times[0]['rise'] and timestamps[i] < times[0]['set']):
+                                if task['specifications_template'] == 'calibrator observation':
+                                    logger.info('min_calibrator_elevation=%s constraint is not met at timestamp=%s' % (min_elevation.rad, timestamps[i]))
+                                else:
+                                    logger.info('min_target_elevation=%s constraint is not met at timestamp=%s' % (min_elevation.rad, timestamps[i]))
+                                return False
+
     return True
 
 
diff --git a/SAS/TMSS/services/scheduling/test/t_dynamic_scheduling.py b/SAS/TMSS/services/scheduling/test/t_dynamic_scheduling.py
index 10cd3206512e03b8c52a20796ce75b46dfe10384..343cc8925a42702dc814b06c808c68d9498f95ee 100755
--- a/SAS/TMSS/services/scheduling/test/t_dynamic_scheduling.py
+++ b/SAS/TMSS/services/scheduling/test/t_dynamic_scheduling.py
@@ -420,10 +420,13 @@ class TestDailyConstraints(TestCase):
         lower_bound = datetime(2020, 1, 1, 8, 0, 0)
         upper_bound = datetime(2020, 1, 1, 12, 0, 0)
         self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_daily_constraints(self.scheduling_unit_blueprint, lower_bound, upper_bound))
-        
+
     def test_can_run_within_timewindow_with_daytime_constraint_returns_correct_value(self):
         # todo: for time ranges across dates, consider removing the mock for this because the moving window cannot be easily mocked
-        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky']['min_distance'] = {}  # remove sky constraint
+        # remove other constraints:
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {}
+
+        # set constraint to test
         self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['require_day'] = True
         self.scheduling_unit_blueprint.save()
 
@@ -551,10 +554,13 @@ class TestDailyConstraints(TestCase):
         lower_bound = datetime(2020, 1, 1, 3, 0, 0)
         upper_bound = datetime(2020, 1, 1, 23, 0, 0)
         self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_daily_constraints(self.scheduling_unit_blueprint, lower_bound, upper_bound))
-        
+
     def test_can_run_within_timewindow_with_nighttime_constraint_returns_correct_value(self):
         # todo: for time ranges across dates, consider removing the mock for this because the moving window cannot be easily mocked
-        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky']['min_distance'] = {}  # remove sky constraint
+        # remove other constraints:
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {}
+
+        # set constraint to test
         self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['require_night'] = True
         self.scheduling_unit_blueprint.save()
 
@@ -569,7 +575,7 @@ class TestDailyConstraints(TestCase):
         lower_bound = datetime(2020, 1, 1, 15, 0, 0)
         upper_bound = datetime(2020, 1, 1, 23, 0, 0)
         self.assertTrue(can_run_within_timewindow(self.scheduling_unit_blueprint, lower_bound, upper_bound))
-        
+
 
     # avoid_twilight
 
@@ -668,7 +674,6 @@ class TestDailyConstraints(TestCase):
         self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_daily_constraints(self.scheduling_unit_blueprint, lower_bound, upper_bound))
 
     def test_can_run_anywhere_within_timewindow_with_daily_constraints_with_twilight_constraint_returns_false_when_partially_in_twilight(self):
-        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky']['min_distance'] = {}  # remove sky constraint
         self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['avoid_twilight'] = True
         self.scheduling_unit_blueprint.save()
 
@@ -684,7 +689,10 @@ class TestDailyConstraints(TestCase):
 
     def test_can_run_within_timewindow_with_twilight_constraint_returns_correct_value(self):
         # todo: for time ranges across dates, consider removing the mock for this because the moving window cannot be easily mocked
-        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky']['min_distance'] = {}  # remove sky constraint
+        # remove other constraints:
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {}
+
+        # set constraint to test
         self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['avoid_twilight'] = True
         self.scheduling_unit_blueprint.save()
 
@@ -725,22 +733,45 @@ class TestSkyConstraints(unittest.TestCase):
         self.distance_mock = self.distance_patcher.start()
         self.distance_mock.return_value = self.distance_data
         self.addCleanup(self.distance_patcher.stop)
+        
+        self.target_rise_and_set_data = {"CS002": [{"rise": datetime(2020, 1, 1, 8, 0, 0), "set": datetime(2020, 1, 1, 12, 30, 0)},
+                                                   {"rise": datetime(2020, 1, 1, 8, 0, 0), "set": datetime(2020, 1, 1, 12, 30, 0)}]}
+        self.target_rise_and_set_patcher = mock.patch('lofar.sas.tmss.services.scheduling.constraints.template_constraints_v1.coordinates_timestamps_and_stations_to_target_rise_and_set')
+        self.target_rise_and_set_mock = self.target_rise_and_set_patcher.start()
+        self.target_rise_and_set_mock.return_value = self.target_rise_and_set_data
+        self.addCleanup(self.target_rise_and_set_patcher.stop)
 
     # min_distance
 
     def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_min_distance_constraint_returns_true_when_met(self):
-        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky']['min_distance'] = {'sun': 0.1, 'moon': 0.1, 'jupiter': 0.1}
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'min_distance': {'sun': 0.1, 'moon': 0.1, 'jupiter': 0.1}}
         self.scheduling_unit_blueprint.save()
         timestamp = datetime(2020, 1, 1, 10, 0, 0)
         returned_value = tc1.can_run_anywhere_within_timewindow_with_sky_constraints(self.scheduling_unit_blueprint, timestamp, timestamp + timedelta(seconds=self.obs_duration))
         self.assertTrue(returned_value)
 
     def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_min_distance_constraint_returns_false_when_not_met(self):
-        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky']['min_distance'] = {'sun': 0.2, 'moon': 0.2, 'jupiter': 0.2}
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'min_distance': {'sun': 0.2, 'moon': 0.2, 'jupiter': 0.2}}
         self.scheduling_unit_blueprint.save()
         timestamp = datetime(2020, 1, 1, 10, 0, 0)
         returned_value = tc1.can_run_anywhere_within_timewindow_with_sky_constraints(self.scheduling_unit_blueprint, timestamp, timestamp + timedelta(seconds=self.obs_duration))
         self.assertFalse(returned_value)
+        
+    # min_target_elevation
+
+    def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_min_target_elevation_constraint_returns_true_when_met(self):
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'min_target_elevation': 0.1}
+        self.scheduling_unit_blueprint.save()
+        timestamp = datetime(2020, 1, 1, 10, 0, 0)
+        returned_value = tc1.can_run_anywhere_within_timewindow_with_sky_constraints(self.scheduling_unit_blueprint, timestamp, timestamp + timedelta(seconds=self.obs_duration))
+        self.assertTrue(returned_value)
+
+    def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_min_target_elevation_constraint_returns_false_when_not_met(self):
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'min_target_elevation': 0.2}
+        self.scheduling_unit_blueprint.save()
+        timestamp = datetime(2020, 1, 1, 11, 0, 0)
+        returned_value = tc1.can_run_anywhere_within_timewindow_with_sky_constraints(self.scheduling_unit_blueprint, timestamp, timestamp + timedelta(seconds=self.obs_duration))
+        self.assertFalse(returned_value)
 
 
 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO)
diff --git a/SAS/TMSS/src/tmss/tmssapp/conversions.py b/SAS/TMSS/src/tmss/tmssapp/conversions.py
index 335b8937493b5c3e26fa9f0a80b798bee31107c0..40765b6998575b0cdccb3b1d11c113c527f64cb3 100644
--- a/SAS/TMSS/src/tmss/tmssapp/conversions.py
+++ b/SAS/TMSS/src/tmss/tmssapp/conversions.py
@@ -25,7 +25,8 @@ def create_astroplan_observer_for_station(station: str) -> Observer:
 
 # default angle to the horizon at which the sunset/sunrise starts and ends, as per LOFAR definition.
 SUN_SET_RISE_ANGLE_TO_HORIZON = Angle(10, unit=astropy.units.deg)
-SUN_SET_RISE_PRECISION = 30  # n_grid_points; higher is more precise but very costly; astropy defaults to 150, errors now can be in the minutes, increase if this is not good enough
+# default n_grid_points; higher is more precise but very costly; astropy defaults to 150, errors now can be in the minutes, increase if this is not good enough
+SUN_SET_RISE_PRECISION = 30
 
 @lru_cache(maxsize=256, typed=False)  # does not like lists, so use tuples to allow caching
 def timestamps_and_stations_to_sun_rise_and_set(timestamps: tuple, stations: tuple, angle_to_horizon: Angle=SUN_SET_RISE_ANGLE_TO_HORIZON) -> dict:
@@ -35,6 +36,7 @@ def timestamps_and_stations_to_sun_rise_and_set(timestamps: tuple, stations: tup
     The night is usually the one _starting_ on the date of the time stamp, unless the given timestamp falls before sunrise, in which case it is the night _ending_ on the timestamp date.
     :param timestamps: tuple of datetimes, e.g. (datetime(2020, 1, 1), datetime(2020, 1, 2))
     :param stations: tuple of station names, e.g. ("CS002",)
+    :param angle_to_horizon: the angle between horizon and given coordinates for which rise and set times are returned
     :return A dict that maps station names to a nested dict that contains lists of start and end times for sunrise, sunset, etc, on each requested date.
         E.g.
         {"CS002":
@@ -55,7 +57,7 @@ def timestamps_and_stations_to_sun_rise_and_set(timestamps: tuple, stations: tup
             # todo: this can probably be made faster by moving the following logic to an own function with single station/timestamp as input and putting the lru_cache on there.
             #  This also means that we have to strip the time from the datetime. Can this be safely done?
             observer = create_astroplan_observer_for_station(station)
-            sunrise_start = observer.sun_rise_time(time=Time(datetime.combine(timestamp.date(), dtime(12,0,0))), which='previous', n_grid_points=SUN_SET_RISE_PRECISION)
+            sunrise_start = observer.sun_rise_time(time=Time(datetime.combine(timestamp.date(), dtime(12,0,0))), horizon=-angle_to_horizon, which='previous', n_grid_points=SUN_SET_RISE_PRECISION)
             sunrise_end = observer.sun_rise_time(time=Time(sunrise_start), horizon=angle_to_horizon, which='next', n_grid_points=SUN_SET_RISE_PRECISION)
             sunset_start = observer.sun_set_time(time=sunrise_end, horizon=angle_to_horizon, which='next', n_grid_points=SUN_SET_RISE_PRECISION)
             sunset_end = observer.sun_set_time(time=sunset_start, horizon=-angle_to_horizon, which='next', n_grid_points=SUN_SET_RISE_PRECISION)
@@ -91,7 +93,7 @@ def coordinates_and_timestamps_to_separation_from_bodies(angle1: float, angle2:
         }
     """
     if direction_type == "J2000":
-        coord = astropy.coordinates.SkyCoord(ra=angle1, dec=angle2, unit=astropy.units.deg)
+        coord = astropy.coordinates.SkyCoord(ra=angle1, dec=angle2, unit=astropy.units.rad)
     else:
         raise ValueError("Do not know how to convert direction_type=%s to SkyCoord" % direction_type)
     return_dict = {}
@@ -105,6 +107,48 @@ def coordinates_and_timestamps_to_separation_from_bodies(angle1: float, angle2:
     return return_dict
 
 
+# default angle above horizon, above which the target it reporte as 'up'
+TARGET_SET_RISE_ANGLE_TO_HORIZON = Angle(0, unit=astropy.units.deg)  # if default should be non-zero, should we include it explicitly in response?
+# default n_grid_points; higher is more precise but very costly; astropy defaults to 150, note that errors can be in the minutes with a lower values
+TARGET_SET_RISE_PRECISION = 150
+
+@lru_cache(maxsize=256, typed=False)  # does not like lists, so use tuples to allow caching
+def coordinates_timestamps_and_stations_to_target_rise_and_set(angle1: float, angle2: float, direction_type: str, timestamps: tuple, stations: tuple, angle_to_horizon: Angle=TARGET_SET_RISE_ANGLE_TO_HORIZON) -> dict:
+    """
+    Compute rise and set times of the given coordinates above the provided horizon, for each given station and timestamp.
+    The set time is always the one following the provided timestamp.
+    This implies that if the target is up at a given timestamp, the surrounding rise and set times are returned.
+    Otherwise both rise and set times follow the timestamp.
+    :param angle1: first angle of celectial coordinates, e.g. RA
+    :param angle2: second angle of celectial coordinates, e.g. Dec
+    :param direction_type: direction_type of celectial coordinates, e.g. 'J2000'
+    :param timestamps: tuple of datetimes, e.g. (datetime(2020, 1, 1), datetime(2020, 1, 2))
+    :param stations: tuple of station names, e.g. ("CS002",)
+    :param angle_to_horizon: the angle between horizon and given coordinates for which rise and set times are returned
+    :return A dict that maps station names to a list of dicts with rise and set times for each requested date.
+        E.g.
+        {"CS002": [{"rise": datetime(2020, 1, 1, 4, 0, 0), "set": datetime(2020, 1, 1, 11, 0, 0)},
+                   {"rise": datetime(2020, 1, 2, 4, 0, 0), "set": datetime(2020, 1, 2, 11, 0, 0)}]
+        }
+    """
+    if direction_type == "J2000":
+        coord = astropy.coordinates.SkyCoord(ra=angle1, dec=angle2, unit=astropy.units.rad)
+    else:
+        raise ValueError("Do not know how to convert direction_type=%s to SkyCoord" % direction_type)
+    return_dict = {}
+    for station in stations:
+        for timestamp in timestamps:
+            # todo: this can probably be made faster by moving the following logic to an own function with single station/timestamp as input and putting the lru_cache on there.
+            observer = create_astroplan_observer_for_station(station)
+            target_set = observer.target_set_time(target=coord, time=Time(timestamp), horizon=angle_to_horizon, which='next', n_grid_points=TARGET_SET_RISE_PRECISION)
+            target_rise = observer.target_rise_time(target=coord, time=Time(target_set), horizon=angle_to_horizon, which='previous', n_grid_points=TARGET_SET_RISE_PRECISION)
+
+            return_dict.setdefault(station, []).append({"rise": target_rise.to_datetime(), "set": target_set.to_datetime()})
+
+    return return_dict
+
+
+
 def local_sidereal_time_for_utc_and_station(timestamp: datetime = None,
                                             station: str = 'CS002',
                                             field: str = 'LBA',
diff --git a/SAS/TMSS/src/tmss/tmssapp/views.py b/SAS/TMSS/src/tmss/tmssapp/views.py
index b099d553958475e0d7410dc7bef529503b7f9d4d..8dabf0b06f1967e925ea8fac41e80afb84e31387 100644
--- a/SAS/TMSS/src/tmss/tmssapp/views.py
+++ b/SAS/TMSS/src/tmss/tmssapp/views.py
@@ -15,7 +15,9 @@ from django.apps import apps
 from rest_framework.decorators import api_view
 from datetime import datetime
 import dateutil.parser
-from lofar.sas.tmss.tmss.tmssapp.conversions import local_sidereal_time_for_utc_and_station, local_sidereal_time_for_utc_and_longitude, timestamps_and_stations_to_sun_rise_and_set, coordinates_and_timestamps_to_separation_from_bodies
+from astropy.coordinates import Angle
+import astropy.units
+from lofar.sas.tmss.tmss.tmssapp.conversions import local_sidereal_time_for_utc_and_station, local_sidereal_time_for_utc_and_longitude, timestamps_and_stations_to_sun_rise_and_set, coordinates_and_timestamps_to_separation_from_bodies, coordinates_timestamps_and_stations_to_target_rise_and_set
 
 # Note: Decorate with @api_view to get this picked up by Swagger
 
@@ -216,3 +218,54 @@ def get_angular_separation_from_bodies(request):
             new_sep_dict.setdefault(body, {})[timestamp.isoformat()] = angle.rad
 
     return JsonResponse(new_sep_dict)
+
+
+
+@permission_classes([AllowAny])
+@authentication_classes([AllowAny])
+@swagger_auto_schema(method='GET',
+                     responses={200: 'A JSON object with rise and set times of the given coordinates above the provided horizon, for each given station and timestamp.'},
+                     operation_description="Get rise and set times of the given coordinates above the provided horizon, for each given station and timestamp. \n\n"
+                                           "Example request: /api/util/target_rise_and_set?angle1=0.5&angle2=0.5&timestamps=2020-01-01T15&horizon=0.3",
+                     manual_parameters=[Parameter(name='angle1', required=True, type='string', in_='query',
+                                                  description="first angle of celectial coordinates as float, e.g. RA"),
+                                        Parameter(name='angle2', required=True, type='string', in_='query',
+                                                  description="second angle of celectial coordinates as float, e.g. RA"),
+                                        Parameter(name='direction_type', required=False, type='string', in_='query',
+                                                  description="direction_type of celectial coordinates as string, e.g. J2000"),
+                                        Parameter(name='timestamps', required=False, type='string', in_='query',
+                                                  description="comma-separated list of isoformat timestamps"),
+                                        Parameter(name='stations', required=False, type='string', in_='query',
+                                                  description="comma-separated list of station names"),
+                                        Parameter(name='horizon', required=False, type='string', in_='query',
+                                                  description="Elevation above horizon for which to return rise/set times as float")])
+@api_view(['GET'])
+def get_target_rise_and_set(request):
+    '''
+    returns rise and set times of the given coordinates above the provided horizon, for each given station and timestamp.
+    '''
+    timestamps = request.GET.get('timestamps', None)
+    angle1 = request.GET.get('angle1')
+    angle2 = request.GET.get('angle2')
+    direction_type = request.GET.get("direction_type", "J2000")
+    stations = tuple(request.GET.get('stations', "CS002").split(','))
+    horizon = request.GET.get('horizon', None)
+
+    if angle1 is None or angle2 is None:
+        raise ValueError("Please provide celestial coordinates via 'angle1', 'angle2' (and optionally 'direction_type') properties.")
+
+    if timestamps is None:
+        timestamps = (datetime.utcnow(),)
+    else:
+        timestamps = timestamps.split(',')
+        timestamps = tuple([dateutil.parser.parse(timestamp, ignoretz=True) for timestamp in timestamps])  #  isot to datetime
+
+    if horizon is None:
+        horizon = Angle(0, unit=astropy.units.rad)
+    else:
+        horizon = Angle(horizon, unit=astropy.units.rad)
+
+    # calculate
+    rise_set_dict = coordinates_timestamps_and_stations_to_target_rise_and_set(angle1=angle1, angle2=angle2, direction_type=direction_type, angle_to_horizon=horizon, timestamps=timestamps, stations=stations)
+    return JsonResponse(rise_set_dict)
+
diff --git a/SAS/TMSS/src/tmss/urls.py b/SAS/TMSS/src/tmss/urls.py
index 42c6ac8971fdf9632d1c2de771c1387f8c6845c1..dda3767daec991e0fe14e9f8330270c2249b8ef3 100644
--- a/SAS/TMSS/src/tmss/urls.py
+++ b/SAS/TMSS/src/tmss/urls.py
@@ -71,6 +71,7 @@ urlpatterns = [
     re_path('util/utc/?', views.utc, name="system-utc"),
     re_path('util/lst/?', views.lst, name="conversion-lst"),
     re_path('util/angular_separation_from_bodies/?', views.get_angular_separation_from_bodies, name='get_angular_separation_from_bodies'),
+    re_path('util/target_rise_and_set/?', views.get_target_rise_and_set, name='get_target_rise_and_set'),
 ]
 
 if os.environ.get('SHOW_DJANGO_DEBUG_TOOLBAR', False):
diff --git a/SAS/TMSS/test/t_conversions.py b/SAS/TMSS/test/t_conversions.py
index 18865051aecdd7bd80946e68ef80487e58f8b815..c3e8e4fbcdc1fa33ef08cee7e664d415c71db441 100755
--- a/SAS/TMSS/test/t_conversions.py
+++ b/SAS/TMSS/test/t_conversions.py
@@ -76,6 +76,8 @@ class SiderealTime(unittest.TestCase):
 
 class UtilREST(unittest.TestCase):
 
+    # utc
+
     def test_util_utc_returns_timestamp(self):
 
         # assert local clock differs not too much from returned TMSS system clock
@@ -86,6 +88,8 @@ class UtilREST(unittest.TestCase):
         delta = abs((returned_datetime - current_datetime).total_seconds())
         self.assertTrue(delta < 60.0)
 
+    # lst
+
     def test_util_lst_returns_longitude(self):
 
         # assert returned value is a parseable hms value
@@ -128,6 +132,8 @@ class UtilREST(unittest.TestCase):
         lon_str2 = r2.content.decode('utf8')
         self.assertNotEqual(lon_str1, lon_str2)
 
+    # sun_rise_and_set
+
     def test_util_sun_rise_and_set_returns_json_structure_with_defaults(self):
         r = requests.get(BASE_URL + '/util/sun_rise_and_set', auth=AUTH)
         self.assertEqual(r.status_code, 200)
@@ -196,6 +202,7 @@ class UtilREST(unittest.TestCase):
         response_date = dateutil.parser.parse(r_dict['CS002']['night'][1]['start']).date()
         self.assertEqual(expected_date, response_date)
 
+    # angular_separation_from_bodies
 
     def test_util_angular_separation_from_bodies_yields_error_when_no_pointing_is_given(self):
         r = requests.get(BASE_URL + '/util/angular_separation_from_bodies', auth=AUTH)
@@ -265,6 +272,90 @@ class UtilREST(unittest.TestCase):
                 self.assertNotEqual(angle, angle_last)
             angle_last = angle
 
+    # target_rise_and_set
+
+    def test_util_target_rise_and_set_returns_json_structure_with_defaults(self):
+        r = requests.get(BASE_URL + '/util/target_rise_and_set?angle1=0.5&angle2=0.5', auth=AUTH)
+        self.assertEqual(r.status_code, 200)
+        r_dict = json.loads(r.content.decode('utf-8'))
+
+        # defaults are CS002 and today
+        self.assertIn('CS002', r_dict.keys())
+        rise = dateutil.parser.parse(r_dict['CS002'][0]['rise'])
+        self.assertEqual(datetime.date.today(), rise.date())
+
+    def test_util_target_rise_and_set_considers_stations(self):
+        stations = ['CS005', 'RS305', 'DE609']
+        r = requests.get(BASE_URL + '/util/target_rise_and_set?angle1=0.5&angle2=0.5&stations=%s' % ','.join(stations), auth=AUTH)
+        self.assertEqual(r.status_code, 200)
+        r_dict = json.loads(r.content.decode('utf-8'))
+
+        # assert station is included in response and timestamps differ
+        target_rise_last = None
+        for station in stations:
+            self.assertIn(station, r_dict.keys())
+            target_rise = dateutil.parser.parse(r_dict[station][0]['rise'])
+            if target_rise_last:
+                self.assertNotEqual(target_rise, target_rise_last)
+            target_rise_last = target_rise
+
+    def test_util_target_rise_and_set_considers_timestamps(self):
+        timestamps = ['2020-01-01', '2020-02-22T16-00-00', '2020-3-11', '2020-01-01']
+        r = requests.get(BASE_URL + '/util/target_rise_and_set?angle1=0.5&angle2=0.5&timestamps=%s' % ','.join(timestamps), auth=AUTH)
+        self.assertEqual(r.status_code, 200)
+        r_dict = json.loads(r.content.decode('utf-8'))
+
+        # assert all requested timestamps are included in response and either rise or set are in that day
+        for i in range(len(timestamps)):
+            expected_date = dateutil.parser.parse(timestamps[i]).date()
+            response_rise_date = dateutil.parser.parse(r_dict['CS002'][i]['rise']).date()
+            response_set_date = dateutil.parser.parse(r_dict['CS002'][i]['set']).date()
+            self.assertTrue(expected_date == response_rise_date or expected_date == response_set_date)
+
+    def test_util_target_rise_and_set_returns_correct_date_of_target_rise_and_set(self):
+        timestamps = ['2020-01-01T02-00-00']
+        r = requests.get(BASE_URL + '/util/target_rise_and_set?angle1=0.5&angle2=0.5&timestamps=%s' % ','.join(timestamps), auth=AUTH)
+        self.assertEqual(r.status_code, 200)
+        r_dict = json.loads(r.content.decode('utf-8'))
+
+        # assert day of timestamp matches day of returned rise
+        expected_date = dateutil.parser.parse(timestamps[0]).date()
+        target_rise = dateutil.parser.parse(r_dict['CS002'][0]['rise'])
+        target_set = dateutil.parser.parse(r_dict['CS002'][0]['set'])
+        self.assertTrue(expected_date == target_rise.date() or expected_date == target_set.date())
+
+        # assert set time falls in the 24h after rise time
+        self.assertTrue(target_set - target_rise > datetime.timedelta(0) and target_set - target_rise < datetime.timedelta(days=1))
+
+    def test_util_target_rise_and_set_considers_coordinates(self):
+        test_coords = [(0.5, 0.5, "J2000"), (0.6, 0.5, "J2000"), (0.6, 0.6, "J2000")]
+        for coords in test_coords:
+            r = requests.get(BASE_URL + '/util/target_rise_and_set?angle1=%s&angle2=%s&direction_type=%s' % coords, auth=AUTH)
+            self.assertEqual(r.status_code, 200)
+            r_dict = json.loads(r.content.decode('utf-8'))
+
+            # assert all requested coordinates yield a response and times differ
+            rise_last = None
+            rise = r_dict['CS002'][0]['rise']
+            if rise_last:
+                self.assertNotEqual(rise, rise_last)
+            rise_last = rise
+
+    def test_util_target_rise_and_set_considers_horizon(self):
+        test_horizons = [0.1, 0.2, 0.3]
+        for horizon in test_horizons:
+            r = requests.get(BASE_URL + '/util/target_rise_and_set?angle1=0.5&angle2=0.5&horizon=%s' % horizon, auth=AUTH)
+            self.assertEqual(r.status_code, 200)
+            r_dict = json.loads(r.content.decode('utf-8'))
+
+            # assert all requested horizons yield a response and times differ
+            rise_last = None
+            rise = r_dict['CS002'][0]['rise']
+            if rise_last:
+                self.assertNotEqual(rise, rise_last)
+            rise_last = rise
+
+
 if __name__ == "__main__":
     os.environ['TZ'] = 'UTC'
     unittest.main()