diff --git a/program/views.py b/program/views.py
index 1a064d9ab5a45ccf85134d075ca64c2a7e2448bc..01245a7fc6d5382f37fef288d64042b13815af41 100644
--- a/program/views.py
+++ b/program/views.py
@@ -171,68 +171,74 @@ def json_day_schedule(request, year=None, month=None, day=None):
     )
 
 
-def json_playout(request):
-    """
-    Return a JSON representation of the scheduled playout.
+class APIPlayoutViewSet(
+    mixins.ListModelMixin,
+    viewsets.GenericViewSet,
+):
+    queryset = TimeSlot.objects.all()
 
-    Expects GET parameters `start` (date), `end` (date), and `includeVirtual` (boolean).
+    def list(self, request, *args, **kwargs):
+        """
+        Return a JSON representation of the scheduled playout.
 
-    - `start` is today by default.
-    - `end` is one week after the start date by default.
-    - `includeVirtual` is false by default.
+        Expects GET parameters `start` (date), `end` (date), and `includeVirtual` (boolean).
 
-    The schedule will include virtual timeslots to fill unscheduled gaps if requested.
+        - `start` is today by default.
+        - `end` is one week after the start date by default.
+        - `includeVirtual` is false by default.
 
-    Called by
-    - engine (playout) to retrieve timeslots within a given timerange
-    - internal calendar to retrieve all timeslots for a week
-    """
+        The schedule will include virtual timeslots to fill unscheduled gaps if requested.
 
-    # datetime.now and datetime.combine return timezone naive datetime objects
-    if request.GET.get("start") is None:
-        schedule_start = timezone.make_aware(datetime.combine(datetime.now(), time(0, 0)))
-    else:
-        schedule_start = timezone.make_aware(
-            datetime.combine(parse_date(request.GET.get("start")), time(0, 0))
-        )
+        Called by
+        - engine (playout) to retrieve timeslots within a given timerange
+        - internal calendar to retrieve all timeslots for a week
+        """
 
-    if request.GET.get("end") is None:
-        schedule_end = schedule_start + timedelta(days=7)
-    else:
-        schedule_end = timezone.make_aware(
-            datetime.combine(parse_date(request.GET.get("end")) + timedelta(days=1), time(0, 0))
-        )
+        # datetime.now and datetime.combine return timezone naive datetime objects
+        if request.GET.get("start") is None:
+            schedule_start = timezone.make_aware(datetime.combine(datetime.now(), time(0, 0)))
+        else:
+            schedule_start = timezone.make_aware(
+                datetime.combine(parse_date(request.GET.get("start")), time(0, 0))
+            )
 
-    include_virtual = request.GET.get("include_virtual") == "true"
+        if request.GET.get("end") is None:
+            schedule_end = schedule_start + timedelta(days=7)
+        else:
+            schedule_end = timezone.make_aware(
+                datetime.combine(parse_date(request.GET.get("end")) + timedelta(days=1), time(0, 0))
+            )
 
-    timeslots = get_timerange_timeslots(schedule_start, schedule_end).select_related("schedule")
+        include_virtual = request.GET.get("include_virtual") == "true"
 
-    schedule = []
+        timeslots = get_timerange_timeslots(schedule_start, schedule_end).select_related("schedule")
 
-    first_timeslot = timeslots.first()
+        schedule = []
 
-    if include_virtual and first_timeslot.start > schedule_start:
-        schedule.append(gap_entry(gap_start=schedule_start, gap_end=first_timeslot.start))
+        first_timeslot = timeslots.first()
 
-    for current, upcoming in pairwise(timeslots):
-        schedule.append(timeslot_entry(timeslot=current))
+        if include_virtual and first_timeslot.start > schedule_start:
+            schedule.append(gap_entry(gap_start=schedule_start, gap_end=first_timeslot.start))
 
-        if include_virtual and current.end != upcoming.start:
-            schedule.append(gap_entry(gap_start=current.end, gap_end=upcoming.start))
+        for current, upcoming in pairwise(timeslots):
+            schedule.append(timeslot_entry(timeslot=current))
 
-    last_timeslot = timeslots.last()
+            if include_virtual and current.end != upcoming.start:
+                schedule.append(gap_entry(gap_start=current.end, gap_end=upcoming.start))
 
-    # we need to append the last timeslot to the schedule to complete it
-    if last_timeslot:
-        schedule.append(timeslot_entry(timeslot=last_timeslot))
+        last_timeslot = timeslots.last()
 
-    if include_virtual and last_timeslot.end < schedule_end:
-        schedule.append(gap_entry(gap_start=last_timeslot.end, gap_end=schedule_end))
+        # we need to append the last timeslot to the schedule to complete it
+        if last_timeslot:
+            schedule.append(timeslot_entry(timeslot=last_timeslot))
 
-    return HttpResponse(
-        json.dumps(schedule, ensure_ascii=False).encode("utf8"),
-        content_type="application/json; charset=utf-8",
-    )
+        if include_virtual and last_timeslot.end < schedule_end:
+            schedule.append(gap_entry(gap_start=last_timeslot.end, gap_end=schedule_end))
+
+        return HttpResponse(
+            json.dumps(schedule, ensure_ascii=False).encode("utf8"),
+            content_type="application/json; charset=utf-8",
+        )
 
 
 @extend_schema_view(
diff --git a/steering/urls.py b/steering/urls.py
index 34403ea166b93b63f24fb416e482d297be8408c9..aa1ca7e693b77f9a4bb4bb12bf90dd5e88586cdb 100644
--- a/steering/urls.py
+++ b/steering/urls.py
@@ -44,7 +44,7 @@ from program.views import (
     APITypeViewSet,
     APIUserViewSet,
     json_day_schedule,
-    json_playout,
+    APIPlayoutViewSet
 )
 
 admin.autodiscover()
@@ -67,12 +67,12 @@ router.register(r"link-types", APILinkTypeViewSet, basename="link-type")
 router.register(r"rrules", APIRRuleViewSet, basename="rrule")
 router.register(r"images", APIImageViewSet, basename="image")
 router.register(r"settings", APIRadioSettingsViewSet, basename="settings")
+router.register(r"playout", APIPlayoutViewSet, basename="playout")
+router.register(r"program/week", APIPlayoutViewSet, basename="program/week")
 
 urlpatterns = [
     path("openid/", include("oidc_provider.urls", namespace="oidc_provider")),
     path("api/v1/", include(router.urls)),
-    path("api/v1/playout", json_playout),
-    path("api/v1/program/week", json_playout),
     path("api/v1/program/<int:year>/<int:month>/<int:day>/", json_day_schedule),
     path("api/v1/schema/", SpectacularAPIView.as_view(), name="schema"),
     path(