diff --git a/arkindex/documents/api/elements.py b/arkindex/documents/api/elements.py
index cc324ab4394bca199b369ef1a938a3319f575208..ffe6b289016846c7c17aad8dddfc64d8176359d5 100644
--- a/arkindex/documents/api/elements.py
+++ b/arkindex/documents/api/elements.py
@@ -176,6 +176,16 @@ class ElementsListAutoSchema(AutoSchema):
                         ]
                     }
                 ),
+                OpenApiParameter(
+                    'metadata_name',
+                    description='Restrict to elements having a metadata with the given name.',
+                    required=False,
+                ),
+                OpenApiParameter(
+                    'metadata_value',
+                    description='Restrict to elements having a metadata with the given value. Requires `metadata_name` to be set.',
+                    required=False,
+                ),
                 OpenApiParameter(
                     'with_best_classes',
                     description='Returns best classifications for each element. '
@@ -319,10 +329,52 @@ class ElementsListBase(CorpusACLMixin, DestroyModelMixin, ListAPIView):
 
         return element_type
 
+    def get_worker_version_filter(self):
+        # If worker_version query param is False, we have to return elements created by humans
+        if self.request.query_params['worker_version'].lower() in ('false', '0'):
+            return None
+
+        try:
+            worker_version_id = uuid.UUID(self.request.query_params['worker_version'])
+        except (TypeError, ValueError):
+            raise ValidationError(['Invalid UUID'])
+
+        if not WorkerVersion.objects.filter(id=worker_version_id).exists():
+            raise ValidationError(['This worker version does not exist.'])
+
+        return worker_version_id
+
+    def get_metadata_queryset(self):
+        """
+        Returns a queryset that includes matched element IDs from metadata filters,
+        or None if no metadata filters apply.
+        """
+        name, value = self.clean_params.get('metadata_name'), self.clean_params.get('metadata_value')
+        if not name and not value:
+            return
+
+        queryset = MetaData.objects.all()
+        errors = defaultdict(list)
+
+        if name:
+            queryset = queryset.filter(name=name)
+
+        if value:
+            if name:
+                queryset = queryset.filter(value=value)
+            else:
+                errors['metadata_value'].append('This filter is not supported without metadata_name.')
+
+        if errors:
+            raise ValidationError(errors)
+
+        return queryset.values('element_id')
+
     def get_filters(self):
         filters = {
             'corpus': self.selected_corpus
         }
+        errors = {}
 
         if 'name' in self.request.query_params:
             filters['name__icontains'] = self.clean_params['name']
@@ -333,19 +385,21 @@ class ElementsListBase(CorpusACLMixin, DestroyModelMixin, ListAPIView):
             filters['type__folder'] = self.folder_filter
 
         if 'worker_version' in self.clean_params:
-            # If worker_version query param is False, we have to return elements created by humans
-            if self.request.query_params['worker_version'].lower() in ('false', '0'):
-                filters['worker_version'] = None
-            else:
-                try:
-                    worker_version_id = uuid.UUID(self.request.query_params['worker_version'])
-                except (TypeError, ValueError):
-                    raise ValidationError({'worker_version': ['Invalid UUID']})
+            try:
+                filters['worker_version_id'] = self.get_worker_version_filter()
+            except ValidationError as e:
+                errors['worker_version'] = e.detail
 
-                if not WorkerVersion.objects.filter(id=worker_version_id).exists():
-                    raise ValidationError({'worker_version': ['This worker version does not exist.']})
+        try:
+            metadata_queryset = self.get_metadata_queryset()
+        except ValidationError as e:
+            errors.update(e.detail)
+        else:
+            if metadata_queryset is not None:
+                filters['id__in'] = metadata_queryset
 
-                filters['worker_version_id'] = worker_version_id
+        if errors:
+            raise ValidationError(errors)
 
         return filters
 
diff --git a/arkindex/documents/tests/test_children_elements.py b/arkindex/documents/tests/test_children_elements.py
index 9a01b442fe7eb4482f50a897bebad22ad5f7e6a4..2aea912c0ef4127a656691b55bc27bf5bece6857 100644
--- a/arkindex/documents/tests/test_children_elements.py
+++ b/arkindex/documents/tests/test_children_elements.py
@@ -343,3 +343,42 @@ class TestChildrenElements(FixtureAPITestCase):
             {r['type'] for r in data['results']},
             {'act', 'page'},
         )
+
+    def test_children_filter_metadata_name(self):
+        with self.assertNumQueries(6):
+            response = self.client.get(
+                reverse('api:elements-children', kwargs={'pk': str(self.vol.id)}),
+                data={'metadata_name': 'folio'}
+            )
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertListEqual(
+            [element['name'] for element in response.json()['results']],
+            [
+                'Volume 1, page 1r',
+                'Volume 1, page 1v',
+                'Volume 1, page 2r',
+            ]
+        )
+
+    def test_children_filter_metadata_value(self):
+        with self.assertNumQueries(1):
+            response = self.client.get(
+                reverse('api:elements-children', kwargs={'pk': str(self.vol.id)}),
+                data={'metadata_value': '1v'}
+            )
+            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+        self.assertDictEqual(response.json(), {
+            'metadata_value': ['This filter is not supported without metadata_name.'],
+        })
+
+    def test_children_filter_metadata_name_value(self):
+        with self.assertNumQueries(6):
+            response = self.client.get(
+                reverse('api:elements-children', kwargs={'pk': str(self.vol.id)}),
+                data={'metadata_name': 'folio', 'metadata_value': '1v'}
+            )
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertListEqual(
+            [element['name'] for element in response.json()['results']],
+            ['Volume 1, page 1v']
+        )
diff --git a/arkindex/documents/tests/test_corpus_elements.py b/arkindex/documents/tests/test_corpus_elements.py
index a76cc525ee74cb6991adfe25f3c32c4f21565d5e..3ded93fe6f449d2f13321cbb807bd846234215d8 100644
--- a/arkindex/documents/tests/test_corpus_elements.py
+++ b/arkindex/documents/tests/test_corpus_elements.py
@@ -345,6 +345,48 @@ class TestListElements(FixtureAPITestCase):
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.json()['count'], self.corpus.elements.count())
 
+    def test_list_elements_filter_metadata_name(self):
+        with self.assertNumQueries(5):
+            response = self.client.get(
+                reverse('api:corpus-elements', kwargs={'corpus': self.corpus.id}),
+                data={'metadata_name': 'folio'}
+            )
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertListEqual(
+            [element['name'] for element in response.json()['results']],
+            [
+                'Volume 1, page 1r',
+                'Volume 1, page 1v',
+                'Volume 1, page 2r',
+                'Volume 2, page 1r',
+                'Volume 2, page 1v',
+                'Volume 2, page 2r',
+            ]
+        )
+
+    def test_list_elements_filter_metadata_value(self):
+        with self.assertNumQueries(1):
+            response = self.client.get(
+                reverse('api:corpus-elements', kwargs={'corpus': self.corpus.id}),
+                data={'metadata_value': '1v'}
+            )
+            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+        self.assertDictEqual(response.json(), {
+            'metadata_value': ['This filter is not supported without metadata_name.'],
+        })
+
+    def test_list_elements_filter_metadata_name_value(self):
+        with self.assertNumQueries(5):
+            response = self.client.get(
+                reverse('api:corpus-elements', kwargs={'corpus': self.corpus.id}),
+                data={'metadata_name': 'folio', 'metadata_value': '1v'}
+            )
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertListEqual(
+            [element['name'] for element in response.json()['results']],
+            ['Volume 1, page 1v', 'Volume 2, page 1v']
+        )
+
     def test_list_elements_with_corpus_false(self):
         with self.assertNumQueries(5):
             response = self.client.get(
diff --git a/arkindex/documents/tests/test_parents_elements.py b/arkindex/documents/tests/test_parents_elements.py
index 08e25dc9a41dcd97cbeffca05d8a93d53e624476..d246973f680e7858c971b2d8a1e04d39dd168d0e 100644
--- a/arkindex/documents/tests/test_parents_elements.py
+++ b/arkindex/documents/tests/test_parents_elements.py
@@ -4,7 +4,7 @@ from django.urls import reverse
 from rest_framework import status
 
 from arkindex.dataimport.models import WorkerVersion
-from arkindex.documents.models import Corpus, Element
+from arkindex.documents.models import Corpus, Element, MetaType
 from arkindex.project.tests import FixtureAPITestCase
 
 
@@ -167,3 +167,47 @@ class TestParentsElements(FixtureAPITestCase):
             [r['name'] for r in response.json()['results']],
             ['Volume 1', ]
         )
+
+    def test_parents_filter_metadata_name(self):
+        element = self.corpus.elements.create(type=self.vol.type, name='Element with metadata')
+        element.metadatas.create(type=MetaType.Text, name='folio', value='42')
+        self.page.add_parent(element)
+
+        with self.assertNumQueries(4):
+            response = self.client.get(
+                reverse('api:elements-parents', kwargs={'pk': str(self.page.id)}),
+                data={'metadata_name': 'folio'}
+            )
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertListEqual(
+            [element['name'] for element in response.json()['results']],
+            ['Element with metadata']
+        )
+
+    def test_parents_filter_metadata_value(self):
+        with self.assertNumQueries(1):
+            response = self.client.get(
+                reverse('api:elements-parents', kwargs={'pk': str(self.page.id)}),
+                data={'metadata_value': '1v'}
+            )
+            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+        self.assertDictEqual(response.json(), {
+            'metadata_value': ['This filter is not supported without metadata_name.'],
+        })
+
+    def test_parents_filter_metadata_name_value(self):
+        element = self.corpus.elements.create(type=self.vol.type, name='Element with metadata')
+        element.metadatas.create(type=MetaType.Text, name='folio', value='42')
+        self.vol.metadatas.create(type=MetaType.Text, name='folio', value='43')
+        self.page.add_parent(element)
+
+        with self.assertNumQueries(4):
+            response = self.client.get(
+                reverse('api:elements-parents', kwargs={'pk': str(self.page.id)}),
+                data={'metadata_name': 'folio', 'metadata_value': '42'}
+            )
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertListEqual(
+            [element['name'] for element in response.json()['results']],
+            ['Element with metadata']
+        )