diff --git a/api/config/api_urls.py b/api/config/api_urls.py
index cab6805b67e394838ec942ecf8b162edb08a88cf..cf5b03744d8b12d5a91fd43df4edbad941870389 100644
--- a/api/config/api_urls.py
+++ b/api/config/api_urls.py
@@ -32,6 +32,10 @@ v1_patterns += [
         include(
             ('funkwhale_api.instance.urls', 'instance'),
             namespace='instance')),
+    url(r'^federation/',
+        include(
+            ('funkwhale_api.federation.api_urls', 'federation'),
+            namespace='federation')),
     url(r'^providers/',
         include(
             ('funkwhale_api.providers.urls', 'providers'),
diff --git a/api/funkwhale_api/federation/api_urls.py b/api/funkwhale_api/federation/api_urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..ecb5c38f13cad0b3a011242182e2fd6202252c9a
--- /dev/null
+++ b/api/funkwhale_api/federation/api_urls.py
@@ -0,0 +1,11 @@
+from rest_framework import routers
+
+from . import views
+
+router = routers.SimpleRouter()
+router.register(
+    r'libraries',
+    views.LibraryViewSet,
+    'libraries')
+
+urlpatterns = router.urls
diff --git a/api/funkwhale_api/federation/library.py b/api/funkwhale_api/federation/library.py
new file mode 100644
index 0000000000000000000000000000000000000000..13608098b4137ea3afc55c37d0bd08a7ab8d3c45
--- /dev/null
+++ b/api/funkwhale_api/federation/library.py
@@ -0,0 +1,97 @@
+import requests
+
+from funkwhale_api.common import session
+
+from . import actors
+from . import serializers
+from . import signing
+from . import webfinger
+
+
+def scan_from_account_name(account_name):
+    """
+    Given an account name such as library@test.library, will:
+
+    1. Perform the webfinger lookup
+    2. Perform the actor lookup
+    3. Perform the library's collection lookup
+
+    and return corresponding data in a dictionary.
+    """
+
+    data = {}
+    try:
+        data['webfinger'] = webfinger.get_resource(
+            'acct:{}'.format(account_name))
+    except requests.ConnectionError:
+        return {
+            'webfinger': {
+                'errors': ['This webfinger resource is not reachable']
+            }
+        }
+    except requests.HTTPError as e:
+        return {
+            'webfinger': {
+                'errors': [
+                    'Error {} during webfinger request'.format(
+                        e.response.status_code)]
+            }
+        }
+
+    try:
+        data['actor'] = actors.get_actor_data(data['webfinger']['actor_url'])
+    except requests.ConnectionError:
+        data['actor'] = {
+            'errors': ['This actor is not reachable']
+        }
+        return data
+    except requests.HTTPError as e:
+        data['actor'] = {
+            'errors': [
+                'Error {} during actor request'.format(
+                    e.response.status_code)]
+        }
+        return data
+
+    serializer = serializers.LibraryActorSerializer(data=data['actor'])
+    serializer.is_valid(raise_exception=True)
+    data['library'] = get_library_data(
+        serializer.validated_data['library_url'])
+
+    return data
+
+
+def get_library_data(library_url):
+    actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    auth = signing.get_auth(actor.private_key, actor.private_key_id)
+    try:
+        response = session.get_session().get(
+            library_url,
+            auth=auth,
+            timeout=5,
+            headers={
+                'Content-Type': 'application/activity+json'
+            }
+        )
+    except requests.ConnectionError:
+        return {
+            'errors': ['This library is not reachable']
+        }
+    scode = response.status_code
+    if scode == 401:
+        return {
+            'errors': ['This library requires authentication']
+        }
+    elif scode == 403:
+        return {
+            'errors': ['Permission denied while scanning library']
+        }
+    elif scode >= 400:
+        return {
+            'errors': ['Error {} while fetching the library'.format(scode)]
+        }
+    serializer = serializers.PaginatedCollectionSerializer(
+        data=response.json(),
+    )
+    serializer.is_valid(raise_exception=True)
+    return serializer.validated_data
diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py
index 7e84e575ac44e10a79058df46f2e3232fa015159..704ad63645012c8642060bd328bdcb8b0c29e78e 100644
--- a/api/funkwhale_api/federation/serializers.py
+++ b/api/funkwhale_api/federation/serializers.py
@@ -27,8 +27,10 @@ class ActorSerializer(serializers.ModelSerializer):
     id = serializers.URLField(source='url')
     outbox = serializers.URLField(source='outbox_url')
     inbox = serializers.URLField(source='inbox_url')
-    following = serializers.URLField(source='following_url', required=False)
-    followers = serializers.URLField(source='followers_url', required=False)
+    following = serializers.URLField(
+        source='following_url', required=False, allow_null=True)
+    followers = serializers.URLField(
+        source='followers_url', required=False, allow_null=True)
     preferredUsername = serializers.CharField(
         source='preferred_username', required=False)
     publicKey = serializers.JSONField(source='public_key', required=False)
@@ -94,6 +96,31 @@ class ActorSerializer(serializers.ModelSerializer):
             return value[:500]
 
 
+class LibraryActorSerializer(ActorSerializer):
+    url = serializers.ListField(
+        child=serializers.JSONField())
+
+    class Meta(ActorSerializer.Meta):
+        fields = ActorSerializer.Meta.fields + ['url']
+
+    def validate(self, validated_data):
+        try:
+            urls = validated_data['url']
+        except KeyError:
+            raise serializers.ValidationError('Missing URL field')
+
+        for u in urls:
+            try:
+                if u['name'] != 'library':
+                    continue
+                validated_data['library_url'] = u['href']
+                break
+            except KeyError:
+                continue
+
+        return validated_data
+
+
 class FollowSerializer(serializers.ModelSerializer):
     # left maps to activitypub fields, right to our internal models
     id = serializers.URLField(source='get_federation_url')
@@ -226,7 +253,6 @@ OBJECT_SERIALIZERS = {
 class PaginatedCollectionSerializer(serializers.Serializer):
     type = serializers.ChoiceField(choices=['Collection'])
     totalItems = serializers.IntegerField(min_value=0)
-    items = serializers.ListField()
     actor = serializers.URLField()
     id = serializers.URLField()
 
diff --git a/api/funkwhale_api/federation/views.py b/api/funkwhale_api/federation/views.py
index da2b193a2d0619d8617fa2656a8e76689f406977..aaab343e40350cd13980d4111c9d0015d94a747b 100644
--- a/api/funkwhale_api/federation/views.py
+++ b/api/funkwhale_api/federation/views.py
@@ -4,15 +4,18 @@ from django.core import paginator
 from django.http import HttpResponse
 from django.urls import reverse
 
-from rest_framework import viewsets
-from rest_framework import views
+from rest_framework import permissions as rest_permissions
 from rest_framework import response
+from rest_framework import views
+from rest_framework import viewsets
 from rest_framework.decorators import list_route, detail_route
 
 from funkwhale_api.music.models import TrackFile
 
 from . import actors
 from . import authentication
+from . import library
+from . import models
 from . import permissions
 from . import renderers
 from . import serializers
@@ -154,3 +157,18 @@ class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet):
                 return response.Response(status=404)
 
         return response.Response(data)
+
+
+class LibraryViewSet(viewsets.GenericViewSet):
+    permission_classes = [rest_permissions.DjangoModelPermissions]
+    queryset = models.Library.objects.all()
+
+    @list_route(methods=['get'])
+    def scan(self, request, *args, **kwargs):
+        account = request.GET.get('account')
+        if not account:
+            return response.Response(
+                {'account': 'This field is mandatory'}, status=400)
+
+        data = library.scan_from_account_name(account)
+        return response.Response(data)
diff --git a/api/funkwhale_api/federation/webfinger.py b/api/funkwhale_api/federation/webfinger.py
index 444998b945feb5d258d0ae1d082b00f0e82c2887..d4170a4318f35c823d52d9c49871193f308826bd 100644
--- a/api/funkwhale_api/federation/webfinger.py
+++ b/api/funkwhale_api/federation/webfinger.py
@@ -36,7 +36,7 @@ def clean_acct(acct_string, ensure_local=True):
         raise forms.ValidationError(
             'Invalid hostname {}'.format(hostname))
 
-    if username not in actors.SYSTEM_ACTORS:
+    if ensure_local and username not in actors.SYSTEM_ACTORS:
         raise forms.ValidationError('Invalid username')
 
     return username, hostname
diff --git a/api/tests/federation/test_library.py b/api/tests/federation/test_library.py
new file mode 100644
index 0000000000000000000000000000000000000000..714a0c306d582eb79a7305906b3bab825be6924c
--- /dev/null
+++ b/api/tests/federation/test_library.py
@@ -0,0 +1,66 @@
+from funkwhale_api.federation import library
+from funkwhale_api.federation import serializers
+
+
+def test_library_scan_from_account_name(mocker, factories):
+    actor = factories['federation.Actor'](
+        preferred_username='library',
+        domain='test.library'
+    )
+    get_resource_result = {'actor_url': actor.url}
+    get_resource = mocker.patch(
+        'funkwhale_api.federation.webfinger.get_resource',
+        return_value=get_resource_result)
+
+    actor_data = serializers.ActorSerializer(actor).data
+    actor_data['manuallyApprovesFollowers'] = False
+    actor_data['url'] = [{
+        'type': 'Link',
+        'name': 'library',
+        'mediaType': 'application/activity+json',
+        'href': 'https://test.library'
+    }]
+    get_actor_data = mocker.patch(
+        'funkwhale_api.federation.actors.get_actor_data',
+        return_value=actor_data)
+
+    get_library_data_result = {'test': 'test'}
+    get_library_data = mocker.patch(
+        'funkwhale_api.federation.library.get_library_data',
+        return_value=get_library_data_result)
+
+    result = library.scan_from_account_name('library@test.actor')
+
+    get_resource.assert_called_once_with('acct:library@test.actor')
+    get_actor_data.assert_called_once_with(actor.url)
+    get_library_data.assert_called_once_with(actor_data['url'][0]['href'])
+
+    assert result == {
+        'webfinger': get_resource_result,
+        'actor': actor_data,
+        'library': get_library_data_result,
+    }
+
+
+def test_get_library_data(r_mock, factories):
+    actor = factories['federation.Actor']()
+    url = 'https://test.library'
+    conf = {
+        'id': url,
+        'items': [],
+        'actor': actor,
+        'page_size': 5,
+    }
+    data = serializers.PaginatedCollectionSerializer(conf).data
+    r_mock.get(url, json=data)
+
+    result = library.get_library_data(url)
+    for f in ['totalItems', 'actor', 'id', 'type']:
+        assert result[f] == data[f]
+
+
+def test_get_library_data_requires_authentication(r_mock, factories):
+    url = 'https://test.library'
+    r_mock.get(url, status_code=403)
+    result = library.get_library_data(url)
+    assert result['errors'] == ['This library requires authentication']
diff --git a/api/tests/federation/test_views.py b/api/tests/federation/test_views.py
index b3fd85910040bb2e045f3517afcb938685e49eec..0b58e20f145d5a78acd7768e01ab31c3da21352b 100644
--- a/api/tests/federation/test_views.py
+++ b/api/tests/federation/test_views.py
@@ -164,3 +164,18 @@ def test_library_actor_includes_library_link(db, settings, api_client):
     ]
     assert response.status_code == 200
     assert response.data['url'] == expected_links
+
+
+def test_can_scan_library(superuser_api_client, mocker):
+    result = {'test': 'test'}
+    scan = mocker.patch(
+        'funkwhale_api.federation.library.scan_from_account_name',
+        return_value=result)
+
+    url = reverse('api:v1:federation:libraries-scan')
+    response = superuser_api_client.get(
+        url, data={'account': 'test@test.library'})
+
+    assert response.status_code == 200
+    assert response.data == result
+    scan.assert_called_once_with('test@test.library')