diff --git a/api/funkwhale_api/federation/activity.py b/api/funkwhale_api/federation/activity.py
index 00140fe53ffad4d89ffa3ceb099bc66be44e1ea2..4eeb193b183007ad6c7c093cd20b881a3a06b89b 100644
--- a/api/funkwhale_api/federation/activity.py
+++ b/api/funkwhale_api/federation/activity.py
@@ -1,4 +1,11 @@
+import logging
+import json
+import requests
+import requests_http_signature
 
+from . import signing
+
+logger = logging.getLogger(__name__)
 
 ACTIVITY_TYPES = [
     'Accept',
@@ -47,5 +54,32 @@ OBJECT_TYPES = [
     'Video',
 ]
 
-def deliver(content, on_behalf_of, to=[]):
-    pass
+def deliver(activity, on_behalf_of, to=[]):
+    from . import actors
+    logger.info('Preparing activity delivery to %s', to)
+    auth = requests_http_signature.HTTPSignatureAuth(
+        use_auth_header=False,
+        headers=[
+            '(request-target)',
+            'user-agent',
+            'host',
+            'date',
+            'content-type',],
+        algorithm='rsa-sha256',
+        key=on_behalf_of.private_key.encode('utf-8'),
+        key_id=on_behalf_of.private_key_id,
+    )
+    for url in to:
+        recipient_actor = actors.get_actor(url)
+        logger.debug('delivering to %s', recipient_actor.inbox_url)
+        logger.debug('activity content: %s', json.dumps(activity))
+        response = requests.post(
+            auth=auth,
+            json=activity,
+            url=recipient_actor.inbox_url,
+            headers={
+                'Content-Type': 'application/activity+json'
+            }
+        )
+        response.raise_for_status()
+        logger.debug('Remote answered with %s', response.status_code)
diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py
index 1832b08bcc0adf4cc605037d6ca311ad395ec1f0..7a9b47d183eb77783a32296133702b6f443076cc 100644
--- a/api/funkwhale_api/federation/actors.py
+++ b/api/funkwhale_api/federation/actors.py
@@ -1,21 +1,27 @@
+import logging
 import requests
 import xml
 
-from django.urls import reverse
 from django.conf import settings
+from django.urls import reverse
+from django.utils import timezone
 
 from rest_framework.exceptions import PermissionDenied
 
 from dynamic_preferences.registries import global_preferences_registry
 
 from . import activity
+from . import factories
 from . import models
 from . import serializers
 from . import utils
 
+logger = logging.getLogger(__name__)
+
 
 def remove_tags(text):
-    return ''.join(xml.etree.ElementTree.fromstring(text).itertext())
+    logger.debug('Removing tags from %s', text)
+    return ''.join(xml.etree.ElementTree.fromstring('<div>{}</div>'.format(text)).itertext())
 
 
 def get_actor_data(actor_url):
@@ -32,6 +38,13 @@ def get_actor_data(actor_url):
         raise ValueError(
             'Invalid actor payload: {}'.format(response.text))
 
+def get_actor(actor_url):
+    data = get_actor_data(actor_url)
+    serializer = serializers.ActorSerializer(data=data)
+    serializer.is_valid(raise_exception=True)
+
+    return serializer.build()
+
 
 class SystemActor(object):
     additional_attributes = {}
@@ -73,6 +86,7 @@ class SystemActor(object):
                     'federation:instance-actors-outbox',
                     kwargs={'actor': id})),
             'public_key': preferences['federation__public_key'],
+            'private_key': preferences['federation__private_key'],
             'summary': summary.format(host=settings.FEDERATION_HOSTNAME)
         }
         p.update(kwargs)
@@ -136,14 +150,13 @@ class TestActor(SystemActor):
         serializer.is_valid(raise_exception=True)
 
         ac = serializer.validated_data
+        logger.info('Received activity on %s inbox', self.id)
         if ac['type'] == 'Create' and ac['object']['type'] == 'Note':
             # we received a toot \o/
             command = self.parse_command(ac['object']['content'])
+            logger.debug('Parsed command: %s', command)
             if command == 'ping':
-                activity.deliver(
-                    content='Pong!',
-                    to=[ac['actor']],
-                    on_behalf_of=self.get_actor_instance())
+                self.handle_ping(ac, actor)
 
     def parse_command(self, message):
         """
@@ -156,6 +169,67 @@ class TestActor(SystemActor):
         except IndexError:
             return
 
+    def handle_ping(self, ac, sender):
+        now = timezone.now()
+        test_actor = self.get_actor_instance()
+        reply_url = 'https://{}/activities/note/{}'.format(
+            settings.FEDERATION_HOSTNAME, now.timestamp()
+        )
+        mention = '@{}@{}'.format(
+            sender.preferred_username,
+            sender.domain
+        )
+        reply_content = '{} Pong!'.format(
+            mention
+        )
+        reply_activity = {
+            "@context": [
+        		"https://www.w3.org/ns/activitystreams",
+        		"https://w3id.org/security/v1",
+        		{
+        			"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+        			"sensitive": "as:sensitive",
+        			"movedTo": "as:movedTo",
+        			"Hashtag": "as:Hashtag",
+        			"ostatus": "http://ostatus.org#",
+        			"atomUri": "ostatus:atomUri",
+        			"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+        			"conversation": "ostatus:conversation",
+        			"toot": "http://joinmastodon.org/ns#",
+        			"Emoji": "toot:Emoji"
+        		}
+        	],
+            'type': 'Create',
+            'actor': test_actor.url,
+            'id': '{}/activity'.format(reply_url),
+            'published': now.isoformat(),
+            'to': ac['actor'],
+            'cc': [],
+            'object': factories.NoteFactory(
+                content='Pong!',
+                summary=None,
+                published=now.isoformat(),
+                id=reply_url,
+                inReplyTo=ac['object']['id'],
+                sensitive=False,
+                url=reply_url,
+                to=[ac['actor']],
+                attributedTo=test_actor.url,
+                cc=[],
+                attachment=[],
+                tag=[
+                    {
+                        "type": "Mention",
+                        "href": ac['actor'],
+                        "name": mention
+                    }
+                ]
+            )
+        }
+        activity.deliver(
+            reply_activity,
+            to=[ac['actor']],
+            on_behalf_of=test_actor)
 
 SYSTEM_ACTORS = {
     'library': LibraryActor(),
diff --git a/api/funkwhale_api/federation/factories.py b/api/funkwhale_api/federation/factories.py
index d34242fc8e983d928b05e6731d854a374ca4358e..ebd6b2fd51712b4fa6ebef26f4734c4764e8ad83 100644
--- a/api/funkwhale_api/federation/factories.py
+++ b/api/funkwhale_api/federation/factories.py
@@ -2,6 +2,8 @@ import factory
 import requests
 import requests_http_signature
 
+from django.utils import timezone
+
 from funkwhale_api.factories import registry
 
 from . import keys
@@ -64,8 +66,24 @@ class ActorFactory(factory.DjangoModelFactory):
 
     @classmethod
     def _generate(cls, create, attrs):
-        has_public = attrs.get('public_key') is None
-        has_private = attrs.get('private_key') is None
+        has_public = attrs.get('public_key') is not None
+        has_private = attrs.get('private_key') is not None
         if not has_public and not has_private:
-            attrs['private_key'], attrs['public'] = keys.get_key_pair()
+            private, public = keys.get_key_pair()
+            attrs['private_key'] = private.decode('utf-8')
+            attrs['public_key'] = public.decode('utf-8')
         return super()._generate(create, attrs)
+
+
+@registry.register(name='federation.Note')
+class NoteFactory(factory.Factory):
+    type = 'Note'
+    id = factory.Faker('url')
+    published = factory.LazyFunction(
+        lambda: timezone.now().isoformat()
+    )
+    inReplyTo = None
+    content = factory.Faker('sentence')
+
+    class Meta:
+        model = dict
diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py
index 201307d46c93214f9cd7f0066eb60bff2a45bd88..fa38678e9d4b2bb2619a38fd8d312a8fc2d1913d 100644
--- a/api/funkwhale_api/federation/models.py
+++ b/api/funkwhale_api/federation/models.py
@@ -38,3 +38,7 @@ class Actor(models.Model):
             self.preferred_username,
             settings.FEDERATION_HOSTNAME,
         )
+
+    @property
+    def private_key_id(self):
+        return '{}#main-key'.format(self.url)
diff --git a/api/tests/federation/test_activity.py b/api/tests/federation/test_activity.py
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a6e1d28aa23623251a1ab26661d2814c14704f00 100644
--- a/api/tests/federation/test_activity.py
+++ b/api/tests/federation/test_activity.py
@@ -0,0 +1,32 @@
+from funkwhale_api.federation import activity
+
+def test_deliver(nodb_factories, r_mock, mocker):
+    to = nodb_factories['federation.Actor']()
+    mocker.patch(
+        'funkwhale_api.federation.actors.get_actor',
+        return_value=to)
+    sender = nodb_factories['federation.Actor']()
+    ac = {
+        'id': 'http://test.federation/activity',
+        'type': 'Create',
+        'actor': sender.url,
+        'object': {
+            'id': 'http://test.federation/note',
+            'type': 'Note',
+            'content': 'Hello',
+        }
+    }
+
+    r_mock.post(to.inbox_url)
+
+    activity.deliver(
+        ac,
+        to=[to.url],
+        on_behalf_of=sender,
+    )
+    request = r_mock.request_history[0]
+
+    assert r_mock.called is True
+    assert r_mock.call_count == 1
+    assert request.url == to.inbox_url
+    assert request.headers['content-type'] == 'application/activity+json'
diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py
index a239f4961bb7917937dd92780eaa4811a3a5f5ed..88a94e56222468155e4b3929d7e40ed3392e3d07 100644
--- a/api/tests/federation/test_actors.py
+++ b/api/tests/federation/test_actors.py
@@ -1,6 +1,8 @@
 import pytest
 
 from django.urls import reverse
+from django.utils import timezone
+
 from rest_framework import exceptions
 
 from funkwhale_api.federation import actors
@@ -128,6 +130,8 @@ def test_test_post_outbox_handles_create_note(mocker, factories):
     deliver = mocker.patch(
         'funkwhale_api.federation.activity.deliver')
     actor = factories['federation.Actor']()
+    now = timezone.now()
+    mocker.patch('django.utils.timezone.now', return_value=now)
     data = {
         'actor': actor.url,
         'type': 'Create',
@@ -138,9 +142,27 @@ def test_test_post_outbox_handles_create_note(mocker, factories):
             'content': '<p><a>@mention</a> /ping</p>'
         }
     }
+    expected_note = factories['federation.Note'](
+        id='https://test.federation/activities/note/{}'.format(
+            now.timestamp()
+        ),
+        content='Pong!',
+        published=now.isoformat(),
+        inReplyTo=data['object']['id'],
+    )
+    test_actor = actors.SYSTEM_ACTORS['test'].get_actor_instance()
+    expected_activity = {
+        'actor': test_actor.url,
+        'id': 'https://test.federation/activities/note/{}/activity'.format(
+            now.timestamp()
+        ),
+        'type': 'Create',
+        'published': now.isoformat(),
+        'object': expected_note
+    }
     actors.SYSTEM_ACTORS['test'].post_inbox(data, actor=actor)
     deliver.assert_called_once_with(
-        content='Pong!',
+        expected_activity,
         to=[actor.url],
         on_behalf_of=actors.SYSTEM_ACTORS['test'].get_actor_instance()
     )