diff --git a/api/config/api_urls.py b/api/config/api_urls.py
index ff6db0d069395c316d207a640e0187ccf92b12df..cab6805b67e394838ec942ecf8b162edb08a88cf 100644
--- a/api/config/api_urls.py
+++ b/api/config/api_urls.py
@@ -1,5 +1,6 @@
 from rest_framework import routers
 from django.conf.urls import include, url
+from funkwhale_api.activity import views as activity_views
 from funkwhale_api.instance import views as instance_views
 from funkwhale_api.music import views
 from funkwhale_api.playlists import views as playlists_views
@@ -10,6 +11,7 @@ from dynamic_preferences.users.viewsets import UserPreferencesViewSet
 
 router = routers.SimpleRouter()
 router.register(r'settings', GlobalPreferencesViewSet, base_name='settings')
+router.register(r'activity', activity_views.ActivityViewSet, 'activity')
 router.register(r'tags', views.TagViewSet, 'tags')
 router.register(r'tracks', views.TrackViewSet, 'tracks')
 router.register(r'trackfiles', views.TrackFileViewSet, 'trackfiles')
diff --git a/api/funkwhale_api/activity/serializers.py b/api/funkwhale_api/activity/serializers.py
index 325d1e820db5699abca69b57b3421b0e0ca1d68b..fd9b185cf9a6d3891f0356208f62b2bb54e8686f 100644
--- a/api/funkwhale_api/activity/serializers.py
+++ b/api/funkwhale_api/activity/serializers.py
@@ -1,5 +1,7 @@
 from rest_framework import serializers
 
+from funkwhale_api.activity import record
+
 
 class ModelSerializer(serializers.ModelSerializer):
     id = serializers.CharField(source='get_activity_url')
@@ -8,3 +10,15 @@ class ModelSerializer(serializers.ModelSerializer):
 
     def get_url(self, obj):
         return self.get_id(obj)
+
+
+class AutoSerializer(serializers.Serializer):
+    """
+    A serializer that will automatically use registered activity serializers
+    to serialize an henerogeneous list of objects (favorites, listenings, etc.)
+    """
+    def to_representation(self, instance):
+        serializer = record.registry[instance._meta.label]['serializer'](
+            instance
+        )
+        return serializer.data
diff --git a/api/funkwhale_api/activity/utils.py b/api/funkwhale_api/activity/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..46336930ef693e29d9d5696ee3ccf12446c2bdad
--- /dev/null
+++ b/api/funkwhale_api/activity/utils.py
@@ -0,0 +1,64 @@
+from django.db import models
+
+from funkwhale_api.common import fields
+from funkwhale_api.favorites.models import TrackFavorite
+from funkwhale_api.history.models import Listening
+
+
+def combined_recent(limit, **kwargs):
+    datetime_field = kwargs.pop('datetime_field', 'creation_date')
+    source_querysets = {
+        qs.model._meta.label: qs for qs in kwargs.pop('querysets')
+    }
+    querysets = {
+        k: qs.annotate(
+            __type=models.Value(
+                qs.model._meta.label, output_field=models.CharField()
+            )
+        ).values('pk', datetime_field, '__type')
+        for k, qs in source_querysets.items()
+    }
+    _qs_list = list(querysets.values())
+    union_qs = _qs_list[0].union(*_qs_list[1:])
+    records = []
+    for row in union_qs.order_by('-{}'.format(datetime_field))[:limit]:
+        records.append({
+            'type': row['__type'],
+            'when': row[datetime_field],
+            'pk': row['pk']
+        })
+    # Now we bulk-load each object type in turn
+    to_load = {}
+    for record in records:
+        to_load.setdefault(record['type'], []).append(record['pk'])
+    fetched = {}
+
+    for key, pks in to_load.items():
+        for item in source_querysets[key].filter(pk__in=pks):
+            fetched[(key, item.pk)] = item
+
+    # Annotate 'records' with loaded objects
+    for record in records:
+        record['object'] = fetched[(record['type'], record['pk'])]
+    return records
+
+
+def get_activity(user, limit=20):
+    query = fields.privacy_level_query(
+        user, lookup_field='user__privacy_level')
+    querysets = [
+        Listening.objects.filter(query).select_related(
+            'track',
+            'user',
+            'track__artist',
+            'track__album__artist',
+        ),
+        TrackFavorite.objects.filter(query).select_related(
+            'track',
+            'user',
+            'track__artist',
+            'track__album__artist',
+        ),
+    ]
+    records = combined_recent(limit=limit, querysets=querysets)
+    return [r['object'] for r in records]
diff --git a/api/funkwhale_api/activity/views.py b/api/funkwhale_api/activity/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..e66de1ccfdc94f51cd823fa5c6b104488a4aad7f
--- /dev/null
+++ b/api/funkwhale_api/activity/views.py
@@ -0,0 +1,20 @@
+from rest_framework import viewsets
+from rest_framework.response import Response
+
+from funkwhale_api.common.permissions import ConditionalAuthentication
+from funkwhale_api.favorites.models import TrackFavorite
+
+from . import serializers
+from . import utils
+
+
+class ActivityViewSet(viewsets.GenericViewSet):
+
+    serializer_class = serializers.AutoSerializer
+    permission_classes = [ConditionalAuthentication]
+    queryset = TrackFavorite.objects.none()
+
+    def list(self, request, *args, **kwargs):
+        activity = utils.get_activity(user=request.user)
+        serializer = self.serializer_class(activity, many=True)
+        return Response({'results': serializer.data}, status=200)
diff --git a/api/tests/activity/test_serializers.py b/api/tests/activity/test_serializers.py
new file mode 100644
index 0000000000000000000000000000000000000000..792fa74b9cbb3ed778c5e84bd746fb210e738acf
--- /dev/null
+++ b/api/tests/activity/test_serializers.py
@@ -0,0 +1,17 @@
+from funkwhale_api.activity import serializers
+from funkwhale_api.favorites.serializers import TrackFavoriteActivitySerializer
+from funkwhale_api.history.serializers import \
+    ListeningActivitySerializer
+
+
+def test_autoserializer(factories):
+    favorite = factories['favorites.TrackFavorite']()
+    listening = factories['history.Listening']()
+    objects = [favorite, listening]
+    serializer = serializers.AutoSerializer(objects, many=True)
+    expected = [
+        TrackFavoriteActivitySerializer(favorite).data,
+        ListeningActivitySerializer(listening).data,
+    ]
+
+    assert serializer.data == expected
diff --git a/api/tests/activity/test_utils.py b/api/tests/activity/test_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..43bb45df84931ccd3ba2a56e555b991627c3a62c
--- /dev/null
+++ b/api/tests/activity/test_utils.py
@@ -0,0 +1,21 @@
+from funkwhale_api.activity import utils
+
+
+def test_get_activity(factories):
+    user = factories['users.User']()
+    listening = factories['history.Listening']()
+    favorite = factories['favorites.TrackFavorite']()
+
+    objects = list(utils.get_activity(user))
+    assert objects == [favorite, listening]
+
+
+def test_get_activity_honors_privacy_level(factories, anonymous_user):
+    listening = factories['history.Listening'](user__privacy_level='me')
+    favorite1 = factories['favorites.TrackFavorite'](
+        user__privacy_level='everyone')
+    favorite2 = factories['favorites.TrackFavorite'](
+        user__privacy_level='instance')
+
+    objects = list(utils.get_activity(anonymous_user))
+    assert objects == [favorite1]
diff --git a/api/tests/activity/test_views.py b/api/tests/activity/test_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..bdc3c6339ffe91981621c8f8272788347a01cc8e
--- /dev/null
+++ b/api/tests/activity/test_views.py
@@ -0,0 +1,18 @@
+from django.urls import reverse
+
+from funkwhale_api.activity import serializers
+from funkwhale_api.activity import utils
+
+
+def test_activity_view(factories, api_client, settings, anonymous_user):
+    settings.API_AUTHENTICATION_REQUIRED = False
+    favorite = factories['favorites.TrackFavorite'](
+        user__privacy_level='everyone')
+    listening = factories['history.Listening']()
+    url = reverse('api:v1:activity-list')
+    objects = utils.get_activity(anonymous_user)
+    serializer = serializers.AutoSerializer(objects, many=True)
+    response = api_client.get(url)
+
+    assert response.status_code == 200
+    assert response.data['results'] == serializer.data