diff --git a/dynamicforms/exports.py b/dynamicforms/exports.py
index b1dacf7a1d14403c7974242d2c816062033f948a..f899f67aa5928254b31180421386751a25c5a388 100644
--- a/dynamicforms/exports.py
+++ b/dynamicforms/exports.py
@@ -7,6 +7,7 @@ import zipfile
 import csv
 from io import BytesIO, StringIO
 from .models import Form
+from django.db.models.fields.files import FieldFile
 
 
 def _get_rows(form: Form) -> List[List[str]]:
@@ -70,3 +71,18 @@ def write_zip(forms, stream=None, folder='forms'):
             value = csv_file.getvalue()
             zip.writestr(csv_filename, value.encode())
     return stream
+
+
+def files_zip(files: List[FieldFile], folder: str='') -> BytesIO:
+    """Write form's files into a ZIP."""
+    stream = BytesIO()
+    with zipfile.ZipFile(stream, 'w') as zip:
+        for file in files:
+            dest = os.path.join(folder, os.path.basename(file.name))
+            contents = file.read()
+            zip.writestr(dest, contents)
+        # fix for Linux zip files read in Windows
+        for file in zip.filelist:
+            file.create_system = 0
+    stream.seek(0)
+    return stream
diff --git a/dynamicforms/views.py b/dynamicforms/views.py
index a257292e4171a9551123202d711a78245b69fe1e..84143d4f8ac7b0558b4542b6509791cc476f8d07 100644
--- a/dynamicforms/views.py
+++ b/dynamicforms/views.py
@@ -1,9 +1,10 @@
 """Dynamic forms views and API endpoints."""
 
+from typing import Union
 from django.http import HttpResponse
 from rest_framework import mixins, viewsets
 
-from .exports import write_zip
+from .exports import write_zip, files_zip
 from .models import Form, FormEntry
 from .serializers import (FormDetailSerializer, FormEntrySerializer,
                           FormSerializer)
@@ -41,6 +42,24 @@ def download_multiple_forms_entries(request, forms):
 
     response = HttpResponse(contents,
                             content_type='application/x-zip-compressed')
-    response['Content-Disposition'] = f'attachment; filename="{filename}"'
+    response['Content-Disposition'] = f'attachment; filename={filename}'
+
+    return response
+
+
+def download_files_zip(request, form: Union[Form, None], folder: str):
+    """Download form files in a ZIP archive."""
+    if form:
+        files_qs = form.files.all()
+        files = (f.file for f in files_qs)
+    else:
+        files = ()
+    filename = f'{folder}_files.zip'
+
+    stream = files_zip(files, folder=folder)
+
+    response = HttpResponse(content_type='application/zip')
+    response['Content-Disposition'] = f'attachment; filename={filename}'
+    response.write(stream.read())
 
     return response
diff --git a/projects/views.py b/projects/views.py
index 6824d2a774d0eefb2ea56df88d63e5d4801eade6..7ea3aba8e542660756ad10fe252e0349d1a33b16 100644
--- a/projects/views.py
+++ b/projects/views.py
@@ -2,19 +2,20 @@
 
 from django.core.exceptions import ObjectDoesNotExist
 from django.shortcuts import redirect
+from django.utils.text import slugify
 from django.utils.timezone import now
 from django_filters import rest_framework as filters
-from django_filters.rest_framework.backends import DjangoFilterBackend
 from rest_framework import mixins, permissions, status, viewsets
 from rest_framework.decorators import action
 from rest_framework.response import Response
 
 from dynamicforms.serializers import FormEntrySerializer
+from dynamicforms.views import download_files_zip
 
 from .models import Edition, Participation, Project
-from .serializers import (EditionDetailSerializer, EditionListSerializer,
-                          ParticipationSerializer, ProjectDetailSerializer,
-                          ProjectSerializer, EditionDocumentsSerializer)
+from .serializers import (EditionDetailSerializer, EditionDocumentsSerializer,
+                          EditionListSerializer, ParticipationSerializer,
+                          ProjectDetailSerializer, ProjectSerializer)
 
 
 class ProjectViewSet(viewsets.ReadOnlyModelViewSet):
@@ -225,7 +226,7 @@ class EditionViewSet(viewsets.ReadOnlyModelViewSet):
         'participations', 'organizers'
     )
     permission_classes = (permissions.IsAuthenticated,)
-    filter_backends = (filters.DjangoFilterBackend,)
+    filter_backends = (filters.backends.DjangoFilterBackend,)
     filter_fields = ('project', 'year',)
 
     def get_serializer_class(self):
@@ -340,6 +341,22 @@ class EditionViewSet(viewsets.ReadOnlyModelViewSet):
         data = serializer.data
         return Response(data)
 
+    @action(methods=['get'], detail=True)
+    def documents_zip(self, request, pk=None):
+        """Download an edition form's documents as a ZIP archive.
+
+        If the edition does not have a form, an empty ZIP file is sent.
+        """
+        edition: Edition = self.get_object()
+        folder = slugify(edition.project.name)
+
+        try:
+            form = edition.edition_form.form
+        except ObjectDoesNotExist:
+            form = None
+
+        return download_files_zip(request, form=form, folder=folder)
+
 
 class ParticipationViewSet(mixins.CreateModelMixin,
                            viewsets.ReadOnlyModelViewSet):
@@ -401,7 +418,7 @@ class ParticipationViewSet(mixins.CreateModelMixin,
     queryset = Participation.objects.prefetch_related('edition').all()
     serializer_class = ParticipationSerializer
     permission_classes = (permissions.IsAuthenticated,)
-    filter_backends = (DjangoFilterBackend,)
+    filter_backends = (filters.backends.DjangoFilterBackend,)
     filter_fields = ('user', 'state',)
 
     @action(methods=['get'], detail=True)