diff --git a/backend/authentication/models.py b/backend/authentication/models.py
index 6c4e50f..b89b561 100644
--- a/backend/authentication/models.py
+++ b/backend/authentication/models.py
@@ -102,6 +102,9 @@ class ToolshedUser(AbstractUser):
         private_key = SigningKey(self.private_key.encode(), encoder=HexEncoder)
         return private_key.sign(message.encode('utf-8'), encoder=HexEncoder).signature.decode('utf-8')
 
+    def public_key(self):
+        return self.public_identity.public_key
+
 
 class FriendRequestOutgoing(models.Model):
     secret = models.CharField(max_length=255)
diff --git a/backend/authentication/signature_auth.py b/backend/authentication/signature_auth.py
index 99b5060..fd36705 100644
--- a/backend/authentication/signature_auth.py
+++ b/backend/authentication/signature_auth.py
@@ -1,3 +1,5 @@
+from nacl.exceptions import BadSignatureError
+from nacl.signing import VerifyKey
 from rest_framework import authentication
 
 from authentication.models import KnownIdentity, ToolshedUser
@@ -48,6 +50,28 @@ def verify_request(request, raw_request_body):
     return username, domain, signed_data, signature_bytes_hex
 
 
+def verify_incoming_friend_request(request, raw_request_body):
+    try:
+        username, domain, signed_data, signature_bytes_hex = verify_request(request, raw_request_body)
+    except ValueError:
+        return False
+    try:
+        befriender = request.data['befriender']
+        befriender_key = request.data['befriender_key']
+    except KeyError:
+        return False
+    if username + "@" + domain != befriender:
+        return False
+    if len(befriender_key) != 64:
+        return False
+    verify_key = VerifyKey(bytes.fromhex(befriender_key))
+    try:
+        verify_key.verify(signed_data.encode('utf-8'), bytes.fromhex(signature_bytes_hex))
+        return True
+    except BadSignatureError:
+        return False
+
+
 def authenticate_request_against_known_identities(request, raw_request_body):
     try:
         username, domain, signed_data, signature_bytes_hex = verify_request(request, raw_request_body)
diff --git a/backend/toolshed/api/friend.py b/backend/toolshed/api/friend.py
index 1fe36a1..66f8342 100644
--- a/backend/toolshed/api/friend.py
+++ b/backend/toolshed/api/friend.py
@@ -1,11 +1,18 @@
+import secrets
+
 from django.urls import path
+from rest_framework import status
+from rest_framework.decorators import api_view
+from rest_framework.generics import get_object_or_404
 from rest_framework.permissions import IsAuthenticated
 from rest_framework.response import Response
 from rest_framework.views import APIView
 from rest_framework.viewsets import ViewSetMixin
 
-from authentication.signature_auth import SignatureAuthentication
-from toolshed.serializers import FriendSerializer
+from authentication.models import KnownIdentity, FriendRequestIncoming, FriendRequestOutgoing, ToolshedUser
+from authentication.signature_auth import verify_incoming_friend_request, split_userhandle_or_throw, \
+    authenticate_request_against_local_users, SignatureAuthentication
+from toolshed.serializers import FriendSerializer, FriendRequestSerializer
 
 
 class Friends(APIView, ViewSetMixin):
@@ -18,7 +25,102 @@ class Friends(APIView, ViewSetMixin):
         serializer = FriendSerializer(friends, many=True)
         return Response(serializer.data)
 
+    def post(self, request, format=None):  # /api/friends/
+        # only for local users
+        try:
+            user = request.user
+            incoming_request = FriendRequestIncoming.objects.get(
+                pk=request.data.get('friend_request_id'),
+                secret=request.data.get('secret'))
+            befriender, _ = KnownIdentity.objects.get_or_create(
+                username=incoming_request.befriender_username,
+                domain=incoming_request.befriender_domain,
+                public_key=incoming_request.befriender_public_key
+            )
+            befriender.save()
+            user.user.get().friends.add(befriender)
+            user.user.get().save()
+            incoming_request.delete()
+            return Response(status=status.HTTP_201_CREATED, data={'status': 'accepted'})
+        except FriendRequestIncoming.DoesNotExist:
+            return Response(status=status.HTTP_404_NOT_FOUND, data={'status': 'not found'})
+
+
+class FriendsRequests(APIView, ViewSetMixin):
+    def get(self, request, format=None):  # /api/friendrequests/
+        raw_request = request.body.decode('utf-8')
+        if user := authenticate_request_against_local_users(request, raw_request):
+            friends_requests = user.friend_requests_incoming.all()
+            serializer = FriendRequestSerializer(friends_requests, many=True)
+            return Response(serializer.data)
+        else:
+            return Response(status=status.HTTP_401_UNAUTHORIZED, data={'status': 'unauthorized'})
+
+    def post(self, request, format=None):  # /api/friendrequests/
+        raw_request = request.body.decode('utf-8')
+        if 'befriender' not in request.data or 'befriendee' not in request.data:
+            return Response(status=status.HTTP_400_BAD_REQUEST, data={'status': 'missing parameters'})
+        befriender_username, befriender_domain = split_userhandle_or_throw(request.data['befriender'])
+        befriendee_username, befriendee_domain = split_userhandle_or_throw(request.data['befriendee'])
+        if befriender_domain == befriendee_domain and befriender_username == befriendee_username:
+            return Response(status=status.HTTP_400_BAD_REQUEST, data={'status': 'cannot befriend yourself'})
+        if user := authenticate_request_against_local_users(request, raw_request):
+            secret = secrets.token_hex(64)
+            befriendee_user = ToolshedUser.objects.filter(username=befriendee_username, domain=befriendee_domain)
+            if befriendee_user.exists():
+                FriendRequestIncoming.objects.create(
+                    befriender_username=befriender_username,
+                    befriender_domain=befriender_domain,
+                    befriender_public_key=user.public_identity.public_key,
+                    secret=secret,  # request.data['secret'] # TODO ??
+                    befriendee_user=befriendee_user.get(),
+                )
+                return Response(status=status.HTTP_201_CREATED, data={'secret': secret, 'status': "pending"})
+            else:
+                FriendRequestOutgoing.objects.create(
+                    befriender_user=user,
+                    befriendee_username=befriendee_username,
+                    befriendee_domain=befriendee_domain,
+                    secret=secret,  # request.data['secret'] # TODO ??
+                )
+                return Response(status=status.HTTP_201_CREATED, data={'secret': secret, 'status': "pending"})
+        elif verify_incoming_friend_request(request, raw_request):
+            try:
+                befriendee = ToolshedUser.objects.get(username=befriendee_username, domain=befriendee_domain)
+                outgoing = FriendRequestOutgoing.objects.filter(
+                    secret=request.data['secret'],
+                    befriender_user=befriendee,  # both sides match
+                    befriendee_username=befriender_username,
+                    befriendee_domain=befriender_domain)
+                if outgoing.exists():
+                    befriender, _ = KnownIdentity.objects.get_or_create(
+                        username=befriender_username,
+                        domain=befriender_domain,
+                        public_key=request.data['befriender_key']
+                    )
+                    befriender.save()
+                    befriendee.friends.add(befriender)
+                    befriendee.save()
+                    outgoing.delete()
+                    return Response(status=status.HTTP_201_CREATED, data={'status': "accepted"})
+                else:
+                    FriendRequestIncoming.objects.create(
+                        befriender_username=befriender_username,
+                        befriender_domain=befriender_domain,
+                        befriender_public_key=request.data['befriender_key'],
+                        befriendee_user=befriendee,
+                        secret=request.data['secret']
+                    )
+                    return Response(status=status.HTTP_201_CREATED, data={'status': "pending"})
+            except ToolshedUser.DoesNotExist:
+                return Response(status=status.HTTP_400_BAD_REQUEST)
+            except KeyError:
+                return Response(status=status.HTTP_400_BAD_REQUEST)
+        else:
+            return Response(status=status.HTTP_400_BAD_REQUEST)
+
 
 urlpatterns = [
     path('friends/', Friends.as_view(), name='friends'),
+    path('friendrequests/', FriendsRequests.as_view(), name='friendrequests'),
 ]
diff --git a/backend/toolshed/serializers.py b/backend/toolshed/serializers.py
index cccebd4..32cde24 100644
--- a/backend/toolshed/serializers.py
+++ b/backend/toolshed/serializers.py
@@ -1,6 +1,5 @@
 from rest_framework import serializers
-
-from authentication.models import KnownIdentity, ToolshedUser
+from authentication.models import KnownIdentity, ToolshedUser, FriendRequestIncoming
 from authentication.serializers import OwnerSerializer
 from files.models import File
 from files.serializers import FileSerializer
@@ -18,6 +17,17 @@ class FriendSerializer(serializers.ModelSerializer):
         return obj.username + '@' + obj.domain
 
 
+class FriendRequestSerializer(serializers.ModelSerializer):
+    befriender = serializers.SerializerMethodField()
+
+    class Meta:
+        model = FriendRequestIncoming
+        fields = ['befriender', 'befriender_public_key', 'secret', 'id']
+
+    def get_befriender(self, obj):
+        return obj.befriender_username + '@' + obj.befriender_domain
+
+
 class PropertySerializer(serializers.ModelSerializer):
     category = serializers.SlugRelatedField(queryset=Category.objects.all(), slug_field='name')
 
diff --git a/backend/toolshed/tests/test_friend.py b/backend/toolshed/tests/test_friend.py
index fec6971..f96b2a1 100644
--- a/backend/toolshed/tests/test_friend.py
+++ b/backend/toolshed/tests/test_friend.py
@@ -1,4 +1,6 @@
+from django.test import Client
 from authentication.tests import SignatureAuthClient, UserTestMixin, ToolshedTestCase
+from authentication.models import FriendRequestIncoming, FriendRequestOutgoing
 
 client = SignatureAuthClient()
 
@@ -75,3 +77,265 @@ class FriendApiTestCase(UserTestMixin, ToolshedTestCase):
         self.assertEqual(reply.status_code, 200)
         self.assertEqual(len(reply.json()), 1)
         self.assertEqual(reply.json()[0]['username'], str(self.f['local_user1']))
+
+
+# what ~should~ happen:
+# 1. user x@A sends a friend request to user y@B
+#   1.1. x@A's client sends a POST request to A/api/friendrequests/ with body {from: x@A, to: y@B}
+#   1.2. A's backend creates a FriendRequestOutgoing object, containing x@A's identity and y@B's name
+#   1.3. x@A's client sends a POST request to B/api/friendrequests/ with body
+#                       {from: x@A, to: y@B, public_key: x@A's public key}
+#   1.4. B's backend creates a FriendRequestIncoming object, containing y@B's and x@A's identities
+# 2. user y@B accepts the friend request
+#   2.1. y@B's client sends a POST request to A/api/friendsrequests/ with body
+#                       {from: x@A, to: y@B, public_key: y@B's public key}
+#   2.2. A's backend matches the data to the FriendRequestOutgoing object, deletes both and  creates a Friend object,
+#       containing x@A's and y@B's identities
+#   2.3. y@B's client sends a POST request to B/api/friends/ containing the id of the FriendRequestIncoming object
+#   2.4. B's backend creates a Friend object, using the identities from the FriendRequestIncoming object
+
+
+class FriendRequestListTestCase(UserTestMixin, ToolshedTestCase):
+
+    def setUp(self):
+        super().setUp()
+        self.prepare_users()
+        FriendRequestIncoming.objects.create(
+            befriender_username=self.f['ext_user2'].username, befriender_domain=self.f['ext_user2'].domain,
+            befriender_public_key=self.f['ext_user2'].public_key(), befriendee_user=self.f['local_user1'],
+            secret='secret1').save()
+
+    def test_friend_request_withouth_auth(self):
+        reply = Client().get('/api/friendrequests/')
+        self.assertEqual(reply.status_code, 401)
+
+    def test_friend_request_empty(self):
+        reply = client.get('/api/friendrequests/', self.f['local_user2'])
+        self.assertEqual(reply.status_code, 200)
+        self.assertEqual(reply.json(), [])
+
+    def test_friend_request_list(self):
+        reply = client.get('/api/friendrequests/', self.f['local_user1'])
+        self.assertEqual(reply.status_code, 200)
+        self.assertEqual(len(reply.json()), 1)
+        self.assertEqual(reply.json()[0]['befriender'], str(self.f['ext_user2']))
+        self.assertEqual(reply.json()[0]['befriender_public_key'], self.f['ext_user2'].public_key())
+
+
+class FriendRequestIncomingTestCase(UserTestMixin, ToolshedTestCase):
+    def setUp(self):
+        super().setUp()
+        self.prepare_users()
+        FriendRequestIncoming.objects.create(
+            befriender_username=self.f['ext_user2'].username, befriender_domain=self.f['ext_user2'].domain,
+            befriender_public_key=self.f['ext_user2'].public_key(), befriendee_user=self.f['local_user1'],
+            secret='secret1').save()
+
+    def test_post_request(self):
+        befriender = self.f['ext_user1']
+        befriendee = self.f['local_user1']
+        reply = client.post('/api/friendrequests/', befriender, {
+            'befriender': str(befriender),
+            'befriender_key': befriender.public_key(),
+            'befriendee': str(befriendee),
+            'secret': 'secret2'
+        })
+        self.assertEqual(reply.status_code, 201)
+        self.assertEqual(reply.json()['status'], 'pending')
+        self.assertEqual(FriendRequestIncoming.objects.count(), 2)
+        incoming = FriendRequestIncoming.objects.get(befriender_username=befriender.username,
+                                                     befriender_domain=befriender.domain)
+        self.assertEqual(incoming.befriendee_user, befriendee)
+        self.assertEqual(incoming.befriender_public_key, befriender.public_key())
+        self.assertEqual(incoming.secret, 'secret2')
+
+    def test_post_request_local(self):
+        befriender = self.f['local_user2']
+        befriendee = self.f['local_user1']
+        reply = client.post('/api/friendrequests/', befriender, {
+            'befriender': str(befriender),
+            'befriendee': str(befriendee),
+            # 'secret': 'secret2'
+        })
+        self.assertEqual(reply.status_code, 201)
+        self.assertEqual(reply.json()['status'], 'pending')
+        self.assertEqual(FriendRequestIncoming.objects.count(), 2)
+        incoming = FriendRequestIncoming.objects.get(befriender_username=befriender.username,
+                                                     befriender_domain=befriender.domain)
+        self.assertEqual(incoming.befriendee_user, befriendee)
+        self.assertEqual(incoming.befriender_public_key, befriender.public_key())
+        # self.assertEqual(incoming.secret, 'secret2')
+
+    def test_post_request_withouth_auth(self):
+        reply = Client().post('/api/friendrequests/')
+        self.assertEqual(reply.status_code, 400)
+
+    def test_post_request_broken_header(self):
+        befriender = self.f['ext_user1']
+        befriendee = self.f['local_user1']
+        broken_client = SignatureAuthClient(header_prefix='broken ')
+        reply = broken_client.post('/api/friendrequests/', befriender, {
+            'befriender': str(befriender),
+            'befriender_key': befriender.public_key(),
+            'befriendee': str(befriendee),
+            'secret': 'secret2'
+        })
+        self.assertEqual(reply.status_code, 400)
+
+    def test_post_request_missing_key(self):
+        befriender = self.f['ext_user1']
+        befriendee = self.f['local_user1']
+        reply = client.post('/api/friendrequests/', befriender, {
+            'befriender': str(befriender),
+            'befriendee': str(befriendee),
+            'secret': 'secret2'
+        })
+        self.assertEqual(reply.status_code, 400)
+
+    def test_post_request_breaking_key(self):
+        befriender = self.f['ext_user1']
+        befriendee = self.f['local_user1']
+        reply = client.post('/api/friendrequests/', befriender, {
+            'befriender': str(befriender),
+            'befriendee': str(befriendee),
+            'secret': 'secret2',
+            'befriender_key': 'broken'
+        })
+        self.assertEqual(reply.status_code, 400)
+
+    def test_post_request_wrong_befriender(self):
+        befriender = self.f['ext_user1']
+        befriendee = self.f['local_user1']
+        reply = client.post('/api/friendrequests/', befriender, {
+            'befriender': str(self.f['local_user2']),
+            'befriender_key': befriender.public_key(),
+            'befriendee': str(befriendee),
+            'secret': 'secret2'
+        })
+        self.assertEqual(reply.status_code, 400)
+
+    def test_post_request_bad_signature(self):
+        befriender = self.f['ext_user1']
+        befriendee = self.f['local_user1']
+        bad_signature = SignatureAuthClient(bad_signature=True)
+        reply = bad_signature.post('/api/friendrequests/', befriender, {
+            'befriender': str(befriender),
+            'befriender_key': befriender.public_key(),
+            'befriendee': str(befriendee),
+            'secret': 'secret2'
+        })
+        self.assertEqual(reply.status_code, 400)
+
+    def test_post_request_self(self):
+        befriender = self.f['local_user1']
+        befriendee = self.f['local_user1']
+        reply = client.post('/api/friendrequests/', befriender, {
+            'befriender': str(befriender),
+            'befriender_key': befriender.public_key(),
+            'befriendee': str(befriendee),
+            'secret': 'secret2'
+        })
+        self.assertEqual(reply.status_code, 400)
+
+    def test_post_request_befreindee_not_found(self):
+        befriender = self.f['ext_user1']
+        befriendee = self.f['local_user1']
+        reply = client.post('/api/friendrequests/', befriender, {
+            'befriender': str(befriender),
+            'befriender_key': befriender.public_key(),
+            'befriendee': 'nonexistent@' + befriendee.domain,
+            'secret': 'secret2'
+        })
+        self.assertEqual(reply.status_code, 400)
+
+    def test_post_request_missing_secret(self):
+        befriender = self.f['ext_user1']
+        befriendee = self.f['local_user1']
+        reply = client.post('/api/friendrequests/', befriender, {
+            'befriender': str(befriender),
+            'befriender_key': befriender.public_key(),
+            'befriendee': str(befriendee)
+        })
+        self.assertEqual(reply.status_code, 400)
+
+    def test_accept_request(self):
+        befriender = self.f['ext_user2']
+        befriendee = self.f['local_user1']
+        request = FriendRequestIncoming.objects.filter(befriender_username=befriender.username,
+                                                       befriender_domain=befriender.domain,
+                                                       befriendee_user=befriendee).first()
+        reply = client.post('/api/friends/', befriendee, {
+            'friend_request_id': request.id,
+            'secret': request.secret
+        })
+        self.assertEqual(reply.status_code, 201)
+        self.assertEqual(reply.json(), {'status': 'accepted'})
+
+    def test_accept_request(self):
+        befriender = self.f['ext_user2']
+        befriendee = self.f['local_user1']
+        request = FriendRequestIncoming.objects.filter(befriender_username=befriender.username,
+                                                       befriender_domain=befriender.domain,
+                                                       befriendee_user=befriendee).first()
+        reply = client.post('/api/friends/', befriendee, {
+            'friend_request_id': request.id,
+            'secret': request.secret
+        })
+        self.assertEqual(reply.status_code, 201)
+        self.assertEqual(reply.json(), {'status': 'accepted'})
+
+    def test_accept_request_not_found(self):
+        befriender = self.f['ext_user2']
+        befriendee = self.f['local_user1']
+        reply = client.post('/api/friends/', befriendee, {
+            'friend_request_id': 999,
+            'secret': 'secret1'
+        })
+        self.assertEqual(reply.status_code, 404)
+
+
+class FriendRequestOutgoingTestCase(UserTestMixin, ToolshedTestCase):
+
+    def setUp(self):
+        super().setUp()
+        self.prepare_users()
+        FriendRequestOutgoing.objects.create(
+            befriender_user=self.f['local_user2'],
+            befriendee_username=self.f['ext_user1'].username,
+            befriendee_domain=self.f['ext_user1'].domain,
+            secret='secret3'
+        ).save()
+
+    def test_post_outgoing_friend_request(self):
+        befriender = self.f['local_user1']
+        befriendee = self.f['ext_user1']
+        reply = client.post('/api/friendrequests/', befriender, {
+            'befriender': str(befriender),
+            'befriendee': str(befriendee),
+        })
+        self.assertEqual(reply.status_code, 201)
+        self.assertTrue('status' in reply.json())
+        self.assertEqual(reply.json()['status'], 'pending')
+        self.assertEqual(FriendRequestOutgoing.objects.count(), 2)
+        outgoing = FriendRequestOutgoing.objects.get(befriender_user=befriender)
+        self.assertTrue('secret' in reply.json())
+        self.assertEqual(reply.json()['secret'], outgoing.secret)
+        self.assertEqual(outgoing.befriendee_username, befriendee.username)
+        self.assertEqual(outgoing.befriendee_domain, befriendee.domain)
+
+    def test_accept_request(self):
+        befriender = self.f['ext_user1']
+        befriendee = self.f['local_user2']
+        reply = client.post('/api/friendrequests/', befriender, {
+            'befriender': str(befriender),
+            'befriender_key': befriender.public_key(),
+            'befriendee': str(befriendee),
+            'secret': 'secret3'
+        })
+        self.assertEqual(reply.status_code, 201)
+        self.assertEqual(reply.json(), {'status': 'accepted'})
+        self.assertEqual(FriendRequestIncoming.objects.count(), 0)
+        self.assertEqual(FriendRequestOutgoing.objects.count(), 0)
+        self.assertEqual(befriendee.friends.count(), 1)
+        self.assertEqual(befriendee.friends.first().username, befriender.username)
+        self.assertEqual(befriendee.friends.first().domain, befriender.domain)