diff --git a/.gitignore b/.gitignore
index 38c00aebdcdf5fe4ab674e00196555aeef5738dd..71d78337c7ffc621211cc066c1afc5c4197226cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,5 +16,5 @@ arkindex/iiif-users/
 htmlcov
 ponos
 openapi/*.yml
-!openapi/patch.yml
+!openapi/paths.yml
 *.key
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 1709b117a63fb2442e06b07c5c4299c2cb10db95..8e6131e8231c890fd1f9931a6e02396a74988341 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -60,8 +60,8 @@ backend-openapi:
     - mkdir -p output
     - pip install -e .
     - pip install uritemplate==3 apistar>=0.7.2
-    - arkindex/manage.py generateschema > output/original.yml
-    - openapi/patch.py openapi/patch.yml output/original.yml > output/schema.yml
+    - arkindex/manage.py generateschema --generator_class arkindex.project.openapi.SchemaGenerator > output/original.yml
+    - openapi/patch.py openapi/paths.yml output/original.yml > output/schema.yml
 
   variables:
     PONOS_DATA_DIR: /tmp
diff --git a/arkindex/project/openapi/__init__.py b/arkindex/project/openapi/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..389596710e549ffbec362b7c541ad3e78b10801a
--- /dev/null
+++ b/arkindex/project/openapi/__init__.py
@@ -0,0 +1,2 @@
+from arkindex.project.openapi.schemas import AutoSchema, SearchAutoSchema  # noqa: F401
+from arkindex.project.openapi.generators import SchemaGenerator  # noqa: F401
diff --git a/arkindex/project/openapi/generators.py b/arkindex/project/openapi/generators.py
new file mode 100644
index 0000000000000000000000000000000000000000..db13e52b4b85459f02d720a16d0752278db420c7
--- /dev/null
+++ b/arkindex/project/openapi/generators.py
@@ -0,0 +1,15 @@
+from pathlib import Path
+from rest_framework.schemas.openapi import SchemaGenerator as BaseSchemaGenerator
+import yaml
+
+PATCH_FILE = Path(__file__).absolute().parent / 'patch.yml'
+
+
+class SchemaGenerator(BaseSchemaGenerator):
+
+    def get_schema(self, **kwargs):
+        with PATCH_FILE.open() as f:
+            patch = yaml.safe_load(f)
+        schema = super().get_schema(**kwargs)
+        schema.update(patch)
+        return schema
diff --git a/arkindex/project/openapi/patch.yml b/arkindex/project/openapi/patch.yml
new file mode 100644
index 0000000000000000000000000000000000000000..41ef4f58fe5eb17914100b7a3fc240c489bb6385
--- /dev/null
+++ b/arkindex/project/openapi/patch.yml
@@ -0,0 +1,43 @@
+info:
+  title: Arkindex API
+  contact:
+    name: Teklia
+    url: https://www.teklia.com/
+    email: paris@teklia.com
+components:
+  securitySchemes:
+    sessionAuth:
+      in: cookie
+      name: arkindex.auth
+      type: apiKey
+    tokenAuth:
+      scheme: Token
+      type: http
+    agentAuth:
+      scheme: Bearer
+      type: http
+security:
+- tokenAuth: []
+- sessionAuth: []
+servers:
+  - description: Arkindex
+    url: https://arkindex.teklia.com
+  - description: Arkindex preproduction
+    url: https://arkindex.dev.teklia.com
+tags:
+  - name: corpora
+  - name: elements
+  - name: search
+  - name: oauth
+  - name: imports
+  - name: files
+  - name: images
+  - name: ponos
+  - name: iiif
+    description: IIIF manifests, annotation lists and services
+  - name: ml
+    description: Machine Learning tools and results
+  - name: entities
+  - name: users
+  - name: management
+    description: Admin-only tools
diff --git a/arkindex/project/openapi.py b/arkindex/project/openapi/schemas.py
similarity index 100%
rename from arkindex/project/openapi.py
rename to arkindex/project/openapi/schemas.py
diff --git a/openapi/patch.py b/openapi/patch.py
index f82346562537071797136f1b2163d0fe43b935cc..b1d5704e7ee630e39ee852ab45fe73e13f139578 100755
--- a/openapi/patch.py
+++ b/openapi/patch.py
@@ -13,7 +13,7 @@ def get_args():
     parser = argparse.ArgumentParser()
     parser.add_argument(
         'patch',
-        help='File describing patches to make on the schema',
+        help="File describing patches to make on the schema's paths",
         type=argparse.FileType('r'),
     )
     parser.add_argument(
@@ -38,15 +38,13 @@ def get_args():
     return args
 
 
-def update_schema(schema, patches):
+def update_schema(schema, paths):
     """
-    Perform updates on an OpenAPI schema using another YAML file describing patches.
-    The update is not recursive; any root key will overwrite the schema's original data.
-    For paths, each operation has its keys updated; the whole operation is not overwritten.
+    Perform updates on an OpenAPI schema's paths using another YAML file describing patches.
+    In paths, each operation has its keys updated; the whole operation is not overwritten.
     Same goes for an operation's responses.
     """
     # Update each method separately
-    paths = patches.pop('paths', {})
     for path, methods in paths.items():
         if path not in schema['paths']:
             print('Creating path {}'.format(path), file=sys.stderr)
@@ -69,11 +67,6 @@ def update_schema(schema, patches):
 
             schema['paths'][path][method].update(operation)
 
-    # Update other root keys
-    for k, v in patches.items():
-        print('Patching {}'.format(k), file=sys.stderr)
-        schema[k] = v
-
     # Set the API version to the Arkindex package version
     try:
         # Try with the VERSION file
diff --git a/openapi/patch.yml b/openapi/patch.yml
deleted file mode 100644
index 6a2645d5f142af6849a3750b5df3efef04fc82ba..0000000000000000000000000000000000000000
--- a/openapi/patch.yml
+++ /dev/null
@@ -1,787 +0,0 @@
-info:
-  title: Arkindex API
-  contact:
-    name: Teklia
-    url: https://www.teklia.com/
-    email: paris@teklia.com
-components:
-  securitySchemes:
-    sessionAuth:
-      in: cookie
-      name: arkindex.auth
-      type: apiKey
-    tokenAuth:
-      scheme: Token
-      type: http
-    agentAuth:
-      scheme: Bearer
-      type: http
-security:
-- tokenAuth: []
-- sessionAuth: []
-servers:
-  - description: Arkindex
-    url: https://arkindex.teklia.com
-  - description: Arkindex preproduction
-    url: https://arkindex.dev.teklia.com
-tags:
-  - name: corpora
-  - name: elements
-  - name: search
-  - name: oauth
-  - name: imports
-  - name: files
-  - name: images
-  - name: ponos
-  - name: iiif
-    description: IIIF manifests, annotation lists and services
-  - name: ml
-    description: Machine Learning tools and results
-  - name: entities
-  - name: users
-  - name: management
-    description: Admin-only tools
-paths:
-  /api/v1/classification/bulk/:
-    post:
-      description: >-
-        Create multiple classifications at once on the same element with
-        the same classifier.
-      tags:
-        - ml
-  /api/v1/classifications/:
-    post:
-      description: >-
-        Manually add a class on a page, as a human, not as a machine learning tool.
-      tags:
-        - ml
-  /api/v1/corpus/:
-    get:
-      description: List corpora with their access rights
-      security: []
-      tags:
-        - corpora
-    post:
-      description: Create a new corpus
-      tags:
-        - corpora
-  /api/v1/corpus/{id}/:
-    get:
-      description: Retrieve a single corpus
-      security: []
-      tags:
-        - corpora
-    put:
-      description: Update a corpus
-      tags:
-        - corpora
-    patch:
-      description: Partially update a corpus
-      tags:
-        - corpora
-    delete:
-      description: >-
-        Delete a corpus. Requires the "admin" right on the corpus.
-
-        Warning: This operation might not work on corpora with a large amount
-        of elements, as the deletion process will take more than 30 seconds.
-      tags:
-        - corpora
-  /api/v1/corpus/{id}/roles/:
-    get:
-      operationId: ListCorpusRoles
-      description: List all roles of a corpus
-      security: []
-      tags:
-        - entities
-    post:
-      description: Create a new entity role
-      responses:
-        '400':
-          description: An error occured while creating the role.
-          content:
-            application/json:
-              schema:
-                properties:
-                  id:
-                    type: string or array
-                    description: The corpus ID.
-                    readOnly: true
-                  corpus:
-                    type: array
-                    description: Errors that occured during corpus ID field validation.
-                    readOnly: true
-              examples:
-                role-exists:
-                  summary: Role already exists.
-                  value:
-                    id: 55cd009d-cd4b-4ec2-a475-b060f98f9138
-                    corpus:
-                      - Role already exists in this corpus
-      tags:
-        - entities
-  /api/v1/corpus/{id}/ml-stats/:
-    delete:
-      # Will need https://trello.com/c/f2K4j50S/ to be removed
-      operationId: DestroyCorpusMLResults
-      description: Delete machine learning results on all elements of a corpus.
-  /api/v1/element/{id}/:
-    get:
-      description: Retrieve a single element's informations and metadata
-      security: []
-      tags:
-        - elements
-    patch:
-      description: Rename an element
-      tags:
-        - elements
-    put:
-      description: Rename an element
-      tags:
-        - elements
-    delete:
-      description: Delete a childless element
-      tags:
-        - elements
-  /api/v1/element/{id}/history/:
-    get:
-      description: List an element's update history
-      security: []
-      tags:
-        - elements
-  /api/v1/element/{id}/links/:
-    get:
-      description: List all links where parent and child are linked to the element
-      operationId: ListElementLinks
-      tags:
-      - entities
-  /api/v1/element/{id}/region/:
-    post:
-      operationId: CreateRegion
-      description: Create a region on an element
-      tags:
-        - elements
-  /api/v1/element/{id}/regions/:
-    get:
-      operationId: ListElementRegions
-      description: List all regions on an element
-      security: []
-      tags:
-        - elements
-  /api/v1/element/{id}/transcriptions/:
-    get:
-      description: List all transcriptions for an element, filtered by type
-      security: []
-      tags:
-        - elements
-      parameters:
-        - description: Element id
-          in: path
-          name: id
-          required: true
-          schema:
-           type: string
-        - description: A page number within the paginated result set.
-          in: query
-          name: page
-          required: false
-          schema:
-            type: integer
-        - description: Transcription type filter
-          in: query
-          name: type
-          required: false
-          schema:
-            type: string
-            enum: ["page", "paragraph", "line", "word", "character"]
-  /api/v1/element/{id}/ml-stats/:
-    delete:
-      # Will need https://trello.com/c/f2K4j50S/ to be removed
-      operationId: DestroyElementMLResults
-      description: Delete machine learning results on an element and its direct children.
-  /api/v1/elements/create/:
-    post:
-      operationId: CreateElement
-      description: Create a new element
-      tags:
-        - elements
-  /api/v1/elements/{id}/:
-    get:
-      operationId: ListRelatedElements
-      description: List all parents and children of a single element
-      security: []
-      tags:
-        - elements
-  /api/v1/elements/{id}/neighbors/:
-    get:
-      description: List neighboring elements
-      tags:
-        - elements
-  /api/v1/elements/selection/:
-    delete:
-      operationId: RemoveSelection
-      description: Remove a specific element or delete any selection
-      requestBody:
-        content:
-          application/json:
-            schema:
-              properties:
-                id:
-                  format: uuid
-                  type: string
-      tags:
-        - elements
-    get:
-      operationId: ListSelection
-      description: List all elements selected
-      tags:
-        - elements
-    post:
-      operationId: AddSelection
-      description: Add specific elements
-      requestBody:
-        content:
-          application/json:
-            schema:
-              properties:
-                ids:
-                  items:
-                    format: uuid
-                    type: string
-                  type: array
-              required:
-              - ids
-      tags:
-      - elements
-  /api/v1/element/{child}/parent/{parent}/:
-    post:
-      operationId: CreateElementParent
-      description: Link an element to a new parent
-      tags:
-        - elements
-    delete:
-      operationId: DestroyElementParent
-      description: Delete the relation between an element and one of its parents
-      tags:
-        - elements
-  /api/v1/entity/:
-    post:
-      operationId: CreateEntity
-      description: Create a new entity
-      tags:
-        - entities
-  /api/v1/element/{id}/entities/:
-    get:
-      operationId: ListElementsEntities
-      description: List all entities linked to an element's transcriptions and metadata
-      tags:
-        - entities
-  /api/v1/entity/link/:
-    post:
-      operationId: CreateEntityLink
-      description: Create a new link between two entities with a role
-      tags:
-        - entities
-  /api/v1/entity/{id}/:
-    get:
-      description: Get all information about entity
-      security: []
-      tags:
-        - entities
-    delete:
-      description: Delete an entity
-      tags:
-      - entities
-    patch:
-      description: Partially update an entity
-      tags:
-      - entities
-    put:
-      description: Update an entity
-      tags:
-      - entities
-  /api/v1/entity/{id}/elements/:
-    get:
-      operationId: ListEntityElements
-      description: Get all elements that have a link with the entity
-      security: []
-      tags:
-        - entities
-  /api/v1/transcription/{id}/entity/:
-    post:
-      operationId: CreateTranscriptionEntity
-      description: Link an existing Entity to a given transcription with its position
-      tags:
-        - entities
-  /api/v1/transcription/{id}/entities/:
-    get:
-      operationId: ListTranscriptionEntities
-      description: List existing entities linked to a specific transcription
-      tags:
-        - entities
-  /api/v1/image/:
-    post:
-      operationId: CreateImage
-      description: >-
-        Create an image on the Arkindex image server, ready for upload to Amazon S3.
-        The response includes an Amazon S3 URL that you can use to upload an image
-        via HTTP PUT. Update the image's status to "checked" to confirm your image
-        is successfully uploaded and visible to Arkindex.
-      tags:
-        - images
-      responses:
-        '400':
-          description: An error occured while validating the image.
-          content:
-            application/json:
-              schema:
-                properties:
-                  detail:
-                    type: string
-                    description: A generic error message when an error occurs outside of a specific field.
-                    readOnly: true
-                  id:
-                    type: string
-                    description: UUID of an existing image, if the error comes from a duplicate hash.
-                    readOnly: true
-                  hash:
-                    type: array
-                    description: One or more error messages for errors when validating the image hash.
-                    readOnly: true
-                  datafile:
-                    type: array
-                    description: One or more error messages for errors when validating the optional DataFile link.
-                    readOnly: true
-              examples:
-                image-exists:
-                  summary: An error where an image with this hash already exists, including the existing image's UUID.
-                  value:
-                    hash:
-                      - Image with this hash already exists
-                    id: 3cc2e9e0-4172-44b1-8d65-bc3fffd076dc
-  /api/v1/image/{id}/:
-    get:
-      description: Retrieve an image
-      tags:
-        - images
-    put:
-      description: Update an image's status
-      tags:
-        - images
-    patch:
-      description: Update an image's status
-      tags:
-        - images
-  /api/v1/imports/:
-    get:
-      operationId: ListDataImports
-      description: List all data imports
-      tags:
-        - imports
-  /api/v1/imports/file/{id}/:
-    get:
-      description: Get an uploaded file's metadata
-      tags:
-        - files
-    patch:
-      description: Update a datafile's status
-      tags:
-        - files
-    put:
-      description: Update a datafile's status
-      tags:
-        - files
-    delete:
-      description: Delete an uploaded file
-      tags:
-        - files
-  /api/v1/imports/files/{id}/:
-    get:
-      description: List uploaded files in a corpus
-      tags:
-        - files
-  /api/v1/imports/fromfiles/:
-    post:
-      description: Start a data import from one or more uploaded files
-      tags:
-        - files
-  /api/v1/imports/hook/{id}/:
-    post:
-      operationId: GitPushHook
-      description: >-
-        This endpoint is intended as a webhook for Git repository hosting applications like GitLab.
-      security: []
-      tags:
-        - repos
-  /api/v1/imports/mltools/:
-    get:
-      description: List available machine learning tools
-      security: []
-      tags:
-        - ml
-  /api/v1/imports/repos/:
-    get:
-      operationId: ListRepositories
-      description: List connected repositories
-      tags:
-        - repos
-  /api/v1/imports/repos/search/{id}/:
-    get:
-      description: >-
-        Search for a repository to connect to.
-
-        Using the given OAuth credentials ID, this uses the Git hosting
-        application API's search feature to look for a repository matching
-        the given query. Without a query, returns a full list.
-      tags:
-        - repos
-    post:
-      description: >-
-        Using the given OAuth credentials, this links an external Git repository
-        to Arkindex, connects a push hook and starts an initial import.
-      tags:
-        - repos
-  /api/v1/imports/repos/{id}/:
-    get:
-      description: Get a repository
-      tags:
-        - repos
-    delete:
-      description: Delete a repository
-      tags:
-        - repos
-  /api/v1/imports/repos/{id}/start/:
-    get:
-      operationId: StartRepositoryImport
-      description: Start a data import on the latest revision on a repository
-      tags:
-        - repos
-  /api/v1/imports/upload/{id}/:
-    post:
-      operationId: UploadDataFile
-      description: Upload a file to a corpus
-      tags:
-        - files
-      responses:
-        '400':
-          description: An error occured while validating the file.
-          content:
-            application/json:
-              schema:
-                properties:
-                  detail:
-                    type: string
-                    description: A generic error message when an error occurs outside of a specific field.
-                    readOnly: true
-                  id:
-                    type: string
-                    description: UUID of an existing DataFile, if the error  comes from a duplicated upload.
-                    readOnly: true
-                  file:
-                    type: array
-                    description: One or more error messages for errors when validating the file itself.
-                    readOnly: true
-                  corpus:
-                    type: array
-                    description: One or more error messages for errors when validating the destination corpus ID.
-                    readOnly: true
-              examples:
-                file-exists:
-                  summary: An error where the data file already exists, including the existing file's UUID.
-                  value:
-                    file:
-                      - File already exists
-                    id: 3cc2e9e0-4172-44b1-8d65-bc3fffd076dc
-  /api/v1/imports/files/create/:
-    post:
-      operationId: CreateDataFile
-      description: Create a Datafile. In case of success, a signed uri is returned to upload file content directly to remote server.
-      tags:
-        - files
-      responses:
-        '400':
-          description: An error occured while creating the data file.
-          content:
-            application/json:
-              schema:
-                properties:
-                  detail:
-                    type: string
-                    description: A generic error message when an error occurs outside of a specific field.
-                    readOnly: true
-                  hash:
-                    type: array
-                    description: Errors that occured during hash field validation.
-                    readOnly: true
-                  corpus:
-                    type: array
-                    description: Errors that occured during corpus ID field validation.
-                    readOnly: true
-                  name:
-                    type: array
-                    description: Errors that occured during name field validation.
-                    readOnly: true
-                  size:
-                    type: array
-                    description: Errors that occured during size field validation.
-                    readOnly: true
-                  id:
-                    type: string
-                    description: UUID of existing DataFile, if the error comes from a duplicated creation.
-                    readOnly: true
-                  status:
-                    type: string
-                    description: Status of existing DataFile, if the error comes from a duplicated creation.
-                    readOnly: true
-                  s3_put_url:
-                    type: string
-                    description: Signed url used to upload file content to remote server, if the error comes from a duplicated creation and file status is not checked.
-                    readOnly: true
-                  s3_url:
-                    type: string
-                    description: Remote file url, if the error comes from a duplicated creation and file status is checked.
-                    readOnly: true
-              examples:
-                file-exists:
-                  summary: Data file already exists. Response include existing file's UUID, status and remote server PUT url to upload file content.
-                  value:
-                    hash:
-                      - DataFile with this hash already exists
-                    id: 55cd009d-cd4b-4ec2-a475-b060f98f9138
-                    status: unchecked
-                    s3_put_url: https://remote-server.net/staging/55cd009d-cd4b-4ec2-a475-b060f98f9138?Credential=mycredential&Signature=mysignature
-  /api/v1/imports/{id}/:
-    get:
-      description: Retrieve a data import
-      tags:
-        - imports
-    delete:
-      description: Delete a data import. Cannot be used on currently running data imports.
-      tags:
-        - imports
-  /api/v1/imports/{id}/retry/:
-    post:
-      operationId: RetryDataImport
-      description: Retry a data import. Can only be used on imports with Error or Failed states.
-      tags:
-        - imports
-  /api/v1/oauth/credentials/:
-    get:
-      description: List all OAuth credentials for the authenticated user
-      tags:
-        - oauth
-  /api/v1/oauth/credentials/{id}/:
-    get:
-      description: Retrieve OAuth credentials
-      tags:
-        - oauth
-    delete:
-      description: Delete OAuth credentials. This may disable access to some Git repositories.
-      tags:
-        - oauth
-  /api/v1/oauth/credentials/{id}/retry/:
-    get:
-      operationId: RetryOAuthCredentials
-      description: Retry the OAuth authentication code flow for pending credentials
-      tags:
-        - oauth
-  /api/v1/oauth/providers/:
-    get:
-      operationId: ListOAuthProviders
-      description: List supported OAuth providers
-      tags:
-        - oauth
-  /api/v1/oauth/providers/{provider}/signin/:
-    get:
-      operationId: StartOAuthSignIn
-      description: Start the OAuth authentication code flow for a given provider
-      tags:
-        - oauth
-  /api/v1/element/{id}/transcriptions/xml/:
-    post:
-      operationId: ImportPageXmlTranscriptions
-      description: Import transcriptions into Arkindex from region data in the PAGE XML format.
-      requestBody:
-        required: true
-        description: >-
-          A PAGE XML document.
-          TextRegion tags will be imported as Paragraph transcriptions
-          and TextLine tags will become Line transcriptions.
-          See https://github.com/PRImA-Research-Lab/PAGE-XML for more info
-          about the PAGE XML format.
-        content:
-          application/xml:
-            schema: {}
-      tags:
-        - ml
-  /api/v1/region/bulk/:
-    post:
-      operationId: CreateRegions
-      description: Add new regions from a machine learning tool.
-    put:
-      operationId: UpdateRegions
-      description: Add new regions from a machine learning tool; if this tool already has any regions on this element, they are deleted.
-  /api/v1/region/{id}/:
-    get:
-      description: Retrieve detailed information about a region
-      security: []
-      tags:
-        - elements
-  /api/v1/transcription/:
-    post:
-      operationId: CreateTranscription
-      description: Create a single transcription on a page
-      tags:
-        - ml
-  /api/v1/transcription/bulk/:
-    post:
-      description: >-
-        Create multiple transcriptions at once, all linked to the same page
-        and to the same recognizer.
-      tags:
-        - ml
-    put:
-      description: >-
-        Replace all existing transcriptions from a given recognizer on a page
-        with other transcriptions.
-      tags:
-        - ml
-  /api/v1/metadata/{id}/:
-    get:
-      operationId: RetrieveMetaData
-      description: Retrieve an existing metadata
-    patch:
-      operationId: PartialUpdateMetaData
-      description: Partially update an existing metadata
-    put:
-      operationId: UpdateMetaData
-      description: Update an existing metadata
-    delete:
-      operationId: DestroyMetaData
-      description: Delete an existing metadata
-  /api/v1/user/:
-    get:
-      description: Retrieve information about the authenticated user
-      tags:
-        - users
-    patch:
-      description: Update a user's password. This action is not allowed to users without confirmed e-mails.
-      tags:
-        - users
-    put:
-      description: Update a user's password. This action is not allowed to users without confirmed e-mails.
-      tags:
-        - users
-    delete:
-      operationId: Logout
-      description: Log out from the API
-      tags:
-        - users
-  /api/v1/user/login/:
-    post:
-      operationId: Login
-      description: Login using a username and a password
-      security: []
-      tags:
-        - users
-  /api/v1/user/password-reset/:
-    post:
-      operationId: ResetPassword
-      description: Start a password reset flow
-      security: []
-      tags:
-        - users
-  /api/v1/user/password-reset/confirm/:
-    post:
-      operationId: PasswordResetConfirm
-      description: Confirm a password reset using data from the confirmation e-mail
-      security: []
-      tags:
-        - users
-  /ponos/v1/agent/:
-    post:
-      description: Register a Ponos agent
-      security: []
-      tags:
-        - ponos
-  /ponos/v1/agent/actions/:
-    get:
-      description: Retrieve any actions the current agent should perform
-      security:
-        - agentAuth: []
-      tags:
-        - ponos
-  /ponos/v1/agent/refresh/:
-    post:
-      operationId: RefreshAgentToken
-      description: Refresh a Ponos agent token when it expires
-      security:
-        - agentAuth: []
-      tags:
-        - ponos
-  /ponos/v1/public-key/:
-    get:
-      operationId: GetPublicKey
-      description: Get the server's public key.
-      security: []
-      tags:
-        - ponos
-      responses:
-        '200':
-          content:
-            application/x-pem-file:
-              schema:
-                type: string
-              example: |-
-                -----BEGIN PUBLIC KEY-----
-                MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEmK2L6lwGzSVZwFSo0eR1z4XV6jJwjeWK
-                YCiPKdMcQnn6u5J016k9U8xZm6XyFnmgvkhnC3wreGBTFzwLCLZCD+F3vo5x8ivz
-                aTgNWsA3WFlqjSIEGz+PAVHSNMobBaJm
-                -----END PUBLIC KEY-----
-  /ponos/v1/task/{id}/:
-    get:
-      description: Retrieve a Ponos task status
-      security: []
-      tags:
-        - ponos
-    put:
-      description: Update a task
-      security:
-        - agentAuth: []
-      tags:
-        - ponos
-    patch:
-      description: Partially update a task
-      security:
-        - agentAuth: []
-      tags:
-        - ponos
-  /ponos/v1/task/{id}/artifacts/:
-    get:
-      description: List all the artifacts of the task
-      security:
-        - agentAuth: []
-      tags:
-        - ponos
-  /ponos/v1/task/{id}/definition/:
-    get:
-      description: Retrieve a Ponos task
-      operationId: RetrieveTaskDefinition
-      security:
-        - agentAuth: []
-      tags:
-        - ponos
-  /ponos/v1/workflow/{id}/:
-    get:
-      description: Retrieve a Ponos workflow status
-      security: []
-      tags:
-        - ponos
-    patch:
-      description: Partially update a workflow
-      tags:
-      - ponos
-    put:
-      description: Update a workflow's status and tasks
-      tags:
-      - ponos
diff --git a/openapi/paths.yml b/openapi/paths.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4720aefedd46456c0cd6437a631bdfd3c482b9c7
--- /dev/null
+++ b/openapi/paths.yml
@@ -0,0 +1,743 @@
+/api/v1/classification/bulk/:
+  post:
+    description: >-
+      Create multiple classifications at once on the same element with
+      the same classifier.
+    tags:
+      - ml
+/api/v1/classifications/:
+  post:
+    description: >-
+      Manually add a class on a page, as a human, not as a machine learning tool.
+    tags:
+      - ml
+/api/v1/corpus/:
+  get:
+    description: List corpora with their access rights
+    security: []
+    tags:
+      - corpora
+  post:
+    description: Create a new corpus
+    tags:
+      - corpora
+/api/v1/corpus/{id}/:
+  get:
+    description: Retrieve a single corpus
+    security: []
+    tags:
+      - corpora
+  put:
+    description: Update a corpus
+    tags:
+      - corpora
+  patch:
+    description: Partially update a corpus
+    tags:
+      - corpora
+  delete:
+    description: >-
+      Delete a corpus. Requires the "admin" right on the corpus.
+
+      Warning: This operation might not work on corpora with a large amount
+      of elements, as the deletion process will take more than 30 seconds.
+    tags:
+      - corpora
+/api/v1/corpus/{id}/roles/:
+  get:
+    operationId: ListCorpusRoles
+    description: List all roles of a corpus
+    security: []
+    tags:
+      - entities
+  post:
+    description: Create a new entity role
+    responses:
+      '400':
+        description: An error occured while creating the role.
+        content:
+          application/json:
+            schema:
+              properties:
+                id:
+                  type: string or array
+                  description: The corpus ID.
+                  readOnly: true
+                corpus:
+                  type: array
+                  description: Errors that occured during corpus ID field validation.
+                  readOnly: true
+            examples:
+              role-exists:
+                summary: Role already exists.
+                value:
+                  id: 55cd009d-cd4b-4ec2-a475-b060f98f9138
+                  corpus:
+                    - Role already exists in this corpus
+    tags:
+      - entities
+/api/v1/corpus/{id}/ml-stats/:
+  delete:
+    # Will need https://trello.com/c/f2K4j50S/ to be removed
+    operationId: DestroyCorpusMLResults
+    description: Delete machine learning results on all elements of a corpus.
+/api/v1/element/{id}/:
+  get:
+    description: Retrieve a single element's informations and metadata
+    security: []
+    tags:
+      - elements
+  patch:
+    description: Rename an element
+    tags:
+      - elements
+  put:
+    description: Rename an element
+    tags:
+      - elements
+  delete:
+    description: Delete a childless element
+    tags:
+      - elements
+/api/v1/element/{id}/history/:
+  get:
+    description: List an element's update history
+    security: []
+    tags:
+      - elements
+/api/v1/element/{id}/links/:
+  get:
+    description: List all links where parent and child are linked to the element
+    operationId: ListElementLinks
+    tags:
+    - entities
+/api/v1/element/{id}/region/:
+  post:
+    operationId: CreateRegion
+    description: Create a region on an element
+    tags:
+      - elements
+/api/v1/element/{id}/regions/:
+  get:
+    operationId: ListElementRegions
+    description: List all regions on an element
+    security: []
+    tags:
+      - elements
+/api/v1/element/{id}/transcriptions/:
+  get:
+    description: List all transcriptions for an element, filtered by type
+    security: []
+    tags:
+      - elements
+    parameters:
+      - description: Element id
+        in: path
+        name: id
+        required: true
+        schema:
+         type: string
+      - description: A page number within the paginated result set.
+        in: query
+        name: page
+        required: false
+        schema:
+          type: integer
+      - description: Transcription type filter
+        in: query
+        name: type
+        required: false
+        schema:
+          type: string
+          enum: ["page", "paragraph", "line", "word", "character"]
+/api/v1/element/{id}/ml-stats/:
+  delete:
+    # Will need https://trello.com/c/f2K4j50S/ to be removed
+    operationId: DestroyElementMLResults
+    description: Delete machine learning results on an element and its direct children.
+/api/v1/elements/create/:
+  post:
+    operationId: CreateElement
+    description: Create a new element
+    tags:
+      - elements
+/api/v1/elements/{id}/:
+  get:
+    operationId: ListRelatedElements
+    description: List all parents and children of a single element
+    security: []
+    tags:
+      - elements
+/api/v1/elements/{id}/neighbors/:
+  get:
+    description: List neighboring elements
+    tags:
+      - elements
+/api/v1/elements/selection/:
+  delete:
+    operationId: RemoveSelection
+    description: Remove a specific element or delete any selection
+    requestBody:
+      content:
+        application/json:
+          schema:
+            properties:
+              id:
+                format: uuid
+                type: string
+    tags:
+      - elements
+  get:
+    operationId: ListSelection
+    description: List all elements selected
+    tags:
+      - elements
+  post:
+    operationId: AddSelection
+    description: Add specific elements
+    requestBody:
+      content:
+        application/json:
+          schema:
+            properties:
+              ids:
+                items:
+                  format: uuid
+                  type: string
+                type: array
+            required:
+            - ids
+    tags:
+    - elements
+/api/v1/element/{child}/parent/{parent}/:
+  post:
+    operationId: CreateElementParent
+    description: Link an element to a new parent
+    tags:
+      - elements
+  delete:
+    operationId: DestroyElementParent
+    description: Delete the relation between an element and one of its parents
+    tags:
+      - elements
+/api/v1/entity/:
+  post:
+    operationId: CreateEntity
+    description: Create a new entity
+    tags:
+      - entities
+/api/v1/element/{id}/entities/:
+  get:
+    operationId: ListElementsEntities
+    description: List all entities linked to an element's transcriptions and metadata
+    tags:
+      - entities
+/api/v1/entity/link/:
+  post:
+    operationId: CreateEntityLink
+    description: Create a new link between two entities with a role
+    tags:
+      - entities
+/api/v1/entity/{id}/:
+  get:
+    description: Get all information about entity
+    security: []
+    tags:
+      - entities
+  delete:
+    description: Delete an entity
+    tags:
+    - entities
+  patch:
+    description: Partially update an entity
+    tags:
+    - entities
+  put:
+    description: Update an entity
+    tags:
+    - entities
+/api/v1/entity/{id}/elements/:
+  get:
+    operationId: ListEntityElements
+    description: Get all elements that have a link with the entity
+    security: []
+    tags:
+      - entities
+/api/v1/transcription/{id}/entity/:
+  post:
+    operationId: CreateTranscriptionEntity
+    description: Link an existing Entity to a given transcription with its position
+    tags:
+      - entities
+/api/v1/transcription/{id}/entities/:
+  get:
+    operationId: ListTranscriptionEntities
+    description: List existing entities linked to a specific transcription
+    tags:
+      - entities
+/api/v1/image/:
+  post:
+    operationId: CreateImage
+    description: >-
+      Create an image on the Arkindex image server, ready for upload to Amazon S3.
+      The response includes an Amazon S3 URL that you can use to upload an image
+      via HTTP PUT. Update the image's status to "checked" to confirm your image
+      is successfully uploaded and visible to Arkindex.
+    tags:
+      - images
+    responses:
+      '400':
+        description: An error occured while validating the image.
+        content:
+          application/json:
+            schema:
+              properties:
+                detail:
+                  type: string
+                  description: A generic error message when an error occurs outside of a specific field.
+                  readOnly: true
+                id:
+                  type: string
+                  description: UUID of an existing image, if the error comes from a duplicate hash.
+                  readOnly: true
+                hash:
+                  type: array
+                  description: One or more error messages for errors when validating the image hash.
+                  readOnly: true
+                datafile:
+                  type: array
+                  description: One or more error messages for errors when validating the optional DataFile link.
+                  readOnly: true
+            examples:
+              image-exists:
+                summary: An error where an image with this hash already exists, including the existing image's UUID.
+                value:
+                  hash:
+                    - Image with this hash already exists
+                  id: 3cc2e9e0-4172-44b1-8d65-bc3fffd076dc
+/api/v1/image/{id}/:
+  get:
+    description: Retrieve an image
+    tags:
+      - images
+  put:
+    description: Update an image's status
+    tags:
+      - images
+  patch:
+    description: Update an image's status
+    tags:
+      - images
+/api/v1/imports/:
+  get:
+    operationId: ListDataImports
+    description: List all data imports
+    tags:
+      - imports
+/api/v1/imports/file/{id}/:
+  get:
+    description: Get an uploaded file's metadata
+    tags:
+      - files
+  patch:
+    description: Update a datafile's status
+    tags:
+      - files
+  put:
+    description: Update a datafile's status
+    tags:
+      - files
+  delete:
+    description: Delete an uploaded file
+    tags:
+      - files
+/api/v1/imports/files/{id}/:
+  get:
+    description: List uploaded files in a corpus
+    tags:
+      - files
+/api/v1/imports/fromfiles/:
+  post:
+    description: Start a data import from one or more uploaded files
+    tags:
+      - files
+/api/v1/imports/hook/{id}/:
+  post:
+    operationId: GitPushHook
+    description: >-
+      This endpoint is intended as a webhook for Git repository hosting applications like GitLab.
+    security: []
+    tags:
+      - repos
+/api/v1/imports/mltools/:
+  get:
+    description: List available machine learning tools
+    security: []
+    tags:
+      - ml
+/api/v1/imports/repos/:
+  get:
+    operationId: ListRepositories
+    description: List connected repositories
+    tags:
+      - repos
+/api/v1/imports/repos/search/{id}/:
+  get:
+    description: >-
+      Search for a repository to connect to.
+
+      Using the given OAuth credentials ID, this uses the Git hosting
+      application API's search feature to look for a repository matching
+      the given query. Without a query, returns a full list.
+    tags:
+      - repos
+  post:
+    description: >-
+      Using the given OAuth credentials, this links an external Git repository
+      to Arkindex, connects a push hook and starts an initial import.
+    tags:
+      - repos
+/api/v1/imports/repos/{id}/:
+  get:
+    description: Get a repository
+    tags:
+      - repos
+  delete:
+    description: Delete a repository
+    tags:
+      - repos
+/api/v1/imports/repos/{id}/start/:
+  get:
+    operationId: StartRepositoryImport
+    description: Start a data import on the latest revision on a repository
+    tags:
+      - repos
+/api/v1/imports/upload/{id}/:
+  post:
+    operationId: UploadDataFile
+    description: Upload a file to a corpus
+    tags:
+      - files
+    responses:
+      '400':
+        description: An error occured while validating the file.
+        content:
+          application/json:
+            schema:
+              properties:
+                detail:
+                  type: string
+                  description: A generic error message when an error occurs outside of a specific field.
+                  readOnly: true
+                id:
+                  type: string
+                  description: UUID of an existing DataFile, if the error  comes from a duplicated upload.
+                  readOnly: true
+                file:
+                  type: array
+                  description: One or more error messages for errors when validating the file itself.
+                  readOnly: true
+                corpus:
+                  type: array
+                  description: One or more error messages for errors when validating the destination corpus ID.
+                  readOnly: true
+            examples:
+              file-exists:
+                summary: An error where the data file already exists, including the existing file's UUID.
+                value:
+                  file:
+                    - File already exists
+                  id: 3cc2e9e0-4172-44b1-8d65-bc3fffd076dc
+/api/v1/imports/files/create/:
+  post:
+    operationId: CreateDataFile
+    description: Create a Datafile. In case of success, a signed uri is returned to upload file content directly to remote server.
+    tags:
+      - files
+    responses:
+      '400':
+        description: An error occured while creating the data file.
+        content:
+          application/json:
+            schema:
+              properties:
+                detail:
+                  type: string
+                  description: A generic error message when an error occurs outside of a specific field.
+                  readOnly: true
+                hash:
+                  type: array
+                  description: Errors that occured during hash field validation.
+                  readOnly: true
+                corpus:
+                  type: array
+                  description: Errors that occured during corpus ID field validation.
+                  readOnly: true
+                name:
+                  type: array
+                  description: Errors that occured during name field validation.
+                  readOnly: true
+                size:
+                  type: array
+                  description: Errors that occured during size field validation.
+                  readOnly: true
+                id:
+                  type: string
+                  description: UUID of existing DataFile, if the error comes from a duplicated creation.
+                  readOnly: true
+                status:
+                  type: string
+                  description: Status of existing DataFile, if the error comes from a duplicated creation.
+                  readOnly: true
+                s3_put_url:
+                  type: string
+                  description: Signed url used to upload file content to remote server, if the error comes from a duplicated creation and file status is not checked.
+                  readOnly: true
+                s3_url:
+                  type: string
+                  description: Remote file url, if the error comes from a duplicated creation and file status is checked.
+                  readOnly: true
+            examples:
+              file-exists:
+                summary: Data file already exists. Response include existing file's UUID, status and remote server PUT url to upload file content.
+                value:
+                  hash:
+                    - DataFile with this hash already exists
+                  id: 55cd009d-cd4b-4ec2-a475-b060f98f9138
+                  status: unchecked
+                  s3_put_url: https://remote-server.net/staging/55cd009d-cd4b-4ec2-a475-b060f98f9138?Credential=mycredential&Signature=mysignature
+/api/v1/imports/{id}/:
+  get:
+    description: Retrieve a data import
+    tags:
+      - imports
+  delete:
+    description: Delete a data import. Cannot be used on currently running data imports.
+    tags:
+      - imports
+/api/v1/imports/{id}/retry/:
+  post:
+    operationId: RetryDataImport
+    description: Retry a data import. Can only be used on imports with Error or Failed states.
+    tags:
+      - imports
+/api/v1/oauth/credentials/:
+  get:
+    description: List all OAuth credentials for the authenticated user
+    tags:
+      - oauth
+/api/v1/oauth/credentials/{id}/:
+  get:
+    description: Retrieve OAuth credentials
+    tags:
+      - oauth
+  delete:
+    description: Delete OAuth credentials. This may disable access to some Git repositories.
+    tags:
+      - oauth
+/api/v1/oauth/credentials/{id}/retry/:
+  get:
+    operationId: RetryOAuthCredentials
+    description: Retry the OAuth authentication code flow for pending credentials
+    tags:
+      - oauth
+/api/v1/oauth/providers/:
+  get:
+    operationId: ListOAuthProviders
+    description: List supported OAuth providers
+    tags:
+      - oauth
+/api/v1/oauth/providers/{provider}/signin/:
+  get:
+    operationId: StartOAuthSignIn
+    description: Start the OAuth authentication code flow for a given provider
+    tags:
+      - oauth
+/api/v1/element/{id}/transcriptions/xml/:
+  post:
+    operationId: ImportPageXmlTranscriptions
+    description: Import transcriptions into Arkindex from region data in the PAGE XML format.
+    requestBody:
+      required: true
+      description: >-
+        A PAGE XML document.
+        TextRegion tags will be imported as Paragraph transcriptions
+        and TextLine tags will become Line transcriptions.
+        See https://github.com/PRImA-Research-Lab/PAGE-XML for more info
+        about the PAGE XML format.
+      content:
+        application/xml:
+          schema: {}
+    tags:
+      - ml
+/api/v1/region/bulk/:
+  post:
+    operationId: CreateRegions
+    description: Add new regions from a machine learning tool.
+  put:
+    operationId: UpdateRegions
+    description: Add new regions from a machine learning tool; if this tool already has any regions on this element, they are deleted.
+/api/v1/region/{id}/:
+  get:
+    description: Retrieve detailed information about a region
+    security: []
+    tags:
+      - elements
+/api/v1/transcription/:
+  post:
+    operationId: CreateTranscription
+    description: Create a single transcription on a page
+    tags:
+      - ml
+/api/v1/transcription/bulk/:
+  post:
+    description: >-
+      Create multiple transcriptions at once, all linked to the same page
+      and to the same recognizer.
+    tags:
+      - ml
+  put:
+    description: >-
+      Replace all existing transcriptions from a given recognizer on a page
+      with other transcriptions.
+    tags:
+      - ml
+/api/v1/metadata/{id}/:
+  get:
+    operationId: RetrieveMetaData
+    description: Retrieve an existing metadata
+  patch:
+    operationId: PartialUpdateMetaData
+    description: Partially update an existing metadata
+  put:
+    operationId: UpdateMetaData
+    description: Update an existing metadata
+  delete:
+    operationId: DestroyMetaData
+    description: Delete an existing metadata
+/api/v1/user/:
+  get:
+    description: Retrieve information about the authenticated user
+    tags:
+      - users
+  patch:
+    description: Update a user's password. This action is not allowed to users without confirmed e-mails.
+    tags:
+      - users
+  put:
+    description: Update a user's password. This action is not allowed to users without confirmed e-mails.
+    tags:
+      - users
+  delete:
+    operationId: Logout
+    description: Log out from the API
+    tags:
+      - users
+/api/v1/user/login/:
+  post:
+    operationId: Login
+    description: Login using a username and a password
+    security: []
+    tags:
+      - users
+/api/v1/user/password-reset/:
+  post:
+    operationId: ResetPassword
+    description: Start a password reset flow
+    security: []
+    tags:
+      - users
+/api/v1/user/password-reset/confirm/:
+  post:
+    operationId: PasswordResetConfirm
+    description: Confirm a password reset using data from the confirmation e-mail
+    security: []
+    tags:
+      - users
+/ponos/v1/agent/:
+  post:
+    description: Register a Ponos agent
+    security: []
+    tags:
+      - ponos
+/ponos/v1/agent/actions/:
+  get:
+    description: Retrieve any actions the current agent should perform
+    security:
+      - agentAuth: []
+    tags:
+      - ponos
+/ponos/v1/agent/refresh/:
+  post:
+    operationId: RefreshAgentToken
+    description: Refresh a Ponos agent token when it expires
+    security:
+      - agentAuth: []
+    tags:
+      - ponos
+/ponos/v1/public-key/:
+  get:
+    operationId: GetPublicKey
+    description: Get the server's public key.
+    security: []
+    tags:
+      - ponos
+    responses:
+      '200':
+        content:
+          application/x-pem-file:
+            schema:
+              type: string
+            example: |-
+              -----BEGIN PUBLIC KEY-----
+              MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEmK2L6lwGzSVZwFSo0eR1z4XV6jJwjeWK
+              YCiPKdMcQnn6u5J016k9U8xZm6XyFnmgvkhnC3wreGBTFzwLCLZCD+F3vo5x8ivz
+              aTgNWsA3WFlqjSIEGz+PAVHSNMobBaJm
+              -----END PUBLIC KEY-----
+/ponos/v1/task/{id}/:
+  get:
+    description: Retrieve a Ponos task status
+    security: []
+    tags:
+      - ponos
+  put:
+    description: Update a task
+    security:
+      - agentAuth: []
+    tags:
+      - ponos
+  patch:
+    description: Partially update a task
+    security:
+      - agentAuth: []
+    tags:
+      - ponos
+/ponos/v1/task/{id}/artifacts/:
+  get:
+    description: List all the artifacts of the task
+    security:
+      - agentAuth: []
+    tags:
+      - ponos
+/ponos/v1/task/{id}/definition/:
+  get:
+    description: Retrieve a Ponos task
+    operationId: RetrieveTaskDefinition
+    security:
+      - agentAuth: []
+    tags:
+      - ponos
+/ponos/v1/workflow/{id}/:
+  get:
+    description: Retrieve a Ponos workflow status
+    security: []
+    tags:
+      - ponos
+  patch:
+    description: Partially update a workflow
+    tags:
+    - ponos
+  put:
+    description: Update a workflow's status and tasks
+    tags:
+    - ponos