diff --git a/src/api/schedule.py b/src/api/schedule.py
index 18beabb0becae360752acf2a7e08091d358313f7..f1acb9e14ba60abaa918d9c30af4f69c39fa3337 100644
--- a/src/api/schedule.py
+++ b/src/api/schedule.py
@@ -33,20 +33,21 @@ class ScheduleEncoder(json.JSONEncoder):
     tz = None
 
     def encode_event(self, event, tz=None):
-        start = event.schedule_start.astimezone(tz or self.tz)
-        duration = event.schedule_duration.seconds
+        start = event.schedule_start.astimezone(tz or self.tz) if event.schedule_start is not None else None
+        duration = event.schedule_duration.seconds if event.schedule_start is not None else None
+        additional_data = event.additional_data or {}
 
         return OrderedDict({
             **event_template,
-            'id': event.additional_data.get('id') or int(re.sub('[^0-9]+', '', event.id)[0:6]),
+            'id': additional_data.get('id') or int(re.sub('[^0-9]+', '', str(event.id))[0:6]),
             'description': event.description,  # TODO: if the description also exists in additional_data it is overwritten due to concatination with abstract
-            'slug': event.slug, 
-            **(event.additional_data or {}),
+            'slug': event.slug,
+            **additional_data,
             'guid': event.id,
-            'date': start.isoformat(),
-            'start': start.strftime('%H:%M'),
-            'duration': '%d:%02d' % (duration/3600, duration % 3600/60),
-            'room': event.room.name,
+            'date': start.isoformat() if start is not None else None,
+            'start': start.strftime('%H:%M') if start is not None else None,
+            'duration': ('%d:%02d' % (duration / 3600, duration % 3600 / 60)) if duration is not None else None,
+            'room': event.room.name if event.room is not None else None,
             'title': event.name,
             'language': event.language,
             'track': event.track.name if event.track else None,
diff --git a/src/api/tests/__init__.py b/src/api/tests/__init__.py
index 63b4d921c6f34af9b38bc3ffc46446c07af9df8d..82daa5fb6d222d49d90eab1eb749750a57920fe0 100644
--- a/src/api/tests/__init__.py
+++ b/src/api/tests/__init__.py
@@ -1,4 +1,6 @@
+from .bbb import *  # noqa: F401, F403
 from .engelsystem import *  # noqa: F401, F403
+from .schedule import *  # noqa: F401, F403
 from .workadventure import *  # noqa: F401, F403
 
 __all__ = '*'
diff --git a/src/api/tests/schedule.py b/src/api/tests/schedule.py
new file mode 100644
index 0000000000000000000000000000000000000000..afeea7f0ae3db02b2e122f370fd7ca0e74a82b79
--- /dev/null
+++ b/src/api/tests/schedule.py
@@ -0,0 +1,61 @@
+import json
+
+from django.test import TestCase
+from django.urls import reverse
+from rest_framework.authtoken.models import Token
+
+from core.models import Assembly, Conference, Event, PlatformUser, Room
+
+
+class ScheduleTest(TestCase):
+    def test_push_event(self):
+        conf = Conference(slug='conf', name='TestConf', is_public=True)
+        conf.save()
+        conf.tracks.create(name='Community').save()
+        assembly = Assembly(name='TestAssembly', slug='asmbly', conference=conf)
+        assembly.save()
+        room = Room(conference=conf, assembly=assembly, name='Foo Room', room_type=Room.RoomType.STAGE)
+        room.save()
+
+        user = PlatformUser(username='bernd', is_active=True)
+        user.save()
+
+        token = Token(user=user)
+        token.save()
+
+        event = Event(conference=conf, assembly=assembly, name='Example Event')
+        event.save()
+
+        update = {
+            "url": "https://fahrplan.events.ccc.de/rc3/2020/Fahrplan/events/11583.html",
+            "id": 11583,
+            "guid": str(event.id),
+            "logo": None,
+            "date": "2020-12-27T12:20:00+01:00",
+            "start": "12:20",
+            "duration": "00:30",
+            "room": "foo room",
+            "slug": "rc3-11583-rc3_eroffnung",
+            "title": "#rC3 Er\u00f6ffnung",
+            "subtitle": "",
+            "track": "Community",
+            "type": "Talk",
+            "language": "de",
+            "abstract": "Willkommen zur ersten und hoffentlich einzigen Remote Chaos Experience!",
+            "description": "",
+            "recording_license": "",
+            "do_not_record": False,
+            "persons": [{"id": 14151, "public_name": "blubbel"}],
+            "links": [],
+            "attachments": []
+        }
+
+        url = reverse('api:event-schedule', kwargs={'conference': conf.slug, 'pk': event.pk})
+
+        with self.modify_settings(API_USERS={'append': user.username}):
+            resp = self.client.post(url, json.dumps(update), content_type='application/json', HTTP_AUTHORIZATION=f'Token {token.key}')
+
+        self.assertEqual(201, resp.status_code, f'Unexpected result from POST: {resp.content}')
+
+        event.refresh_from_db()
+        self.assertTrue('rC3' in event.name, f'Expected "rC3" in event name "{event.name}".')
diff --git a/src/api/views/schedule.py b/src/api/views/schedule.py
index 117d8f04f365af6d1d21da499ac4f1720038f22e..d4de2532563e4f6d58a31a7e878f6827251772dc 100644
--- a/src/api/views/schedule.py
+++ b/src/api/views/schedule.py
@@ -1,18 +1,20 @@
-import json
+from datetime import timedelta
 import logging
 
-from django.conf import settings
 from django.db.models import F
 from django.http import JsonResponse, HttpResponse
+from django.utils.dateparse import parse_datetime
 from django.views.generic import View
+from rest_framework import authentication
 from rest_framework.response import Response
+from rest_framework.views import APIView
 import pytz
 
 from core.models.events import Event
 from core.models.rooms import Room
 from core.models.conference import ConferenceTrack
 
-
+from ..permissions import IsApiUserOrReadOnly
 from ..schedule import Schedule, ScheduleEncoder
 from .mixins import ConferenceSlugMixin
 
@@ -63,50 +65,76 @@ class RoomSchedule(ConferenceSlugMixin, View):
         return schedule_response(schedule, kwargs)
 
 
-class EventSchedule(ConferenceSlugMixin, View):
-    def get(self, *args, **kwargs):
+def schedulexml_time_to_timedelta(s):
+    if ':' in s:
+        hours, minutes = s.split(':')
+    else:
+        hours, minutes = 0, s
+
+    timedelta(hours=int(hours), minutes=int(minutes))
+
+
+class EventSchedule(ConferenceSlugMixin, APIView):
+    authentication_classes = [authentication.TokenAuthentication]
+    permission_classes = [IsApiUserOrReadOnly]
+
+    def get(self, request, conference, pk, format=None, **kwargs):
         tz = pytz.timezone(self.conference.timezone)
         event = Event.objects \
             .accessible_by_user(conference=self.conference, user=self.request.user) \
-            .get(pk=kwargs.get('pk'))
-        return Response(ScheduleEncoder.encode_event(event, tz))
+            .get(pk=pk)
+        return Response(ScheduleEncoder().encode_event(event, tz))
 
-    def post(self, *args, **kwargs):
-        if not self.request.user.is_authenticated and self.request.user.username in settings.API_USERS:
-            return HttpResponse(status_code=401)
+    def post(self, request, conference, pk, format=None, **kwargs):
+        event = request.data
+        if len(event) == 0:
+            return Response({'error': 'No data.'}, status=400)
 
-        event = json.loads(self.request.body)
+        try:
+            obj = Event.objects.get(conference=self.conference, pk=pk)
+        except Event.DoesNotExist:
+            obj = Event(conference=self.conference)
 
-        obj, created = Event.objects \
-            .accessible_by_user(conference=self.conference, user=self.request.user) \
-            .get_or_create(pk=event['guid'])
+        try:
+            if 'guid' in event:
+                if event['guid'] != str(obj.pk):
+                    print(f'Attempted update of event {obj.pk} with guid "{event["guid"]}".')
+                    logger.warning(f'Attempted update of event {obj.pk} with guid "{event["guid"]}".')
+                    return JsonResponse({'error': 'GUID mismatch.'})
 
-        if created:
-            obj.conference = self.conference
+            if 'slug' in event:
+                obj.slug = event['slug']
 
-        try:
-            obj.slug = event['slug']
             obj.room = Room.objects.get(conference=self.conference, name__iexact=event['room'])
-            obj.kind = 'assembly' if not(obj.room.assembly.is_official) else 'official'
-            obj.name = event['title']
-            obj.language = event['language']
-            obj.description = str(event['abstract']) + "\n\n" + str(event['description'])
+            obj.assembly = obj.room.assembly
+            obj.kind = 'assembly' if not obj.room.assembly.is_official else 'official'
             obj.is_public = True
-            obj.schedule_start = event['date'],
-            obj.schedule_duration = event['duration'] + ':00',
-            obj.track = ConferenceTrack.objects.get(conference=self.conference, name__iexact=event['track']),
+
+            if 'title' in event:
+                obj.name = event['title']
+
+            if 'language' in event:
+                obj.language = event['language']
+
+            obj.description = str(event['abstract']) + "\n\n" + str(event['description'])
+
+            obj.schedule_start = parse_datetime(event['date'])
+            obj.schedule_duration = schedulexml_time_to_timedelta(event['duration'])
+
+            obj.track = ConferenceTrack.objects.get(conference=self.conference, name__iexact=event['track'])
+
             obj.additional_data = filter_additional_data(event)
 
         except Room.DoesNotExist:
-            return JsonResponse({'error': 'Room {} does not exist'.format(event['room'])}, status_code=400)
+            return Response({'error': 'Room {} does not exist'.format(event['room'])}, status=400)
 
         except ConferenceTrack.DoesNotExist:
-            return JsonResponse({'error': 'Track {} does not exist'.format(event['track'])}, status_code=400)
+            return Response({'error': 'Track {} does not exist'.format(event['track'])}, status=400)
 
         obj.save()
-        logger.info('Event %s updated via POST by %s', obj, self.request.user)
+        logger.info('Event %s updated via POST by %s', obj, request.user)
 
-        return HttpResponse(status_code=201)
+        return HttpResponse(status=201)
 
 
 def filter_additional_data(data):
diff --git a/src/rc3platform/settings/base.py b/src/rc3platform/settings/base.py
index 6566f013a5807f0085b6325df81b6120f7cd71ab..4711ef80478f7526688f64860bfee35eee8cc18e 100644
--- a/src/rc3platform/settings/base.py
+++ b/src/rc3platform/settings/base.py
@@ -193,6 +193,9 @@ FORBIDDEN_ASSEMBLY_SLUGS = ['visit', 'maps', 'api', 'pusher']
 MAIL_REPLY_TO = []
 SUPPORT_HTML_MAILS = False
 
+# API access
+API_USERS = []
+
 # SSO
 SSO_COOKIE_NAME = 'SSO_TOKEN'
 SSO_COOKIE_DOMAIN = '.localhost'