diff --git a/api/funkwhale_api/federation/contexts.py b/api/funkwhale_api/federation/contexts.py
index 0873bcd46b04d715e861a9244a1327187d50167d..b3fc112f0e0c233595ba9a63d4af9bf6bee73d2a 100644
--- a/api/funkwhale_api/federation/contexts.py
+++ b/api/funkwhale_api/federation/contexts.py
@@ -214,6 +214,7 @@ CONTEXTS = [
                 "shares": {"@id": "as:shares", "@type": "@id"},
                 # Added manually
                 "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+                "Hashtag": "as:Hashtag",
             }
         },
     },
diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py
index f7210e324bc2092b99a4e692b4d63bdc43ad7978..d975dfaec0304485fbdf70ee45f4ee8ee6a6d5dc 100644
--- a/api/funkwhale_api/federation/serializers.py
+++ b/api/funkwhale_api/federation/serializers.py
@@ -11,6 +11,7 @@ from funkwhale_api.common import utils as funkwhale_utils
 from funkwhale_api.music import licenses
 from funkwhale_api.music import models as music_models
 from funkwhale_api.music import tasks as music_tasks
+from funkwhale_api.tags import models as tags_models
 
 from . import activity, actors, contexts, jsonld, models, tasks, utils
 
@@ -781,6 +782,20 @@ MUSIC_ENTITY_JSONLD_MAPPING = {
 }
 
 
+class TagSerializer(jsonld.JsonLdSerializer):
+    type = serializers.ChoiceField(choices=[contexts.AS.Hashtag])
+    name = serializers.CharField(max_length=100)
+
+    class Meta:
+        jsonld_mapping = {"name": jsonld.first_val(contexts.AS.name)}
+
+    def validate_name(self, value):
+        if value.startswith("#"):
+            # remove trailing #
+            value = value[1:]
+        return value
+
+
 class MusicEntitySerializer(jsonld.JsonLdSerializer):
     id = serializers.URLField(max_length=500)
     published = serializers.DateTimeField()
@@ -797,8 +812,10 @@ class MusicEntitySerializer(jsonld.JsonLdSerializer):
             self.updateable_fields, validated_data, instance
         )
         if updated_fields:
-            return music_tasks.update_library_entity(instance, updated_fields)
+            music_tasks.update_library_entity(instance, updated_fields)
 
+        tags = [t["name"] for t in validated_data.get("tags", []) or []]
+        tags_models.set_tags(instance, *tags)
         return instance
 
 
@@ -892,6 +909,9 @@ class TrackSerializer(MusicEntitySerializer):
     album = AlbumSerializer()
     license = serializers.URLField(allow_null=True, required=False)
     copyright = serializers.CharField(allow_null=True, required=False)
+    tags = serializers.ListField(
+        child=TagSerializer(), min_length=0, required=False, allow_null=True
+    )
 
     updateable_fields = [
         ("name", "title"),
@@ -914,6 +934,7 @@ class TrackSerializer(MusicEntitySerializer):
                 "disc": jsonld.first_val(contexts.FW.disc),
                 "license": jsonld.first_id(contexts.FW.license),
                 "position": jsonld.first_val(contexts.FW.position),
+                "tags": jsonld.raw(contexts.AS.tag),
             },
         )
 
@@ -941,6 +962,12 @@ class TrackSerializer(MusicEntitySerializer):
             "attributedTo": instance.attributed_to.fid
             if instance.attributed_to
             else None,
+            "tag": [
+                {"type": "Hashtag", "name": "#{}".format(tag)}
+                for tag in sorted(
+                    instance.tagged_items.values_list("tag__name", flat=True)
+                )
+            ],
         }
 
         if self.context.get("include_ap_context", self.parent is None):
@@ -950,6 +977,7 @@ class TrackSerializer(MusicEntitySerializer):
     def create(self, validated_data):
         from funkwhale_api.music import tasks as music_tasks
 
+        tags = [t["name"] for t in validated_data.get("tags", []) or []]
         references = {}
         actors_to_fetch = set()
         actors_to_fetch.add(
@@ -981,7 +1009,6 @@ class TrackSerializer(MusicEntitySerializer):
             if not url:
                 continue
             references[url] = actors.get_actor(url)
-
         metadata = music_tasks.federation_audio_track_to_metadata(
             validated_data, references
         )
@@ -990,6 +1017,7 @@ class TrackSerializer(MusicEntitySerializer):
         if from_activity:
             metadata["from_activity_id"] = from_activity.pk
         track = music_tasks.get_track_from_import_metadata(metadata, update_cover=True)
+        tags_models.add_tags(track, *tags)
         return track
 
     def update(self, obj, validated_data):
diff --git a/api/tests/federation/test_serializers.py b/api/tests/federation/test_serializers.py
index 43b465d5480fa0096ea8fcc99e889fdde3826a0e..140f31528760dd09f1308f69546260b4767ec8b9 100644
--- a/api/tests/federation/test_serializers.py
+++ b/api/tests/federation/test_serializers.py
@@ -604,7 +604,11 @@ def test_activity_pub_album_serializer_to_ap(factories):
 
 def test_activity_pub_track_serializer_to_ap(factories):
     track = factories["music.Track"](
-        license="cc-by-4.0", copyright="test", disc_number=3, attributed=True
+        license="cc-by-4.0",
+        copyright="test",
+        disc_number=3,
+        attributed=True,
+        set_tags=["Punk", "Rock"],
     )
     expected = {
         "@context": jsonld.get_default_context(),
@@ -626,6 +630,10 @@ def test_activity_pub_track_serializer_to_ap(factories):
             track.album, context={"include_ap_context": False}
         ).data,
         "attributedTo": track.attributed_to.fid,
+        "tag": [
+            {"type": "Hashtag", "name": "#Punk"},
+            {"type": "Hashtag", "name": "#Rock"},
+        ],
     }
     serializer = serializers.TrackSerializer(track)
 
@@ -633,6 +641,7 @@ def test_activity_pub_track_serializer_to_ap(factories):
 
 
 def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
+    add_tags = mocker.patch("funkwhale_api.tags.models.add_tags")
     track_attributed_to = factories["federation.Actor"]()
     album_attributed_to = factories["federation.Actor"]()
     album_artist_attributed_to = factories["federation.Actor"]()
@@ -685,6 +694,10 @@ def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
                 "published": published.isoformat(),
             }
         ],
+        "tag": [
+            {"type": "Hashtag", "name": "#Hello"},
+            {"type": "Hashtag", "name": "World"},
+        ],
     }
     r_mock.get(data["album"]["cover"]["href"], body=io.BytesIO(b"coucou"))
     serializer = serializers.TrackSerializer(data=data, context={"activity": activity})
@@ -728,6 +741,48 @@ def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
     assert album_artist.creation_date == published
     assert album_artist.attributed_to == album_artist_attributed_to
 
+    add_tags.assert_called_once_with(track, *["Hello", "World"])
+
+
+def test_activity_pub_track_serializer_from_ap_update(factories, r_mock, mocker):
+    set_tags = mocker.patch("funkwhale_api.tags.models.set_tags")
+    track_attributed_to = factories["federation.Actor"]()
+    track = factories["music.Track"]()
+
+    published = timezone.now()
+    data = {
+        "@context": jsonld.get_default_context(),
+        "type": "Track",
+        "id": track.fid,
+        "published": published.isoformat(),
+        "musicbrainzId": str(uuid.uuid4()),
+        "name": "Black in back",
+        "position": 5,
+        "disc": 2,
+        "attributedTo": track_attributed_to.fid,
+        "album": serializers.AlbumSerializer(track.album).data,
+        "artists": [serializers.ArtistSerializer(track.artist).data],
+        "tag": [
+            {"type": "Hashtag", "name": "#Hello"},
+            # Ensure we can handle tags without a leading #
+            {"type": "Hashtag", "name": "World"},
+        ],
+    }
+    serializer = serializers.TrackSerializer(track, data=data)
+    assert serializer.is_valid(raise_exception=True)
+
+    serializer.save()
+    track.refresh_from_db()
+
+    assert track.fid == data["id"]
+    assert track.title == data["name"]
+    assert track.position == data["position"]
+    assert track.disc_number == data["disc"]
+    assert track.attributed_to == track_attributed_to
+    assert str(track.mbid) == data["musicbrainzId"]
+
+    set_tags.assert_called_once_with(track, *["Hello", "World"])
+
 
 def test_activity_pub_upload_serializer_from_ap(factories, mocker, r_mock):
     activity = factories["federation.Activity"]()