diff --git a/src/core/locale/de/LC_MESSAGES/django.po b/src/core/locale/de/LC_MESSAGES/django.po
index b4539d5ac811cda06e218c61a9fdf19a389ea814..688dfe079bea5801a2835f4d419f2b64f42aa3ba 100644
--- a/src/core/locale/de/LC_MESSAGES/django.po
+++ b/src/core/locale/de/LC_MESSAGES/django.po
@@ -2261,6 +2261,9 @@ msgstr "Telefon/Handy"
 msgid "UserCommunicationChannel__channel-matrix"
 msgstr "Matrix (Riot/Element)"
 
+msgid "UserCommunicationChannel__channel-activitypub"
+msgstr "ActivityPub (z. B. Mastodon)"
+
 msgid "UserCommunicationChannel__channel__help"
 msgstr "Art des Kommunikationskanals"
 
@@ -2273,6 +2276,12 @@ msgstr "Kontaktangabe in kanalspezifischer Notation"
 msgid "UserCommunicationChannel__address"
 msgstr "Ziel"
 
+msgid "UserCommunicationChannel__caption__help"
+msgstr "Freitext/Bezeichnung für den Kanal, wird ggf. öffentlich eingeblendet"
+
+msgid "UserCommunicationChannel__caption"
+msgstr "Beschriftung"
+
 msgid "UserCommunicationChannel__is_verified__help"
 msgstr "die Kontaktadresse wurde verifiziert"
 
diff --git a/src/core/locale/en/LC_MESSAGES/django.po b/src/core/locale/en/LC_MESSAGES/django.po
index a14e196906e77746c661aa02694907522064148a..ea7d8e254c5858b570ea1801da22fc34eaa61c0b 100644
--- a/src/core/locale/en/LC_MESSAGES/django.po
+++ b/src/core/locale/en/LC_MESSAGES/django.po
@@ -2259,6 +2259,9 @@ msgstr "phone/mobile"
 msgid "UserCommunicationChannel__channel-matrix"
 msgstr "Matrix (Riot/Element)"
 
+msgid "UserCommunicationChannel__channel-activitypub"
+msgstr "ActivityPub (e.g. Mastodon)"
+
 msgid "UserCommunicationChannel__channel__help"
 msgstr "the type of communication channel"
 
@@ -2271,6 +2274,12 @@ msgstr "contact address in channel-specific notation"
 msgid "UserCommunicationChannel__address"
 msgstr "Address"
 
+msgid "UserCommunicationChannel__caption__help"
+msgstr "description of this communication channel, might be shown publicly"
+
+msgid "UserCommunicationChannel__caption"
+msgstr "caption"
+
 msgid "UserCommunicationChannel__is_verified__help"
 msgstr "this contact detail has been verified"
 
diff --git a/src/core/migrations/0160_usercommunicationchannel_caption_and_more.py b/src/core/migrations/0160_usercommunicationchannel_caption_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..98d0703d8b8549d8f1b4cc14df0258ab5ac41fff
--- /dev/null
+++ b/src/core/migrations/0160_usercommunicationchannel_caption_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.1.2 on 2024-11-17 17:34
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0159_event_banner_image_url_alter_event_banner_image'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='usercommunicationchannel',
+            name='caption',
+            field=models.CharField(blank=True, help_text='UserCommunicationChannel__caption__help', max_length=200, null=True, verbose_name='UserCommunicationChannel__caption'),
+        ),
+        migrations.AlterField(
+            model_name='usercommunicationchannel',
+            name='channel',
+            field=models.CharField(choices=[('mail', 'UserCommunicationChannel__channel-mail'), ('xmpp', 'UserCommunicationChannel__channel-xmpp'), ('irc', 'UserCommunicationChannel__channel-irc'), ('dect', 'UserCommunicationChannel__channel-dect'), ('phone', 'UserCommunicationChannel__channel-phone'), ('matrix', 'UserCommunicationChannel__channel-matrix'), ('activitypub', 'UserCommunicationChannel__channel-activitypub')], help_text='UserCommunicationChannel__channel__help', max_length=20, verbose_name='UserCommunicationChannel__channel'),
+        ),
+    ]
diff --git a/src/core/models/users.py b/src/core/models/users.py
index 64502faea558f7a34d9a20f0ba1190a5a20d548f..717f9827668f360941c16dd742c1da1ae4b6b614 100644
--- a/src/core/models/users.py
+++ b/src/core/models/users.py
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any
 from uuid import uuid4
 
 from timezone_field import TimeZoneField
+from urllib3.util import parse_url
 
 from django.conf import settings
 from django.contrib.auth.models import AbstractUser, UserManager
@@ -576,6 +577,7 @@ class UserCommunicationChannel(models.Model):
         DECT = 'dect', _('UserCommunicationChannel__channel-dect')
         PHONE = 'phone', _('UserCommunicationChannel__channel-phone')
         MATRIX = 'matrix', _('UserCommunicationChannel__channel-matrix')
+        ACTIVITYPUB = 'activitypub', _('UserCommunicationChannel__channel-activitypub')
 
     user = models.ForeignKey(PlatformUser, related_name='communication_channels', on_delete=models.CASCADE)
 
@@ -584,6 +586,10 @@ class UserCommunicationChannel(models.Model):
     )
     address = models.CharField(max_length=255, help_text=_('UserCommunicationChannel__address__help'), verbose_name=_('UserCommunicationChannel__address'))
 
+    caption = models.CharField(
+        max_length=200, blank=True, null=True, help_text=_('UserCommunicationChannel__caption__help'), verbose_name=_('UserCommunicationChannel__caption')
+    )
+
     is_verified = models.BooleanField(
         default=False, help_text=_('UserCommunicationChannel__is_verified__help'), verbose_name=_('UserCommunicationChannel__is_verified')
     )
@@ -625,6 +631,14 @@ class UserCommunicationChannel(models.Model):
         if _RE_TELEPHONE.match(address) is None and _RE_VANITY.match(address) is None:
             raise ValidationError({'address': 'expected an international telephone number (starting with + or 00)'})
 
+    @staticmethod
+    def validate_activitypub_url(address):
+        url = parse_url(address)
+        if url.scheme not in ['http', 'https']:
+            raise ValidationError({'address': 'Expected https:// URL.'})
+        if url.host is None or url.path is None:
+            raise ValidationError({'address': 'Expected valid URL.'})
+
     @property
     def can_notify(self):
         """Signals whether this channel can be used for notifications."""
@@ -644,6 +658,9 @@ class UserCommunicationChannel(models.Model):
             if self.use_for_notifications:
                 raise ValidationError({'use_for_notifications': _('UserCommunicationChannel__cannot_notify__phone')})
 
+        elif self.channel == self.Channel.ACTIVITYPUB:
+            self.validate_activitypub_url(self.address)
+
         # TODO: verify the other channel types for correct syntax as well
 
         if self.use_for_notifications:
diff --git a/src/core/tests/users.py b/src/core/tests/users.py
index d1047ef2226ac09a739a0a308feaf5587c91e895..8f7d86b228aba0fc438f70386b91d574d5547b1c 100644
--- a/src/core/tests/users.py
+++ b/src/core/tests/users.py
@@ -72,6 +72,23 @@ class UserCommunicationChannelStaticTests(TestCase):
 
         self.check_good_bad('phone number', UserCommunicationChannel.validate_phone_number, good_numbers, bad_numbers)
 
+    def test_activitypub_validation(self):
+        good_addresses = [
+            'https://chaos.social/@ordnung',
+            'https://chaos.social/@events/113499399916580165',
+            'https://social.mrtoto.net/@apptest',
+            'https://social.mrtoto.net/@apptest/113085273939532302',
+        ]
+        bad_addresses = [
+            '',
+            'foo',
+            'chaos.social/ordnung',
+            'chaos.social/@ordnung',
+            'ftp://ccc.de',
+            'hub@cccv.de',
+        ]
+        self.check_good_bad('ActivityPub URL', UserCommunicationChannel.validate_activitypub_url, good_addresses, bad_addresses)
+
 
 class UserCommunicationChannelTests(TestCase):
     def setUp(self):