diff --git a/dependencies/pip/dev_requirements.txt b/dependencies/pip/dev_requirements.txt
index 1df8bbb9f1..5e4e485c5d 100644
--- a/dependencies/pip/dev_requirements.txt
+++ b/dependencies/pip/dev_requirements.txt
@@ -169,6 +169,7 @@ django==4.2.15
     #   django-taggit
     #   django-timezone-field
     #   djangorestframework
+    #   drf-spectacular
     #   jsonfield
     #   model-bakery
 django-allauth==0.61.1
@@ -238,6 +239,7 @@ djangorestframework==3.15.1
     #   -r dependencies/pip/requirements.in
     #   djangorestframework-csv
     #   drf-extensions
+    #   drf-spectacular
 djangorestframework-csv==3.0.2
     # via -r dependencies/pip/requirements.in
 djangorestframework-jsonp==1.0.2
@@ -252,6 +254,8 @@ docutils==0.20.1
     # via statistics
 drf-extensions==0.7.1
     # via -r dependencies/pip/requirements.in
+drf-spectacular==0.27.2
+    # via -r dependencies/pip/requirements.in
 et-xmlfile==1.1.0
     # via openpyxl
 exceptiongroup==1.2.0
@@ -344,6 +348,8 @@ idna==3.6
     # via
     #   requests
     #   yarl
+inflection==0.5.1
+    # via drf-spectacular
 iniconfig==2.0.0
     # via pytest
 invoke==2.2.0
@@ -365,6 +371,7 @@ jsonfield==3.1.0
 jsonschema==4.21.1
     # via
     #   -r dependencies/pip/requirements.in
+    #   drf-spectacular
     #   formpack
 jsonschema-specifications==2023.12.1
     # via jsonschema
@@ -542,7 +549,9 @@ pyxform==2.2.0
     #   -r dependencies/pip/requirements.in
     #   formpack
 pyyaml==6.0.1
-    # via responses
+    # via
+    #   drf-spectacular
+    #   responses
 redis==5.0.3
     # via
     #   celery
@@ -654,7 +663,9 @@ tzdata==2024.1
 ua-parser==0.18.0
     # via -r dependencies/pip/requirements.in
 uritemplate==4.1.1
-    # via google-api-python-client
+    # via
+    #   drf-spectacular
+    #   google-api-python-client
 urllib3==1.26.18
     # via
     #   botocore
diff --git a/dependencies/pip/requirements.in b/dependencies/pip/requirements.in
index 968f5b6ba8..47b69d72f9 100644
--- a/dependencies/pip/requirements.in
+++ b/dependencies/pip/requirements.in
@@ -111,3 +111,5 @@ modilabs-python-utils
 djangorestframework-csv
 djangorestframework-jsonp
 pandas
+
+drf-spectacular # OpenAPI v3 schema generator
diff --git a/dependencies/pip/requirements.txt b/dependencies/pip/requirements.txt
index 76175a5dc5..1ab008aa6e 100644
--- a/dependencies/pip/requirements.txt
+++ b/dependencies/pip/requirements.txt
@@ -138,6 +138,7 @@ django==4.2.15
     #   django-taggit
     #   django-timezone-field
     #   djangorestframework
+    #   drf-spectacular
     #   jsonfield
 django-allauth==0.61.1
     # via -r dependencies/pip/requirements.in
@@ -206,6 +207,7 @@ djangorestframework==3.15.1
     #   -r dependencies/pip/requirements.in
     #   djangorestframework-csv
     #   drf-extensions
+    #   drf-spectacular
 djangorestframework-csv==3.0.2
     # via -r dependencies/pip/requirements.in
 djangorestframework-jsonp==1.0.2
@@ -218,6 +220,8 @@ docutils==0.20.1
     # via statistics
 drf-extensions==0.7.1
     # via -r dependencies/pip/requirements.in
+drf-spectacular==0.27.2
+    # via -r dependencies/pip/requirements.in
 et-xmlfile==1.1.0
     # via openpyxl
 flower==2.0.1
@@ -288,6 +292,8 @@ idna==3.6
     # via
     #   requests
     #   yarl
+inflection==0.5.1
+    # via drf-spectacular
 isodate==0.6.1
     # via azure-storage-blob
 jmespath==1.0.1
@@ -299,6 +305,7 @@ jsonfield==3.1.0
 jsonschema==4.21.1
     # via
     #   -r dependencies/pip/requirements.in
+    #   drf-spectacular
     #   formpack
 jsonschema-specifications==2023.12.1
     # via jsonschema
@@ -417,7 +424,9 @@ pyxform==2.2.0
     #   -r dependencies/pip/requirements.in
     #   formpack
 pyyaml==6.0.1
-    # via responses
+    # via
+    #   drf-spectacular
+    #   responses
 redis==5.0.3
     # via
     #   celery
@@ -502,7 +511,9 @@ tzdata==2024.1
 ua-parser==0.18.0
     # via -r dependencies/pip/requirements.in
 uritemplate==4.1.1
-    # via google-api-python-client
+    # via
+    #   drf-spectacular
+    #   google-api-python-client
 urllib3==1.26.18
     # via
     #   botocore
diff --git a/kobo/apps/openrosa/libs/renderers/renderers.py b/kobo/apps/openrosa/libs/renderers/renderers.py
index 53db176681..7b9cfa6906 100644
--- a/kobo/apps/openrosa/libs/renderers/renderers.py
+++ b/kobo/apps/openrosa/libs/renderers/renderers.py
@@ -58,7 +58,7 @@ def filter_renderers(self, renderers, format):
 
 class MediaFileRenderer(BaseRenderer):
     media_type = '*/*'
-    format = None
+    format = 'TODO'
     charset = None
     render_style = 'binary'
 
diff --git a/kobo/settings/base.py b/kobo/settings/base.py
index 0e632527bc..21ace455c0 100644
--- a/kobo/settings/base.py
+++ b/kobo/settings/base.py
@@ -142,6 +142,7 @@
     'guardian',
     'kobo.apps.openrosa.libs',
     'kobo.apps.project_ownership.ProjectOwnershipAppConfig',
+    'drf_spectacular',
 )
 
 MIDDLEWARE = [
@@ -640,9 +641,6 @@
     'positive_int_minus_one': ['django.forms.fields.IntegerField', {
         'min_value': -1
     }],
-    'positive_int': ['django.forms.fields.IntegerField', {
-        'min_value': 0
-    }],
 }
 
 CONSTANCE_CONFIG_FIELDSETS = {
@@ -937,6 +935,15 @@ def __init__(self, *args, **kwargs):
     'DEFAULT_VERSIONING_CLASS': 'kpi.versioning.APIAutoVersioning',
     # Cannot be placed in kpi.exceptions.py because of circular imports
     'EXCEPTION_HANDLER': 'kpi.utils.drf_exceptions.custom_exception_handler',
+    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
+}
+
+SPECTACULAR_SETTINGS = {
+    'TITLE': 'KoboToolbox API',
+    'DESCRIPTION': 'Warning: experimental schema generation. Use at your own risk.',
+    'VERSION': '0.0.1',
+    'SERVE_INCLUDE_SCHEMA': False,
+    # OTHER SETTINGS
 }
 
 OPENROSA_REST_FRAMEWORK = {
diff --git a/kobo/urls.py b/kobo/urls.py
index 7c5ba388c8..25187bdee1 100644
--- a/kobo/urls.py
+++ b/kobo/urls.py
@@ -8,15 +8,29 @@
 from django.views.generic.base import RedirectView
 from rest_framework import status
 from rest_framework.exceptions import server_error
+from drf_spectacular.views import (
+    SpectacularAPIView,
+    SpectacularRedocView,
+    SpectacularSwaggerView,
+)
 
 from kpi.utils.urls import is_request_for_html
 
 admin.autodiscover()
-admin.site.login = staff_member_required(
-    admin.site.login, login_url=settings.LOGIN_URL
-)
+admin.site.login = staff_member_required(admin.site.login, login_url=settings.LOGIN_URL)
 
 urlpatterns = [
+    path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
+    path(
+        'api/schema/swagger/',
+        SpectacularSwaggerView.as_view(url_name='schema'),
+        name='swagger-ui',
+    ),
+    path(
+        'api/schema/redoc/',
+        SpectacularRedocView.as_view(url_name='schema'),
+        name='redoc',
+    ),
     # https://github.com/stochastic-technologies/django-loginas
     re_path(r'^admin/', include('loginas.urls')),
     # Disable admin login form
diff --git a/kpi/renderers.py b/kpi/renderers.py
index 49019edd62..bb1f30aba2 100644
--- a/kpi/renderers.py
+++ b/kpi/renderers.py
@@ -24,7 +24,7 @@ class AssetJsonRenderer(renderers.JSONRenderer):
 
 class MediaFileRenderer(renderers.BaseRenderer):
     media_type = '*/*'
-    format = None
+    format = 'TODO'
     charset = None
     render_style = 'binary'