diff --git a/src/api/serializers.py b/src/api/serializers.py
index e0341bd899ad99a43ac6e8fe8549e497d41c0611..2ac02e23e81d31d04572c295787c421cc36da605 100644
--- a/src/api/serializers.py
+++ b/src/api/serializers.py
@@ -78,14 +78,20 @@ class HubModelSerializer(ValidatingModelSerializer):
             staff_only_fields = None
 
         # now check the user's conference membership based on the request
-        self.request_user = request_user = self.context['request'].user
-        self.conference_member = None
-        if request_user.is_authenticated:
-            with contextlib.suppress(ConferenceMember.DoesNotExist):
-                self.conference_member = ConferenceMember.objects.select_related('conference').get(
-                    conference=self.conference,
-                    user=request_user,
-                )
+        if 'request' not in self.context:
+            from django.contrib.auth.models import AnonymousUser
+
+            self.request_user = request_user = AnonymousUser()
+            self.conference_member = None
+        else:
+            self.request_user = request_user = self.context['request'].user
+            self.conference_member = None
+            if request_user.is_authenticated:
+                with contextlib.suppress(ConferenceMember.DoesNotExist):
+                    self.conference_member = ConferenceMember.objects.select_related('conference').get(
+                        conference=self.conference,
+                        user=request_user,
+                    )
 
         # store if the request's user has staff permissions in the conference (either direct or globally)
         self.is_staff = request_user.is_superuser or request_user.is_staff or (self.conference_member is not None and self.conference_member.is_staff)
@@ -307,6 +313,14 @@ class PlatformUserVisibleSerializer(serializers.ModelSerializer):
         read_only_fields = fields
 
 
+class PlatformUserByUsernameFieldSerializer(serializers.SlugRelatedField):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, slug_field='username', **kwargs)
+
+    def get_queryset(self):
+        return PlatformUser.objects.all()
+
+
 class DirectMessageSerializerSentShort(HubModelSerializer):
     class Meta:
         model = DirectMessage
@@ -345,3 +359,13 @@ class DirectMessageSerializerReceived(HubModelSerializer):
 
     sender = PlatformUserVisibleSerializer(read_only=True)
     recipient = PlatformUserVisibleSerializer(read_only=True)
+
+
+class DirectMessageSendSerializer(HubModelSerializer):
+    class Meta:
+        model = DirectMessage
+        fields = ['id', 'recipient', 'subject', 'body']
+        read_only_fields = ['id']
+        write_only_fields = [f for f in fields if f != 'id']
+
+    recipient = PlatformUserByUsernameFieldSerializer()
diff --git a/src/api/tests/messages.py b/src/api/tests/messages.py
index b80b0e9f64a3275ee6a71471de3f4f2ed875460e..77b9649dce27d2a15509435bb31c341290696783 100644
--- a/src/api/tests/messages.py
+++ b/src/api/tests/messages.py
@@ -1,6 +1,7 @@
 import uuid
 from datetime import datetime
 
+from freezegun import freeze_time
 from pytz import UTC
 
 from django.test import Client, TestCase, override_settings
@@ -259,3 +260,61 @@ class MessagesTestCase(TestCase):
         dm.save()
         resp = c.get(reverse('api:my-message-sent', kwargs={'pk': str(other_dm.pk)}))
         self.assertEqual(resp.status_code, 404)
+
+    def test_DirectMessagesSend(self):
+        c = Client()
+        resp = c.post(reverse('api:my-send-message'))
+        self.assertEqual(resp.status_code, 403)
+        c.force_login(self.human_user1)
+
+        with freeze_time(datetime(2020, 1, 2, 3, 4, 5, tzinfo=UTC)):
+            resp = c.post(
+                reverse('api:my-send-message'),
+                {
+                    'recipient': self.human_user2.username,
+                    'subject': 'Message Subject',
+                    'body': 'Message Body',
+                },
+            )
+        self.assertEqual(resp.status_code, 201)
+        self.assertEqual(DirectMessage.objects.all().count(), 1)
+        dm = DirectMessage.objects.get()
+        self.assertEqual(dm.conference, self.conference1)
+        self.assertEqual(dm.sender, self.human_user1)
+        self.assertEqual(dm.recipient, self.human_user2)
+        self.assertEqual(dm.timestamp, datetime(2020, 1, 2, 3, 4, 5, tzinfo=UTC))
+        self.assertEqual(dm.subject, 'Message Subject')
+        self.assertEqual(dm.body, 'Message Body')
+        self.assertFalse(dm.was_read)
+        self.assertFalse(dm.has_responded)
+        self.assertFalse(dm.deleted_by_recipient)
+
+        with freeze_time(datetime(2020, 3, 5, 7, 9, 11, tzinfo=UTC)):
+            resp = c.post(
+                reverse('api:my-send-message'),
+                {
+                    'recipient': self.human_user2.username,
+                    'subject': 'Message Subject2',
+                    'body': 'Message Body2',
+                },
+            )
+
+        self.assertEqual(DirectMessage.objects.all().count(), 2)
+        dm2 = DirectMessage.objects.get(pk=resp.json()['id'])
+        self.assertEqual(dm2.timestamp, datetime(2020, 3, 5, 7, 9, 11, tzinfo=UTC))
+
+        self.human_user1.shadow_banned = True
+        self.human_user1.save()
+        with freeze_time(datetime(2020, 3, 5, 7, 9, 11, tzinfo=UTC)):
+            resp = c.post(
+                reverse('api:my-send-message'),
+                {
+                    'recipient': self.human_user2.username,
+                    'subject': 'Message Subject, Sender is shadow banned',
+                    'body': 'Message Body, Sender is shadow banned',
+                },
+            )
+
+        self.assertEqual(DirectMessage.objects.all().count(), 3)
+        dm3 = DirectMessage.objects.get(pk=resp.json()['id'])
+        self.assertTrue(dm3.deleted_by_recipient)
diff --git a/src/api/urls.py b/src/api/urls.py
index cc96f91ff392a4374f4442305095f678406579ec..b92ea88dca3a9781b96bf6d0306ee9fa74f345ef 100644
--- a/src/api/urls.py
+++ b/src/api/urls.py
@@ -19,6 +19,7 @@ urlpatterns = [
     path('me/sent-messages/', messages.DirectMessagesSent.as_view(), name='my-messages-sent'),
     path('me/received-messages/<uuid:pk>', messages.DirectMessageReceived.as_view(), name='my-message-received'),
     path('me/sent-messages/<uuid:pk>', messages.DirectMessageSent.as_view(), name='my-message-sent'),
+    path('me/send-message', messages.DirectMessageSend.as_view(), name='my-send-message'),
     path('me/friends', users.friends, name='friends'),
     path('me/timeline', users.UserTimelineList.as_view(), name='timeline-list'),
     # conference-specific views
diff --git a/src/api/views/messages.py b/src/api/views/messages.py
index 3cf37d2bc6bd03435610d793da8b062542817b0f..1e9adb8176771bc3d75ee2fb19fb7a0949e25a40 100644
--- a/src/api/views/messages.py
+++ b/src/api/views/messages.py
@@ -1,8 +1,9 @@
+from django.conf import settings
 from rest_framework import generics, permissions
 
 from core.models.messages import DirectMessage
 
-from ..serializers import DirectMessageSerializerReceived, DirectMessageSerializerReceivedShort, DirectMessageSerializerSent, DirectMessageSerializerSentShort
+from ..serializers import DirectMessageSerializerReceived, DirectMessageSerializerReceivedShort, DirectMessageSerializerSent, DirectMessageSerializerSentShort, DirectMessageSendSerializer
 from .mixins import ConferenceSlugMixin
 
 
@@ -52,3 +53,15 @@ class DirectMessageSent(ConferenceSlugMixin, generics.RetrieveAPIView):
             .select_related('recipient', 'sender')
             .order_by('-timestamp')
         )
+
+
+class DirectMessageSend(ConferenceSlugMixin, generics.CreateAPIView):
+    permission_classes = [permissions.IsAuthenticated]
+    serializer_class = DirectMessageSendSerializer
+
+    def perform_create(self, serializer: DirectMessageSendSerializer):
+        serializer.save(
+            conference=self.conference,
+            sender=self.request.user,
+            deleted_by_recipient=self.request.user != serializer.validated_data['recipient'] and self.request.user.shadow_banned,
+        )