diff --git a/arkindex/documents/tests/test_put_elements.py b/arkindex/documents/tests/test_put_elements.py
new file mode 100644
index 0000000000000000000000000000000000000000..07a3e530cbc27459b68b29f06ab181e46e8e6f16
--- /dev/null
+++ b/arkindex/documents/tests/test_put_elements.py
@@ -0,0 +1,254 @@
+from django.urls import reverse
+from rest_framework import status
+
+from arkindex.documents.models import Corpus, Element
+from arkindex.images.models import ImageServer
+from arkindex.project.aws import S3FileStatus
+from arkindex.project.tests import FixtureAPITestCase
+from arkindex.users.models import Role, User
+
+
+class TestPatchElements(FixtureAPITestCase):
+
+    @classmethod
+    def setUpTestData(cls):
+        super().setUpTestData()
+        cls.volume_type = cls.corpus.types.get(slug='volume')
+        cls.page_type = cls.corpus.types.get(slug='page')
+        cls.line_type = cls.corpus.types.get(slug='text_line')
+        cls.vol = cls.corpus.elements.get(name='Volume 1')
+        cls.element = Element.objects.get(name='Volume 1, page 2r')
+        cls.line = cls.corpus.elements.get(name='Text line')
+        cls.image = ImageServer.objects.local.images.create(
+            path="kingdom/far/away",
+            status=S3FileStatus.Checked,
+            width=42,
+            height=42,
+        )
+        cls.private_corpus = Corpus.objects.create(name='private', public=False)
+        cls.private_elt = cls.private_corpus.elements.create(type=cls.private_corpus.types.create(slug='type'))
+
+    def test_put_element_unverified(self):
+        """
+        Check putting an element with an unverified email is not possible
+        """
+        user = User.objects.create_user('nope@nope.fr')
+        self.assertFalse(user.verified_email)
+        self.client.force_login(user)
+
+        response = self.client.put(
+            reverse('api:element-retrieve', kwargs={'pk': str(self.vol.id)}),
+            data={'name': 'Untitled (2)'},
+            format='json',
+        )
+        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+        self.assertDictEqual(
+            response.json(),
+            {'detail': 'You do not have permission to perform this action.'}
+        )
+
+    def test_put_no_write_access(self):
+        # Create read_only corpus right
+        self.private_corpus.memberships.create(user=self.user, level=Role.Guest.value)
+        self.assertTrue(self.user.verified_email)
+        self.client.force_login(self.user)
+        response = self.client.put(
+            reverse('api:element-retrieve', kwargs={'pk': str(self.private_elt.id)}),
+            data={'name': 'Untitled (2)'},
+            format='json',
+        )
+        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+        self.assertDictEqual(
+            response.json(),
+            {'detail': 'You do not have write access to this element.'}
+        )
+
+    def test_put_element_no_read_access(self):
+        """
+        Check putting an element as anonymous user is not possible
+        """
+        ext_user = User.objects.create_user(email='ark@ark.net')
+        ext_user.verified_email = True
+        ext_user.save()
+        self.client.force_login(ext_user)
+        response = self.client.put(
+            reverse('api:element-retrieve', kwargs={'pk': str(self.private_elt.id)}),
+            data={'name': 'Untitled (2)'},
+            format='json',
+        )
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+    def test_put_element(self):
+        self.client.force_login(self.user)
+        self.assertEqual(self.vol.name, 'Volume 1')
+        self.assertEqual(self.vol.type, self.volume_type)
+        with self.assertNumQueries(11):
+            response = self.client.put(
+                reverse('api:element-retrieve', kwargs={'pk': str(self.vol.id)}),
+                data={'name': 'Untitled (2)', 'type': 'text_line'},
+                format='json',
+            )
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        data = response.json()
+        self.assertEqual(data['name'], 'Untitled (2)')
+        self.assertEqual(data['type'], 'text_line')
+        self.vol.refresh_from_db()
+        self.assertEqual(self.vol.name, 'Untitled (2)')
+        self.assertEqual(self.vol.type, self.line_type)
+
+    def test_put_element_name_type_only(self):
+        """
+        Ensure putting an element only updates name and type, nothing else,
+        and every other property is ignored
+        """
+        self.client.force_login(self.user)
+        box_type = self.corpus.types.create(slug='xyzzy', display_name='box', folder=True)
+        response = self.client.put(
+            reverse('api:element-retrieve', kwargs={'pk': str(self.vol.id)}),
+            data={
+                'id': 'beef',
+                'name': 'New name',
+                'type': 'xyzzy',
+                'corpus': 'something',
+                'thumbnail_url': '/dev/urandom',
+                'thumbnail_put_url': '/dev/null',
+                'zone': 51,
+                'metadata': [],
+                'classifications': [],
+            },
+            format='json',
+        )
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.json()['name'], 'New name')
+        self.assertEqual(response.json()['type'], 'xyzzy')
+        self.vol.refresh_from_db()
+        self.assertEqual(self.vol.name, 'New name')
+        self.assertEqual(self.vol.type, box_type)
+        self.assertNotEqual(self.vol.id, 'beef')
+        self.assertNotEqual(self.vol.corpus_id, 'something')
+
+    def test_put_element_polygon(self):
+        self.client.force_login(self.user)
+        self.assertIsNotNone(self.element.image_id)
+        self.assertIsNotNone(self.element.polygon)
+        response = self.client.put(
+            reverse('api:element-retrieve', kwargs={'pk': str(self.element.id)}),
+            data={
+                'name': 'Untitled (2)',
+                'type': 'text_line',
+                'polygon': [[10, 20], [10, 30], [30, 30], [30, 20], [10, 20]]
+            },
+            format='json',
+        )
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.json()['name'], 'Untitled (2)')
+        self.assertEqual(response.json()['type'], 'text_line')
+        self.element.refresh_from_db()
+        self.assertEqual(self.element.name, 'Untitled (2)')
+        self.assertEqual(self.element.type, self.line_type)
+        self.assertTupleEqual(
+            self.element.polygon.coords,
+            ((10, 20), (10, 30), (30, 30), (30, 20), (10, 20))
+        )
+
+    def test_put_element_polygon_without_image(self):
+        self.client.force_login(self.user)
+        self.vol.image = None
+        self.vol.polygon = None
+        self.vol.save()
+        response = self.client.put(
+            reverse('api:element-retrieve', kwargs={'pk': str(self.vol.id)}),
+            data={
+                'name': 'Untitled (2)',
+                'type': 'text_line',
+                'polygon': [[10, 20], [10, 30], [30, 30], [30, 20], [10, 20]]
+            },
+            format='json',
+        )
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+        self.assertDictEqual(response.json(), {
+            'image': ['Image is required when defining a polygon on an element without an existing zone']
+        })
+
+    def test_put_element_image_preserve_polygon(self):
+        self.client.force_login(self.user)
+        self.assertNotEqual(self.element.image, self.image)
+        expected_polygon = self.element.polygon.clone()
+        response = self.client.put(
+            reverse('api:element-retrieve', kwargs={'pk': str(self.element.id)}),
+            data={
+                'name': 'Untitled (2)',
+                'type': 'text_line',
+                'image': str(self.image.id),
+            },
+            format='json',
+        )
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.element.refresh_from_db()
+        self.assertEqual(self.element.image, self.image)
+        self.assertEqual(self.element.polygon, expected_polygon)
+
+    def test_put_element_new_zone(self):
+        self.client.force_login(self.user)
+        self.assertIsNone(self.vol.image_id)
+        response = self.client.put(
+            reverse('api:element-retrieve', kwargs={'pk': str(self.vol.id)}),
+            data={
+                'name': 'Untitled (2)',
+                'type': 'text_line',
+                'image': str(self.image.id),
+                'polygon': [[10, 20], [10, 30], [30, 30], [30, 20], [10, 20]],
+            },
+            format='json',
+        )
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.vol.refresh_from_db()
+        self.assertEqual(self.vol.image, self.image)
+        self.assertTupleEqual(
+            self.vol.polygon.coords,
+            ((10, 20), (10, 30), (30, 30), (30, 20), (10, 20))
+        )
+
+    def test_put_element_invalid_dimensions(self):
+        self.client.force_login(self.user)
+        self.assertIsNone(self.vol.image)
+        bad_image = self.imgsrv.images.create(
+            path='oh-no',
+            status=S3FileStatus.Unchecked,
+            width=0,
+            height=0,
+        )
+        response = self.client.put(
+            reverse('api:element-retrieve', kwargs={'pk': str(self.vol.id)}),
+            data={
+                'name': 'Untitled (2)',
+                'type': 'text_line',
+                'image': str(bad_image.id),
+                'polygon': [[10, 20], [10, 30], [30, 30], [30, 20], [10, 20]],
+            },
+            format='json',
+        )
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+        self.assertDictEqual(response.json(), {'image': ['This image does not have valid dimensions.']})
+        self.assertIsNone(self.vol.image)
+
+    def test_put_element_polygon_negative(self):
+        self.client.force_login(self.user)
+        response = self.client.put(
+            reverse('api:element-retrieve', kwargs={'pk': str(self.element.id)}),
+            data={
+                'name': 'Untitled (2)',
+                'type': 'text_line',
+                'polygon': [[-10, -20], [10, 30], [30, 30], [30, 20], [10, 20]]
+            },
+            format='json',
+        )
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+        self.assertDictEqual(response.json(), {
+            'polygon': {
+                '0': {
+                    '0': ['Ensure this value is greater than or equal to 0.'],
+                    '1': ['Ensure this value is greater than or equal to 0.'],
+                }
+            }
+        })