diff --git a/program/models.py b/program/models.py
index 0b634d0c51b848ebe319dd0113d4de35909c66a8..ac7677b65c830e680e80c91c0dc23277d8962d4d 100644
--- a/program/models.py
+++ b/program/models.py
@@ -41,6 +41,12 @@ from steering.settings import (
 )
 
 
+class ScheduleConflictError(ValidationError):
+    def __init__(self, *args, conflicts=None, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.conflicts = conflicts
+
+
 class Type(models.Model):
     name = models.CharField(max_length=32)
     slug = models.SlugField(max_length=32, unique=True)
@@ -323,25 +329,25 @@ class Schedule(models.Model):
             True if sdl.get("add_business_days_only") is True else False
         )
 
-        # TODO: replace `dstart` with `first_date` when the dashboard is updated
-        first_date = parse_date(str(sdl["dstart"]))
-        # TODO: replace `tstart` with `start_time` when the dashboard is updated
+        first_date = parse_date(str(sdl["first_date"]))
         start_time = (
-            sdl["tstart"] + ":00" if len(str(sdl["tstart"])) == 5 else sdl["tstart"]
+            sdl["start_time"] + ":00"
+            if len(str(sdl["start_time"])) == 5
+            else sdl["start_time"]
+        )
+        end_time = (
+            sdl["end_time"] + ":00"
+            if len(str(sdl["end_time"])) == 5
+            else sdl["end_time"]
         )
-        # TODO: replace `tend` with `end_time` when the dashboard is updated
-        end_time = sdl["tend"] + ":00" if len(str(sdl["tend"])) == 5 else sdl["tend"]
 
         start_time = parse_time(str(start_time))
         end_time = parse_time(str(end_time))
 
-        # TODO: replace `until` with `last_date` when the dashboard is updated
-        if sdl["until"]:
-            last_date = parse_date(str(sdl["until"]))
+        if sdl["last_date"]:
+            last_date = parse_date(str(sdl["last_date"]))
         else:
-            # If no until date was set
-            # Set it to the end of the year
-            # Or add x days
+            # If last_date was not set, set it to the end of the year or add x days
             if AUTO_SET_UNTIL_DATE_TO_END_OF_YEAR:
                 year = timezone.now().year
                 last_date = parse_date(f"{year}-12-31")
@@ -350,10 +356,9 @@ class Schedule(models.Model):
                     days=+AUTO_SET_UNTIL_DATE_TO_DAYS_IN_FUTURE
                 )
 
-        # TODO: replace `byweekday` with `by_weekday` when the dashboard is updated
         schedule = Schedule(
             pk=pk,
-            by_weekday=sdl["byweekday"],
+            by_weekday=sdl["by_weekday"],
             rrule=rrule,
             first_date=first_date,
             start_time=start_time,
@@ -373,7 +378,7 @@ class Schedule(models.Model):
     def generate_timeslots(schedule):
         """
         Returns a list of timeslot objects based on a schedule and its rrule
-        Returns past timeslots as well, starting from dstart (not today)
+        Returns past timeslots as well, starting from first_date (not today)
         """
 
         by_week_no = None
@@ -696,7 +701,7 @@ class Schedule(models.Model):
         # Generate schedule to be saved
         schedule = Schedule.instantiate_upcoming(sdl, show_pk, schedule_pk)
 
-        # Copy if dstart changes for generating timeslots
+        # Copy if first_date changes for generating timeslots
         gen_schedule = schedule
 
         # Generate timeslots
@@ -733,7 +738,7 @@ class Schedule(models.Model):
         """
 
         sdl = data["schedule"]
-        solutions = data["solutions"]
+        solutions = data.get("solutions", [])
 
         # Regenerate conflicts
         schedule = Schedule.instantiate_upcoming(sdl, show_pk, schedule_pk)
@@ -742,13 +747,13 @@ class Schedule(models.Model):
 
         if schedule.rrule.freq > 0 and schedule.first_date == schedule.last_date:
             raise ValidationError(
-                _("Start and until dates mustn't be the same"),
+                _("Start and end dates must not be the same."),
                 code="no-same-day-start-and-end",
             )
 
         if schedule.last_date < schedule.first_date:
             raise ValidationError(
-                _("Until date mustn't be before start"),
+                _("End date mustn't be before start."),
                 code="no-start-after-end",
             )
 
@@ -757,9 +762,10 @@ class Schedule(models.Model):
         )
 
         if len(solutions) != num_conflicts:
-            raise ValidationError(
+            raise ScheduleConflictError(
                 _("Numbers of conflicts and solutions don't match."),
                 code="one-solution-per-conflict",
+                conflicts=conflicts,
             )
 
         # Projected timeslots to create
@@ -964,7 +970,11 @@ class Schedule(models.Model):
             conflicts["notes"] = data.get("notes")
             conflicts["playlists"] = data.get("playlists")
 
-            return conflicts
+            raise ScheduleConflictError(
+                _("Not all conflicts have been resolved."),
+                code="unresolved-conflicts",
+                conflicts=conflicts,
+            )
 
         # Collect upcoming timeslots to delete which might still remain
         del_timeslots = TimeSlot.objects.filter(
diff --git a/program/serializers.py b/program/serializers.py
index ee5488be3cac05a12c20d6893e753555247b2a37..3ef7853fe0607197e3d206588af14f90e83b9436 100644
--- a/program/serializers.py
+++ b/program/serializers.py
@@ -36,7 +36,6 @@ from program.models import (
     MusicFocus,
     Note,
     NoteLink,
-    RRule,
     Schedule,
     Show,
     TimeSlot,
@@ -405,35 +404,16 @@ class ShowSerializer(serializers.HyperlinkedModelSerializer):
 
 
 class ScheduleSerializer(serializers.ModelSerializer):
-    rrule = serializers.PrimaryKeyRelatedField(
-        queryset=RRule.objects.all(),
-        help_text=Schedule.rrule.field.help_text,
-    )
-    show = serializers.PrimaryKeyRelatedField(
-        queryset=Show.objects.all(),
-        help_text=Schedule.show.field.help_text,
-    )
-    # TODO: remove this when the dashboard is updated
-    byweekday = serializers.IntegerField(
-        source="by_weekday",
-        help_text=Schedule.by_weekday.field.help_text,
-    )
-    dstart = serializers.DateField(
-        source="first_date",
-        help_text=Schedule.first_date.field.help_text,
-    )
-    tstart = serializers.TimeField(
-        source="start_time",
-        help_text=Schedule.start_time.field.help_text,
-    )
-    tend = serializers.TimeField(
-        source="end_time",
-        help_text=Schedule.end_time.field.help_text,
-    )
-    until = serializers.DateField(
-        source="last_date",
-        help_text=Schedule.last_date.field.help_text,
-    )
+    class Meta:
+        model = Schedule
+        fields = "__all__"
+
+
+class UnsavedScheduleSerializer(ScheduleSerializer):
+    id = serializers.IntegerField(allow_null=True)
+
+
+class ScheduleInRequestSerializer(ScheduleSerializer):
     dryrun = serializers.BooleanField(
         write_only=True,
         required=False,
@@ -464,11 +444,11 @@ class ScheduleSerializer(serializers.ModelSerializer):
     def update(self, instance, validated_data):
         """Update and return an existing Schedule instance, given the validated data."""
 
-        instance.by_weekday = validated_data.get("byweekday", instance.by_weekday)
-        instance.first_date = validated_data.get("dstart", instance.first_date)
-        instance.start_time = validated_data.get("tstart", instance.start_time)
-        instance.end_time = validated_data.get("tend", instance.end_time)
-        instance.last_date = validated_data.get("until", instance.last_date)
+        instance.by_weekday = validated_data.get("by_weekday", instance.by_weekday)
+        instance.first_date = validated_data.get("first_date", instance.first_date)
+        instance.start_time = validated_data.get("start_time", instance.start_time)
+        instance.end_time = validated_data.get("end_time", instance.end_time)
+        instance.last_date = validated_data.get("last_date", instance.last_date)
         instance.is_repetition = validated_data.get(
             "is_repetition", instance.is_repetition
         )
@@ -525,41 +505,24 @@ class DryRunTimeSlotSerializer(serializers.Serializer):
 
 
 class ScheduleCreateUpdateRequestSerializer(serializers.Serializer):
-    schedule = ScheduleSerializer()
-    solutions = serializers.DictField(child=serializers.ChoiceField(SOLUTION_CHOICES))
+    schedule = ScheduleInRequestSerializer()
+    solutions = serializers.DictField(
+        child=serializers.ChoiceField(SOLUTION_CHOICES), required=False
+    )
     notes = serializers.DictField(child=serializers.IntegerField(), required=False)
     playlists = serializers.DictField(child=serializers.IntegerField(), required=False)
 
 
-# TODO: There shouldn’t be a separate ScheduleSerializer for use in responses.
-#       Instead the default serializer should be used. Unfortunately, the
-#       code that generates the data creates custom dicts with this particular format.
-class ScheduleInResponseSerializer(serializers.Serializer):
-    # "Schedule schema type" is the rendered name of the ScheduleSerializer.
-    """
-    For documentation on the individual fields see the
-    Schedule schema type.
-    """
-    add_business_days_only = serializers.BooleanField()
-    add_days_no = serializers.IntegerField(allow_null=True)
-    by_weekday = serializers.IntegerField()
-    default_playlist_id = serializers.IntegerField(allow_null=True)
-    end_time = serializers.TimeField()
-    first_date = serializers.DateField()
-    id = serializers.PrimaryKeyRelatedField(queryset=Schedule.objects.all())
-    is_repetition = serializers.BooleanField()
-    last_date = serializers.DateField()
-    rrule = serializers.PrimaryKeyRelatedField(queryset=RRule.objects.all())
-    show = serializers.PrimaryKeyRelatedField(queryset=Note.objects.all())
-    start_time = serializers.TimeField()
-
-
-class ScheduleConflictResponseSerializer(serializers.Serializer):
+class ScheduleResponseSerializer(serializers.Serializer):
     projected = ProjectedTimeSlotSerializer(many=True)
     solutions = serializers.DictField(child=serializers.ChoiceField(SOLUTION_CHOICES))
     notes = serializers.DictField(child=serializers.IntegerField())
     playlists = serializers.DictField(child=serializers.IntegerField())
-    schedule = ScheduleInResponseSerializer()
+    schedule = ScheduleSerializer()
+
+
+class ScheduleConflictResponseSerializer(ScheduleResponseSerializer):
+    schedule = UnsavedScheduleSerializer()
 
 
 class ScheduleDryRunResponseSerializer(serializers.Serializer):
diff --git a/program/views.py b/program/views.py
index e0f7b4d893f9c8d1ec21198f1b0b2f60441df75a..acd66207de9ee0a73d6ae0430b9536917adbf7ff 100644
--- a/program/views.py
+++ b/program/views.py
@@ -42,6 +42,7 @@ from program.models import (
     MusicFocus,
     Note,
     Schedule,
+    ScheduleConflictError,
     Show,
     TimeSlot,
     Topic,
@@ -58,6 +59,7 @@ from program.serializers import (
     ScheduleConflictResponseSerializer,
     ScheduleCreateUpdateRequestSerializer,
     ScheduleDryRunResponseSerializer,
+    ScheduleResponseSerializer,
     ScheduleSerializer,
     ShowSerializer,
     TimeSlotSerializer,
@@ -344,9 +346,10 @@ class APIShowViewSet(DisabledObjectPermissionCheckMixin, viewsets.ModelViewSet):
 @extend_schema_view(
     create=extend_schema(
         summary="Create a new schedule.",
+        request=ScheduleCreateUpdateRequestSerializer,
         responses={
             status.HTTP_201_CREATED: OpenApiResponse(
-                response=ScheduleConflictResponseSerializer,
+                response=ScheduleResponseSerializer,
                 description=(
                     "Signals the successful creation of the schedule and of the projected "
                     "timeslots."
@@ -366,9 +369,9 @@ class APIShowViewSet(DisabledObjectPermissionCheckMixin, viewsets.ModelViewSet):
                     Returned in case the request contained invalid data.
 
                     This may happen if:
-                    * the until date is before the start date (`no-start-after-end`),
+                    * the last date is before the start date (`no-start-after-end`),
                       in which case you should correct either the start or until date.
-                    * The start and until date are the same (`no-same-day-start-and-end`).
+                    * The start and last date are the same (`no-same-day-start-and-end`).
                       This is only allowed for single timeslots with the recurrence rule
                       set to `once`. You should fix either the start or until date.
                     * The number of conflicts and solutions aren’t the same
@@ -410,8 +413,14 @@ class APIShowViewSet(DisabledObjectPermissionCheckMixin, viewsets.ModelViewSet):
         },
     ),
     retrieve=extend_schema(summary="Retrieve a single schedule."),
-    update=extend_schema(summary="Update an existing schedule."),
-    partial_update=extend_schema(summary="Partially update an existing schedule."),
+    update=extend_schema(
+        summary="Update an existing schedule.",
+        request=ScheduleCreateUpdateRequestSerializer,
+    ),
+    partial_update=extend_schema(
+        summary="Partially update an existing schedule.",
+        request=ScheduleCreateUpdateRequestSerializer,
+    ),
     destroy=extend_schema(summary="Delete an existing schedule."),
     list=extend_schema(summary="List all schedules."),
 )
@@ -475,28 +484,16 @@ class APIScheduleViewSet(
         if show_pk is None or "schedule" not in request.data:
             return Response(status=status.HTTP_400_BAD_REQUEST)
 
-        # First create submit -> return projected timeslots and collisions
-        # TODO: Perhaps directly insert into database if no conflicts found
-        if "solutions" not in request.data:
-            # TODO: respond with status.HTTP_409_CONFLICT when the dashboard can handle it
-            return Response(
-                Schedule.make_conflicts(request.data["schedule"], pk, show_pk),
-            )
-
-        # Otherwise try to resolve
-        resolution = Schedule.resolve_conflicts(request.data, pk, show_pk)
+        try:
+            resolution = Schedule.resolve_conflicts(request.data, pk, show_pk)
+        except ScheduleConflictError as exc:
+            return Response(exc.conflicts, status.HTTP_409_CONFLICT)
 
         if all(key in resolution for key in ["create", "update", "delete"]):
             # this is a dry-run
             return Response(resolution, status=status.HTTP_202_ACCEPTED)
 
-        # If resolution went well
-        if "projected" not in resolution:
-            return Response(resolution, status=status.HTTP_201_CREATED)
-
-        # Otherwise return conflicts
-        # TODO: respond with status.HTTP_409_CONFLICT when the dashboard can handle it
-        return Response(resolution)
+        return Response(resolution, status=status.HTTP_201_CREATED)
 
     def update(self, request, *args, **kwargs):
         """
@@ -532,26 +529,13 @@ class APIScheduleViewSet(
             serializer = ScheduleSerializer(schedule)
             return Response(serializer.data)
 
-        # First update submit -> return projected timeslots and collisions
-        if "solutions" not in request.data:
-            # TODO: respond with status.HTTP_409_CONFLICT when the dashboard can handle it
-            return Response(
-                Schedule.make_conflicts(
-                    request.data["schedule"], schedule.pk, schedule.show.pk
-                )
+        try:
+            resolution = Schedule.resolve_conflicts(
+                request.data, schedule.pk, schedule.show.pk
             )
+        except ScheduleConflictError as exc:
+            return Response(exc.conflicts, status.HTTP_409_CONFLICT)
 
-        # Otherwise try to resolve
-        resolution = Schedule.resolve_conflicts(
-            request.data, schedule.pk, schedule.show.pk
-        )
-
-        # If resolution went well
-        if "projected" not in resolution:
-            return Response(resolution)
-
-        # Otherwise return conflicts
-        # TODO: respond with status.HTTP_409_CONFLICT when the dashboard can handle it
         return Response(resolution)
 
     def destroy(self, request, *args, **kwargs):