{
  "openapi": "3.0.0",
  "info": {
    "title": "Hubdoc API",
    "description": "API de la GED Hubdoc : workspaces, dossiers, documents, éditique\n(Markdown/Typst), uploads, annuaire (users, contacts, groupes) et\npermissions.\n\n## Authentification\n\nToutes les requêtes exigent un token `Bearer` OAuth2 (ou PAT) avec le\nscope adapté au verbe HTTP : `read` pour les `GET`, `write` pour les\nmutations. L'obtention des tokens (flows OAuth2, device flow CLI,\ntoken exchange Edifice, PAT) est documentée sur la page\n[Authentification](../authentication/).\n",
    "version": "1.0.0",
    "contact": {
      "name": "API Support",
      "email": "support@sinoia.fr"
    }
  },
  "servers": [
    {
      "url": "http://localhost:3000",
      "description": "Serveur de développement"
    },
    {
      "url": "https://rec.hubdoc.sinoia.cloud",
      "description": "Serveur de recette"
    }
  ],
  "security": [
    {
      "OAuth2Password": [
        "read"
      ]
    },
    {
      "OAuth2AuthCode": [
        "read"
      ]
    }
  ],
  "tags": [
    {
      "name": "Workspaces",
      "description": "Workspace management"
    },
    {
      "name": "Folders",
      "description": "Folder management"
    },
    {
      "name": "Documents",
      "description": "Document management"
    },
    {
      "name": "Composed Documents",
      "description": "Composed Documents — éditique Markdown/Typst. Permet de créer des\ndocuments à partir d'un template, gérer les parts (sections), et\ncompiler en PDF via le pipeline Typst.\n"
    },
    {
      "name": "Document Templates",
      "description": "Document Templates — modèles Typst/Markdown réutilisables.\nRead-only pour tous les users, CRUD réservé aux admins.\n"
    },
    {
      "name": "BulkUploads",
      "description": "Bulk upload operations"
    },
    {
      "name": "ChunkedUploads",
      "description": "Chunked upload operations"
    },
    {
      "name": "Contacts",
      "description": "Contact management"
    },
    {
      "name": "Groups",
      "description": "Group management"
    },
    {
      "name": "Group Members",
      "description": "Membres d'un groupe (ajout, retrait, liste)"
    },
    {
      "name": "Permissions",
      "description": "Permission management"
    },
    {
      "name": "Public Access",
      "description": "Accès public — la **troisième voie d'accès**, à côté des ACL nominatives\net des liens de partage. Rendre public, rendre privé, repropager,\nexclure, lever une exclusion, et lire l'état qui en résulte.\n\n## Deux modèles de propagation, délibérément différents\n\n|  | ACL nominatives | Accès public |\n|---|---|---|\n| Mécanisme | ascendance évaluée **à la lecture** | **estampillage explicite**, ligne par ressource |\n| Document ajouté après | hérite immédiatement | **pas public** tant qu'on n'a pas repropagé |\n| Document déplacé hors du sous-arbre | perd le droit | concession périmée |\n| Retrait sur une ressource | impossible sans permission négative | marqueur d'exclusion, transitif |\n\nLa divergence est **volontaire** : fail-closed assumé, pour qu'aucune\nressource ne soit publique sans qu'un dossier l'annonce. Elle surprend,\nd'où `subtree.unstamped` et `reason` dans l'état rendu.\n\n## Ce que l'accès public N'ouvre PAS\n\n- aucune **écriture** : le court-circuit est borné à la lecture, un accès\n  public ne sert donc jamais à obtenir le droit de publier ;\n- aucune **indexation** : une ressource publique n'est délibérément pas\n  cherchable ;\n- la racine d'une publication est toujours un **dossier**.\n"
    },
    {
      "name": "Users",
      "description": "User management"
    },
    {
      "name": "Mass Communications",
      "description": "Mass communication campaigns"
    }
  ],
  "paths": {
    "/api/v1/documents/workspaces": {
      "get": {
        "summary": "Lister les espaces de travail",
        "description": "Récupérer la liste de tous les espaces de travail",
        "tags": [
          "Workspaces"
        ],
        "responses": {
          "200": {
            "description": "Réponse réussie",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Workspace"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Créer un espace de travail",
        "description": "Créer un nouvel espace de travail",
        "tags": [
          "Workspaces"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContentTypeHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WorkspaceMutation"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Espace de travail créé avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Workspace"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/documents/workspaces/{id}": {
      "get": {
        "summary": "Récupérer un espace de travail",
        "description": "Récupérer un espace de travail spécifique par identifiant",
        "tags": [
          "Workspaces"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse réussie",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Workspace"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "patch": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Mettre à jour un espace de travail",
        "description": "Mettre à jour un espace de travail existant",
        "tags": [
          "Workspaces"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          },
          {
            "$ref": "#/components/parameters/ContentTypeHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WorkspaceMutation"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Espace de travail mis à jour avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Workspace"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Supprimer un espace de travail",
        "description": "Supprimer un espace de travail existant",
        "tags": [
          "Workspaces"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          }
        ],
        "responses": {
          "204": {
            "description": "Espace de travail supprimé avec succès"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/documents/folders": {
      "get": {
        "summary": "Lister les dossiers",
        "description": "Récupère une liste de dossiers selon les paramètres de filtrage fournis.\n\n**Filtrage par contexte :**\n- Sans paramètres : tous les dossiers accessibles à l'utilisateur\n- `workspace_id` : dossiers d'un workspace spécifique\n- `documents_folder_id` : sous-dossiers d'un dossier parent spécifique\n\n**Filtrage avancé avec Ransack :**\n- Utilisez le paramètre `q` pour des recherches avancées\n- Exemple : `q[name_cont]=admin` pour chercher les dossiers contenant \"admin\"\n",
        "tags": [
          "Folders"
        ],
        "parameters": [
          {
            "name": "workspace_id",
            "in": "query",
            "description": "ID du workspace pour filtrer les dossiers",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "documents_folder_id",
            "in": "query",
            "description": "ID du dossier parent pour filtrer les sous-dossiers",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Champ de tri (name, updated_at, created_at, etc.)",
            "required": false,
            "schema": {
              "type": "string",
              "default": "updated_at"
            }
          },
          {
            "name": "direction",
            "in": "query",
            "description": "Direction du tri",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Nombre d'éléments par page",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Numéro de page",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Filtres Ransack pour recherche avancée.\nExemples :\n- `q[name_cont]=projet` : nom contenant \"projet\"\n- `q[description_present]=true` : avec description\n- `q[color_eq]=#FF0000` : couleur exacte en hexadecimal\n",
            "required": false,
            "style": "deepObject",
            "explode": true,
            "schema": {
              "type": "object",
              "additionalProperties": true
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Liste des dossiers récupérée avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Folder"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Créer un dossier",
        "description": "Créer un nouveau dossier",
        "tags": [
          "Folders"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContentTypeHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FolderMutation"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Dossier créé avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Folder"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/documents/folders/{id}": {
      "get": {
        "summary": "Récupérer un dossier avec ses éléments",
        "description": "Récupérer un dossier spécifique par identifiant avec son contenu (sous-dossiers et fichiers)",
        "tags": [
          "Folders"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse réussie",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Folder"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "oneOf": [
                              {
                                "allOf": [
                                  {
                                    "$ref": "#/components/schemas/Folder"
                                  },
                                  {
                                    "type": "object",
                                    "properties": {
                                      "type": {
                                        "type": "string",
                                        "enum": [
                                          "folder"
                                        ]
                                      }
                                    }
                                  }
                                ]
                              },
                              {
                                "allOf": [
                                  {
                                    "$ref": "#/components/schemas/Document"
                                  },
                                  {
                                    "type": "object",
                                    "properties": {
                                      "type": {
                                        "type": "string",
                                        "enum": [
                                          "file"
                                        ]
                                      }
                                    }
                                  }
                                ]
                              }
                            ]
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "patch": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Mettre à jour un dossier",
        "description": "Mettre à jour un dossier existant",
        "tags": [
          "Folders"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          },
          {
            "$ref": "#/components/parameters/ContentTypeHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FolderMutation"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Dossier mis à jour avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Folder"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Supprimer un dossier",
        "description": "Supprimer un dossier existant",
        "tags": [
          "Folders"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          }
        ],
        "responses": {
          "204": {
            "description": "Dossier supprimé avec succès"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/documents/documents": {
      "get": {
        "summary": "Lister les documents",
        "description": "Récupère une liste de documents selon les paramètres de filtrage fournis.\n\n**Filtrage par contexte :**\n- Sans paramètres : tous les documents accessibles à l'utilisateur\n- `workspace_id` : documents d'un workspace spécifique\n- `documents_folder_id` : documents d'un dossier spécifique\n\n**Filtrage avancé avec Ransack :**\n- Utilisez le paramètre `q` pour des recherches avancées\n- Exemple : `q[name_cont]=rapport` pour chercher les documents contenant \"rapport\"\n",
        "tags": [
          "Documents"
        ],
        "parameters": [
          {
            "name": "workspace_id",
            "in": "query",
            "description": "ID du workspace pour filtrer les documents",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "documents_folder_id",
            "in": "query",
            "description": "ID du dossier pour filtrer les documents",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Champ de tri (name, updated_at, created_at, etc.)",
            "required": false,
            "schema": {
              "type": "string",
              "default": "updated_at"
            }
          },
          {
            "name": "direction",
            "in": "query",
            "description": "Direction du tri",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Nombre d'éléments par page",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Numéro de page",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Filtres Ransack pour recherche avancée.\nExemples :\n- `q[name_cont]=test` : nom contenant \"test\"\n- `q[description_present]=true` : avec description\n",
            "required": false,
            "style": "deepObject",
            "explode": true,
            "schema": {
              "type": "object",
              "additionalProperties": true
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Liste des documents récupérée avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Document"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Créer un document",
        "description": "Créer un document en uploadant un fichier (classique ou chunked).\n\n**Deux modes d'upload :**\n- **Mode classique** : Upload direct d'un fichier via `uploaded_file`\n- **Mode chunked** : Référence à un upload chunked complété via `chunked_upload_id`\n\n**Deux scénarios :**\n- **Sans bulk_upload_id** : Crée automatiquement un BulkUpload pour traçabilité\n- **Avec bulk_upload_id** : Attache le fichier à un BulkUpload existant (cas hubdoc-tools)\n\n**Mode fusion (bulk_upload avec `merge_to_pdf`)** : le fichier est stagé\n(réponse `202 Accepted`, aucun document individuel créé) à sa `position`\n(obligatoire, unique, à partir de 0). Quand `total_files` sources ont été\nreçues, elles sont fusionnées en un seul document PDF dans l'ordre des\npositions ; son ID est exposé par `GET /bulk_uploads/:id`\n(`merged_document_id`). Formats acceptés : PDF, JPEG, PNG.\n\n**Fonctionnalités :**\n- Classification automatique optionnelle\n- Métadonnées utilisateur personnalisées\n- Source automatique basée sur l'application OAuth\n",
        "tags": [
          "Documents"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "bulk_upload_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "ID d'un BulkUpload existant (optionnel, pour uploads groupés)"
                  },
                  "workspace_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "ID du workspace"
                  },
                  "documents_folder_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "ID du dossier parent"
                  },
                  "domain_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "ID du domaine"
                  },
                  "document_type_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "ID du type de document"
                  },
                  "auto_classify": {
                    "type": "boolean",
                    "description": "Active la classification automatique des documents",
                    "default": false
                  },
                  "metadata_user": {
                    "type": "object",
                    "description": "Métadonnées utilisateur personnalisées"
                  },
                  "uploaded_file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Fichier à uploader (soit uploaded_file, soit chunked_upload_id requis)"
                  },
                  "position": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Position du fichier dans le PDF fusionné (requis si le BulkUpload est en mode merge_to_pdf)"
                  },
                  "pages": {
                    "type": "string",
                    "description": "Réservé (sélection de plages de pages, non implémenté — envoyer null)"
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Pour uploads chunked uniquement",
                "properties": {
                  "bulk_upload_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "ID d'un BulkUpload existant (optionnel)"
                  },
                  "chunked_upload_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "ID d'un upload chunked complété"
                  },
                  "workspace_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "ID du workspace"
                  },
                  "documents_folder_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "ID du dossier parent"
                  },
                  "auto_classify": {
                    "type": "boolean",
                    "default": false
                  },
                  "position": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Position du fichier dans le PDF fusionné (requis si le BulkUpload est en mode merge_to_pdf)"
                  }
                },
                "required": [
                  "chunked_upload_id"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Document créé avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Document"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "bulk_upload_id": {
                          "type": "string",
                          "format": "uuid",
                          "description": "ID du BulkUpload associé"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "202": {
            "description": "Fichier stagé pour fusion (BulkUpload en mode merge_to_pdf), aucun document individuel créé",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "staged": {
                      "type": "boolean",
                      "description": "Toujours true — le fichier attend la fusion"
                    },
                    "position": {
                      "type": "integer",
                      "minimum": 0,
                      "description": "Position du fichier dans le PDF fusionné"
                    },
                    "bulk_upload": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "received_files": {
                          "type": "integer",
                          "minimum": 0
                        },
                        "total_files": {
                          "type": "integer",
                          "minimum": 0
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "in_progress",
                            "completed",
                            "failed"
                          ]
                        }
                      },
                      "required": [
                        "id",
                        "received_files",
                        "total_files",
                        "status"
                      ]
                    }
                  },
                  "required": [
                    "staged",
                    "position",
                    "bulk_upload"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/documents/documents/{id}": {
      "get": {
        "summary": "Récupérer un document",
        "description": "Récupérer un document spécifique par identifiant",
        "tags": [
          "Documents"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse réussie",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Document"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "patch": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Mettre à jour un document",
        "description": "Mettre à jour un document existant.\n\n**Deux modes :**\n- **application/json** : Mise à jour des métadonnées uniquement (name, description, etc.)\n- **multipart/form-data** : Remplacement du fichier via `uploaded_file`\n",
        "tags": [
          "Documents"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DocumentMutation"
              }
            },
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/DocumentMutation"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Document mis à jour avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Document"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Supprimer un document",
        "description": "Supprimer un document existant",
        "tags": [
          "Documents"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          }
        ],
        "responses": {
          "204": {
            "description": "Document supprimé avec succès"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/documents/documents/{id}/versions": {
      "post": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Créer une nouvelle version d'un document",
        "description": "Crée une nouvelle **version** du document : le contenu courant est d'abord\narchivé comme version (historique), puis remplacé par le fichier uploadé.\nL'identifiant du document reste inchangé.\n\nÀ utiliser lorsqu'un livrable déjà présent doit être **mis à jour** sans\ncréer de doublon ni casser les liens existants (ex. resync d'un artefact\nmodifié par un agent Hubdoc/Synapse).\n",
        "tags": [
          "Documents"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "uploaded_file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Nouveau contenu du document (soit uploaded_file, soit chunked_upload_id requis)"
                  },
                  "chunked_upload_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Référence à un upload chunked complété (alternative à uploaded_file)"
                  },
                  "comment": {
                    "type": "string",
                    "description": "Commentaire optionnel associé à la version créée"
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Pour versions à partir d'un upload chunked uniquement",
                "properties": {
                  "chunked_upload_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Référence à un upload chunked complété"
                  },
                  "comment": {
                    "type": "string",
                    "description": "Commentaire optionnel associé à la version créée"
                  }
                },
                "required": [
                  "chunked_upload_id"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Nouvelle version créée ; le document (contenu courant mis à jour) est renvoyé",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Document"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/documents/folders/{folder_id}/public_access": {
      "get": {
        "security": [
          {
            "OAuth2Password": [
              "read"
            ]
          },
          {
            "OAuth2AuthCode": [
              "read"
            ]
          }
        ],
        "summary": "Lire l'état d'accès public d'un dossier",
        "description": "Rend l'état complet : publique ou non, la racine qui l'explique, les\nexclusions, et le bilan du sous-arbre — **dont les ressources non\nestampillées**, celles qu'une repropagation rendrait publiques.\n\nExige le droit d'**écriture** sur le dossier, et non de lecture : l'état\nénumère nommément le complément privé du sous-arbre publié. L'ouvrir en\nlecture rendrait un dossier public utilisable comme annuaire de ce qu'il\nne publie pas.\n",
        "tags": [
          "Public Access"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FolderIdParameter"
          },
          {
            "$ref": "#/components/parameters/PublicAccessLimitParameter"
          }
        ],
        "responses": {
          "200": {
            "description": "État d'accès public du dossier",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicAccessState"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Publier un dossier (rendre public)",
        "description": "Publie le dossier comme **racine publique** et propage la concession sur\ntout son sous-arbre. Geste explicite, tracé et révocable.\n\nC'est la porte générique du « rendre public » : elle remplace le détour\npar `POST /api/v1/documents/folders/{id}/site`, qui reste servi le temps\nque le CLI publié migre (hubdoc-tools#29) mais ne doit plus être employé\npour ce geste.\n\n**La racine d'une publication est toujours un dossier** : rendre un\ndocument isolé public n'existe pas. Les documents sont couverts par la\npropagation depuis un dossier.\n\nIdempotent : republier un dossier déjà publié rend `200` et ne ressuscite\naucune concession que personne n'a reprise. Publier lève l'exclusion\nportée par le dossier lui-même (celles de ses descendants restent).\n\nAction à conséquence : la portée doit être annoncée à l'utilisateur\n**avant** validation, jamais après.\n",
        "tags": [
          "Public Access"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FolderIdParameter"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "propagate": {
                    "type": "boolean",
                    "default": true,
                    "description": "`false` publie la racine SANS estampiller le sous-arbre : seul\nle dossier devient public. Réservé aux publications en deux\ntemps ; la propagation reste alors à faire explicitement.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Dossier déjà publié — état inchangé (idempotence)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicAccessState"
                }
              }
            }
          },
          "201": {
            "description": "Dossier publié",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicAccessState"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Dépublier un dossier (rendre privé)",
        "description": "Retire la racine publique. **Une seule écriture** : toutes les concessions\ndu sous-arbre sont conditionnées à la présence de cette racine, elles\npériment donc d'un coup, sans dépropagation ni tâche de fond.\n\nIdempotent. L'état rendu après coup n'est pas redondant : un dossier\nimbriqué dans un AUTRE sous-arbre publié reste public une fois dépublié,\net seul l'état le dit.\n",
        "tags": [
          "Public Access"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FolderIdParameter"
          }
        ],
        "responses": {
          "200": {
            "description": "État d'accès public après dépublication",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicAccessState"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/documents/folders/{folder_id}/public_access/propagate": {
      "post": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Repropager l'accès public sur le sous-arbre",
        "description": "Réestampille tout le sous-arbre d'une racine publiée : l'existant est\nréestampillé, les ajouts sont couverts. Idempotent.\n\n**C'est la contrepartie assumée du fail-closed.** Un document déposé dans\nun dossier publié n'est pas public tant que ce geste n'a pas eu lieu :\n`subtree.unstamped` le compte, ce geste le résorbe. Un agent qui régénère\nun site doit enchaîner régénération et repropagation.\n\nLes ressources explicitement exclues ne sont jamais réestampillées — sans\nquoi l'exclusion serait illusoire dès la première régénération.\n\nÉchoue en `422` si le dossier n'est pas publié comme racine : propager\nn'est pas un raccourci pour publier.\n",
        "tags": [
          "Public Access"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FolderIdParameter"
          }
        ],
        "responses": {
          "200": {
            "description": "État d'accès public après repropagation (`granted_count` renseigné)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicAccessState"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/documents/folders/{folder_id}/public_access/exclusion": {
      "post": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Exclure un dossier de l'accès public",
        "description": "Pose le marqueur « jamais public ici » sur le dossier et coupe l'accès\n**immédiatement** : les concessions qu'il portait sont retirées, celles de\nson sous-arbre aussi.\n\n**La portée est TRANSITIVE** : exclure un dossier exclut tout ce qu'il\ncontient, aujourd'hui et demain. Elle doit être annoncée à l'utilisateur\navant validation — sans quoi « jamais public ici » ne protégerait que le\nlibellé du dossier pendant que son contenu resterait lisible par URL\ndirecte.\n\nLe marqueur **survit aux repropagations** et **voyage avec la ressource** :\nun dossier exclu déplacé dans un autre sous-arbre public reste exclu. Le\nretirer est un geste explicite (`DELETE`).\n\nExclure un dossier qui est lui-même une racine publiée le dépublie : c'est\nbien le sens du geste.\n\nIdempotent : `200` si l'exclusion existait déjà, `201` sinon.\n",
        "tags": [
          "Public Access"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FolderIdParameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Exclusion déjà en place (idempotence)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicAccessState"
                }
              }
            }
          },
          "201": {
            "description": "Exclusion posée",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicAccessState"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Lever l'exclusion d'un dossier",
        "description": "Retire le marqueur « jamais public ici ». **Ne rend rien public** : cela\nrend seulement le dossier à nouveau éligible à une propagation, qui reste\nun geste distinct.\n\nIdempotent.\n",
        "tags": [
          "Public Access"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FolderIdParameter"
          }
        ],
        "responses": {
          "200": {
            "description": "État d'accès public après levée de l'exclusion",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicAccessState"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/documents/documents/{file_id}/public_access": {
      "get": {
        "security": [
          {
            "OAuth2Password": [
              "read"
            ]
          },
          {
            "OAuth2AuthCode": [
              "read"
            ]
          }
        ],
        "summary": "Lire l'état d'accès public d'un document",
        "description": "Rend l'état complet du document : publique ou non, **quelle racine**\nl'explique, s'il est exclu et par quoi.\n\nLe champ `reason` porte le diagnostic quand le document n'est pas public.\n`stale` y est le cas le plus instructif : le document a été estampillé un\njour, mais sa racine a été dépubliée ou lui-même déplacé — la concession\nest périmée, sans qu'aucune écriture n'ait été nécessaire.\n\n`subtree` est toujours `null` : un document n'a pas de sous-arbre.\n\nExige le droit d'**écriture** sur le document (cf. l'endpoint dossier).\n",
        "tags": [
          "Public Access"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FileIdParameter"
          }
        ],
        "responses": {
          "200": {
            "description": "État d'accès public du document",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicAccessState"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/documents/documents/{file_id}/public_access/exclusion": {
      "post": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Exclure un document de l'accès public",
        "description": "Pose le marqueur « jamais public ici » sur le document et coupe l'accès\nimmédiatement : la concession qu'il portait est retirée.\n\nC'est le SEUL moyen de retirer une ressource d'un sous-arbre public sans\ndépublier la racine entière.\n\nLe marqueur **survit aux repropagations** et **voyage avec le document** :\ndéplacé dans un autre sous-arbre public, il reste exclu — un marqueur de\nconfidentialité qui s'évaporerait au déplacement serait le pire des deux\nmondes.\n\nIdempotent : `200` si l'exclusion existait déjà, `201` sinon.\n",
        "tags": [
          "Public Access"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FileIdParameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Exclusion déjà en place (idempotence)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicAccessState"
                }
              }
            }
          },
          "201": {
            "description": "Exclusion posée",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicAccessState"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Lever l'exclusion d'un document",
        "description": "Retire le marqueur « jamais public ici ». **Ne rend rien public** : le\ndocument redevient seulement éligible à une propagation, qu'il faut\ndéclencher explicitement sur la racine (`POST …/public_access/propagate`).\n\nIdempotent.\n",
        "tags": [
          "Public Access"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FileIdParameter"
          }
        ],
        "responses": {
          "200": {
            "description": "État d'accès public après levée de l'exclusion",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicAccessState"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/documents/composed_documents": {
      "get": {
        "summary": "Lister les documents composés",
        "description": "Liste les `ComposedDocument` accessibles à l'utilisateur (scopés\npar `ComposedDocumentPolicy::Scope`).\n\nFiltres optionnels :\n- `workspace_id` : workspace propriétaire\n- `folder_id` : dossier propriétaire\n- `status` : draft / in_progress / ready / exported\n",
        "tags": [
          "Composed Documents"
        ],
        "parameters": [
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "folder_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "in_progress",
                "ready",
                "exported"
              ]
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Liste paginée",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "composed_documents": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ComposedDocument"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Créer un document composé à partir d'un template",
        "description": "Instancie un `ComposedDocument` depuis un `DocumentTemplate`\nidentifié par sa `key`. Le typst_source, variables_schema et\ndefault_parts sont hérités du template ; les `variables` du body\nremplissent les placeholders.\n\nPipeline d'interactor : `Documents::ComposedDocuments::CreateFromTemplate`.\n",
        "tags": [
          "Composed Documents"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContentTypeHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ComposedDocumentMutation"
                  },
                  {
                    "type": "object",
                    "required": [
                      "template_key"
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Document composé créé",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "composed_document": {
                      "$ref": "#/components/schemas/ComposedDocument"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      }
    },
    "/api/v1/documents/composed_documents/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/IdParameter"
        }
      ],
      "get": {
        "summary": "Récupérer un document composé",
        "tags": [
          "Composed Documents"
        ],
        "responses": {
          "200": {
            "description": "Document composé",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "composed_document": {
                      "$ref": "#/components/schemas/ComposedDocument"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Modifier un document composé",
        "description": "Met à jour les attributs modifiables : `name`, `description`,\n`variables`, `page_settings`, `metadata_user`. Le `typst_source`\nn'est pas modifié directement par cette route — pour modifier le\ncontenu, utiliser les endpoints `/parts`.\n",
        "tags": [
          "Composed Documents"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContentTypeHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ComposedDocumentMutation"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Mis à jour",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "composed_document": {
                      "$ref": "#/components/schemas/ComposedDocument"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      },
      "delete": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Supprimer un document composé",
        "tags": [
          "Composed Documents"
        ],
        "responses": {
          "204": {
            "description": "Supprimé"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/documents/composed_documents/{id}/export": {
      "parameters": [
        {
          "$ref": "#/components/parameters/IdParameter"
        }
      ],
      "post": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Compiler le document composé en PDF",
        "description": "Déclenche le pipeline `Documents::ComposedDocuments::AssembleAndCompile` :\n\n1. Assemble les `parts` (markdown converti, typst concaténé)\n2. Compile via `Documents::TypstCompiler` → PDF\n3. Attache le PDF au `Documents::File` du composed_document\n4. Passe le status à `exported`\n5. Republie pour ré-indexation search\n\n**Synchrone** : la réponse n'arrive qu'une fois le PDF compilé et\nattaché — pas de polling nécessaire côté client.\n\nRetourne une structure PLATE dédiée au résultat d'export (le PDF est\nen tête via `pdf_file_id`). Le détail du composé reste accessible via\n`GET /composed_documents/:id`.\n",
        "tags": [
          "Composed Documents"
        ],
        "parameters": [
          {
            "name": "include_draft",
            "in": "query",
            "description": "Inclure les parts en statut `draft` dans le rendu (défaut false).",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Compilation réussie",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "composed_document_id",
                    "pdf_file_id"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "example": true
                    },
                    "composed_document_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "pdf_file_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "pdf_url": {
                      "type": "string",
                      "example": "https://hq.sinoia.cloud/documents/browse/files/019e6ec8-86ee-7518-9d1e-25e6a2f746bf"
                    },
                    "pdf_byte_size": {
                      "type": "integer",
                      "nullable": true,
                      "example": 12415202
                    },
                    "took_ms": {
                      "type": "integer",
                      "example": 22341
                    },
                    "warnings": {
                      "type": "array",
                      "description": "Avertissements non bloquants remontés par l'assembler (#463) —\np.ex. un `header:`/`footer:`/`margin:` défini dans le\n`typst_source` qui aurait été écrasé par une part ou par\npage_settings : la directive concurrente est ignorée et un\nwarning est émis ici.\n",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Échec de compilation Typst",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/documents/composed_documents/{id}/parts": {
      "parameters": [
        {
          "$ref": "#/components/parameters/IdParameter"
        }
      ],
      "get": {
        "summary": "Lister les parts d'un document composé",
        "description": "Retourne toutes les `Documents::Part` du composed_document,\nordonnées par `position`.\n",
        "tags": [
          "Composed Documents"
        ],
        "responses": {
          "200": {
            "description": "Liste des parts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "parts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Part"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/documents/composed_documents/{id}/parts/{part_id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "ID du composed_document parent",
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        },
        {
          "name": "part_id",
          "in": "path",
          "required": true,
          "description": "ID de la part",
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "summary": "Récupérer une part",
        "tags": [
          "Composed Documents"
        ],
        "responses": {
          "200": {
            "description": "Part",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "part": {
                      "$ref": "#/components/schemas/Part"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Modifier une part",
        "description": "Met à jour les attributs modifiables d'une part : title, content,\ncontent_format, position, status, alignment, page_break, etc.\n\nLes transitions de status sont validées par\n`Documents::Part::VALID_TRANSITIONS`.\n",
        "tags": [
          "Composed Documents"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContentTypeHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartMutation"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Part mise à jour",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "part": {
                      "$ref": "#/components/schemas/Part"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      },
      "delete": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Supprimer une part",
        "tags": [
          "Composed Documents"
        ],
        "responses": {
          "204": {
            "description": "Supprimée"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/documents/composed_documents/{id}/parts/reorder": {
      "parameters": [
        {
          "$ref": "#/components/parameters/IdParameter"
        }
      ],
      "patch": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Réordonner les parts d'un document composé",
        "description": "Met à jour les positions de toutes les parts en un seul appel.\nLe body est un array `[{ id: uuid, position: int }, ...]`.\n\nToutes les parts du document doivent être incluses (sinon les\nomises restent à leur position actuelle, mais on recommande de\ntoujours envoyer la liste complète).\n",
        "tags": [
          "Composed Documents"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContentTypeHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "parts"
                ],
                "properties": {
                  "parts": {
                    "type": "array",
                    "description": "Liste {id, position} pour chaque part à repositionner",
                    "items": {
                      "type": "object",
                      "required": [
                        "id",
                        "position"
                      ],
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "position": {
                          "type": "integer",
                          "minimum": 0
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Parts réordonnées",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "parts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Part"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      }
    },
    "/api/v1/documents/document_templates": {
      "get": {
        "summary": "Lister les templates de document actifs",
        "description": "Retourne tous les `DocumentTemplate` actifs, triés par nom.\n\nTemplates = modèles de documents Typst/Markdown servant de base à\nla création de `ComposedDocument`. Read-only pour tout user\nauthentifié, CRUD réservé aux admins.\n",
        "tags": [
          "Document Templates"
        ],
        "responses": {
          "200": {
            "description": "Liste des templates",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "templates": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DocumentTemplate"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Créer un nouveau template (admin only)",
        "description": "Crée un `DocumentTemplate`. Réservé aux administrateurs (Pundit\n`DocumentTemplatePolicy#create?` = `administrator?`).\n\nLe `typst_source` et la `key` sont obligatoires. `key` doit être\nunique.\n",
        "tags": [
          "Document Templates"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContentTypeHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DocumentTemplateMutation"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Template créé",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "template": {
                      "$ref": "#/components/schemas/DocumentTemplate"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/documents/document_templates/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/IdParameter"
        }
      ],
      "get": {
        "summary": "Récupérer un template",
        "tags": [
          "Document Templates"
        ],
        "responses": {
          "200": {
            "description": "Template récupéré",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "template": {
                      "$ref": "#/components/schemas/DocumentTemplate"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Modifier un template (admin only)",
        "description": "Met à jour les attributs d'un template. Seuls les champs présents\ndans le body sont touchés. Réservé aux administrateurs.\n",
        "tags": [
          "Document Templates"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContentTypeHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DocumentTemplateMutation"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Template mis à jour",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "template": {
                      "$ref": "#/components/schemas/DocumentTemplate"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      },
      "delete": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Supprimer un template (admin only)",
        "description": "Supprime un template. Refuse si des `composed_documents` y sont\nrattachés (422). Passer `?force=true` pour nullifier les\nréférences au lieu de bloquer.\n",
        "tags": [
          "Document Templates"
        ],
        "parameters": [
          {
            "name": "force",
            "in": "query",
            "description": "Nullifier les composed_documents rattachés au lieu de bloquer",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Template supprimé"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      }
    },
    "/api/v1/documents/bulk_uploads": {
      "post": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Créer un BulkUpload",
        "description": "Créer un BulkUpload pour regrouper plusieurs fichiers uploadés séparément.\n\n**Workflow typique** :\n1. Créer un BulkUpload avec le nombre total de fichiers\n2. Uploader chaque fichier via POST /api/v1/documents/documents en passant le bulk_upload_id\n3. Récupérer le statut final via GET /api/v1/documents/bulk_uploads/:id\n\n**Cas d'usage** :\n- Import en masse depuis hubdoc-tools\n- Upload parallélisé de gros volumes\n- Traçabilité d'un lot d'uploads\n\n**Mode fusion PDF** (`merge_to_pdf: true` + `merged_file_name`) : les\nfichiers du lot ne créent pas de documents individuels ; chacun est envoyé\navec une `position` explicite, puis toutes les sources sont fusionnées en\nun seul document PDF dans l'ordre des positions dès que `total_files`\nsources ont été reçues. Suivre `GET /bulk_uploads/:id` jusqu'au statut\n`completed` pour récupérer `merged_document_id`. Formats acceptés : PDF,\nJPEG, PNG (les images deviennent une page A4).\n",
        "tags": [
          "BulkUploads"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkUploadMutation"
              },
              "examples": {
                "basic": {
                  "summary": "BulkUpload basique",
                  "value": {
                    "total_files": 50,
                    "source": "hubdoc-tools"
                  }
                },
                "with_folder": {
                  "summary": "Avec dossier de destination",
                  "value": {
                    "total_files": 25,
                    "documents_folder_id": "123e4567-e89b-12d3-a456-426614174000",
                    "workspace_id": "987e6543-e21b-12d3-a456-426614174000",
                    "auto_classify": true,
                    "source": "hubdoc-tools"
                  }
                },
                "merge_to_pdf": {
                  "summary": "Fusion du lot en un seul PDF",
                  "value": {
                    "total_files": 3,
                    "documents_folder_id": "123e4567-e89b-12d3-a456-426614174000",
                    "merge_to_pdf": true,
                    "merged_file_name": "dossier_complet",
                    "source": "hubdoc-tools"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "BulkUpload créé avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkUpload"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/documents/bulk_uploads/{id}": {
      "get": {
        "summary": "Récupérer un BulkUpload",
        "description": "Récupère les informations et le statut d'un BulkUpload.\n\n**Utilisation** :\n- Suivre la progression d'un import en masse\n- Vérifier le statut final après tous les uploads\n- Obtenir le nombre de fichiers en succès/échec\n",
        "tags": [
          "BulkUploads"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "ID du BulkUpload"
          }
        ],
        "responses": {
          "200": {
            "description": "BulkUpload récupéré avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkUpload"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/documents/chunked_uploads": {
      "post": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Initialiser une session d'upload par chunks",
        "description": "Crée une session d'upload pour un fichier volumineux qui sera uploadé par morceaux (chunks).\n\n**Workflow typique** :\n1. Initialiser la session avec ce endpoint (retourne un upload_id)\n2. Uploader chaque chunk via PATCH /api/v1/documents/chunked_uploads/{upload_id}/chunks/{chunk_number}\n3. Finaliser l'upload via POST /api/v1/documents/chunked_uploads/{upload_id}/complete\n4. Créer le document final via POST /api/v1/documents/documents avec le chunked_upload_id\n\n**Limites** :\n- Taille max de fichier : 5 GB\n- Taille de chunk recommandée : 5 MB\n- Durée de vie de la session : 24 heures\n\n**Cas d'usage** :\n- Upload de fichiers > 10 MB\n- Upload avec suivi de progression granulaire\n- Upload avec reprise en cas d'interruption\n",
        "tags": [
          "ChunkedUploads"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChunkedUploadMutation"
              },
              "examples": {
                "basic": {
                  "summary": "Session basique",
                  "value": {
                    "filename": "document.pdf",
                    "file_size": 52428800,
                    "content_type": "application/pdf",
                    "chunk_size": 5242880
                  }
                },
                "with_folder": {
                  "summary": "Avec dossier de destination",
                  "value": {
                    "filename": "presentation.pptx",
                    "file_size": 104857600,
                    "content_type": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
                    "chunk_size": 5242880,
                    "workspace_id": "987e6543-e21b-12d3-a456-426614174000",
                    "documents_folder_id": "123e4567-e89b-12d3-a456-426614174000"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Session créée avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/ChunkedUploadSessionResponse"
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "upload_id": "a1b2c3d4e5f6",
                    "chunk_size": 5242880,
                    "total_chunks": 10,
                    "expires_at": "2024-11-10T10:00:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/documents/chunked_uploads/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identifiant unique de la session d'upload (upload_id)",
          "schema": {
            "type": "string"
          },
          "example": "a1b2c3d4e5f6"
        }
      ]
    },
    "/api/v1/documents/chunked_uploads/{id}/chunks/{chunk_number}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identifiant unique de la session d'upload (upload_id)",
          "schema": {
            "type": "string"
          },
          "example": "a1b2c3d4e5f6"
        },
        {
          "name": "chunk_number",
          "in": "path",
          "required": true,
          "description": "Numéro du chunk à uploader (1-indexed)",
          "schema": {
            "type": "integer",
            "minimum": 1
          },
          "example": 5
        }
      ],
      "patch": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Uploader un chunk",
        "description": "Upload un chunk spécifique d'un fichier dans une session d'upload par chunks.\n\n**Important** :\n- Les chunks sont numérotés à partir de 1\n- Le body doit contenir les données binaires brutes du chunk\n- Content-Type doit être application/octet-stream\n- Les chunks peuvent être uploadés dans n'importe quel ordre\n- Les chunks déjà uploadés retournent une réponse idempotente (pas de re-upload)\n\n**Retry** :\n- En cas d'échec, le client peut retry le même chunk\n- L'API gère l'idempotence automatiquement\n\n**Progression** :\n- La réponse inclut la progression globale\n- Utilisez GET /api/v1/documents/chunked_uploads/{id}/status pour un statut détaillé\n",
        "tags": [
          "ChunkedUploads"
        ],
        "requestBody": {
          "required": true,
          "description": "Données binaires du chunk",
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Chunk uploadé avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/ChunkedUploadChunkResponse"
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "chunk_number": 5,
                    "upload_id": "a1b2c3d4e5f6",
                    "progress": 50,
                    "uploaded_chunks": 5,
                    "total_chunks": 10,
                    "status": "processing"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Paramètres invalides (chunk_number invalide, etc.)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Invalid chunk number"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "410": {
            "description": "Session expirée",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Upload session has expired"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Upload du chunk échoué",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Failed to upload chunk to storage"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/documents/chunked_uploads/{id}/complete": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identifiant unique de la session d'upload (upload_id)",
          "schema": {
            "type": "string"
          },
          "example": "a1b2c3d4e5f6"
        }
      ],
      "post": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Finaliser l'upload par chunks",
        "description": "Finalise une session d'upload par chunks en assemblant tous les chunks.\n\n**Prérequis** :\n- Tous les chunks doivent avoir été uploadés\n- La session ne doit pas être expirée\n\n**Processus** :\n1. Vérifie que tous les chunks sont présents\n2. Assemble les chunks dans le storage (MinIO/S3)\n3. Vérifie le checksum si fourni (recommandé)\n4. Marque la session comme \"completed\"\n5. Retourne l'object_key pour référence future\n\n**Après finalisation** :\n- Utilisez l'upload_id pour créer le document final via POST /api/v1/documents/documents\n- Le fichier assemblé est disponible dans le storage\n",
        "tags": [
          "ChunkedUploads"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChunkedUploadCompleteRequest"
              },
              "example": {
                "checksum": "1B2M2Y8AsgTpgAmY7PhCfg=="
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Upload finalisé avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/ChunkedUploadCompleteResponse"
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "upload_id": "a1b2c3d4e5f6",
                    "status": "completed",
                    "assembled_file_size": 52428800,
                    "object_key": "uploads/abc123/document.pdf"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "description": "Impossible de finaliser l'upload",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Not all chunks have been uploaded"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/documents/chunked_uploads/{id}/status": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identifiant unique de la session d'upload (upload_id)",
          "schema": {
            "type": "string"
          },
          "example": "a1b2c3d4e5f6"
        }
      ],
      "get": {
        "summary": "Obtenir le statut d'un upload",
        "description": "Récupère le statut actuel d'une session d'upload par chunks.\n\n**Informations retournées** :\n- Statut actuel (pending, processing, completed, failed, cancelled, expired)\n- Progression en pourcentage\n- Nombre de chunks uploadés vs total\n- Date d'expiration\n- Indicateur d'expiration\n\n**Cas d'usage** :\n- Suivi de progression pendant l'upload\n- Vérification avant de finaliser\n- Reprise après interruption\n- Monitoring d'uploads parallèles\n",
        "tags": [
          "ChunkedUploads"
        ],
        "responses": {
          "200": {
            "description": "Statut récupéré avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/ChunkedUploadStatusResponse"
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "upload_id": "a1b2c3d4e5f6",
                    "status": "processing",
                    "progress": 50,
                    "uploaded_chunks": 5,
                    "total_chunks": 10,
                    "expires_at": "2024-11-10T10:00:00Z",
                    "expired": false
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Session d'upload introuvable",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Upload session not found"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/documents/chunked_uploads/{id}/cancel": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Identifiant unique de la session d'upload (upload_id)",
          "schema": {
            "type": "string"
          },
          "example": "a1b2c3d4e5f6"
        }
      ],
      "delete": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Annuler un upload",
        "description": "Annule une session d'upload par chunks en cours.\n\n**Actions effectuées** :\n- Annule le multipart upload sur le storage\n- Supprime tous les chunks uploadés\n- Marque la session comme \"cancelled\"\n\n**Restrictions** :\n- Ne peut pas annuler un upload déjà complété\n- Les sessions expirées sont automatiquement nettoyées\n\n**Cas d'usage** :\n- Annulation volontaire de l'utilisateur\n- Erreur détectée côté client\n- Upload obsolète ou erroné\n",
        "tags": [
          "ChunkedUploads"
        ],
        "responses": {
          "200": {
            "description": "Upload annulé avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "cancelled"
                          ],
                          "example": "cancelled"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "status": "cancelled"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Session d'upload introuvable",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Upload session not found"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Impossible d'annuler l'upload",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Cannot cancel completed upload"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/documents/typst/fonts": {
      "get": {
        "summary": "Lister les polices Typst disponibles",
        "description": "Catalogue des polices effectivement résolvables par le compilateur\nTypst (mêmes chemins que la compilation des `ComposedDocument`).\nPermet de découvrir les `font_family` valides avant de configurer\n`page_settings.font_family`.\n\nRéponse cachée 1h (les polices ne changent qu'au déploiement / à\nl'installation de paquets).\n",
        "tags": [
          "Documents"
        ],
        "parameters": [
          {
            "name": "include_system",
            "in": "query",
            "description": "Inclure les polices système (/usr/share/fonts) en plus des polices embarquées.",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "with_variants",
            "in": "query",
            "description": "Inclure la liste des variantes (graisses/styles) par famille.",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": true
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Catalogue des polices",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "fonts",
                    "total",
                    "font_paths"
                  ],
                  "properties": {
                    "fonts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "family",
                          "variants"
                        ],
                        "properties": {
                          "family": {
                            "type": "string",
                            "example": "Plus Jakarta Sans"
                          },
                          "variants": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "example": [
                              "Regular",
                              "Bold",
                              "Italic",
                              "Bold Italic"
                            ]
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer",
                      "example": 238
                    },
                    "font_paths": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "/usr/share/fonts",
                        "engines/documents/fonts"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/contacts": {
      "get": {
        "summary": "Lister les contacts",
        "description": "Récupère une liste de contacts selon les paramètres de filtrage fournis.\n\n**Filtrage avancé avec Ransack :**\n- Utilisez le paramètre `q` pour des recherches avancées\n- Exemple : `q[email_address_cont]=example.com` pour chercher les contacts avec une adresse contenant \"example.com\"\n",
        "tags": [
          "Contacts"
        ],
        "parameters": [
          {
            "name": "sort",
            "in": "query",
            "description": "Champ de tri (email_address, first_name, last_name, created_at, updated_at, etc.)",
            "required": false,
            "schema": {
              "type": "string",
              "default": "updated_at"
            }
          },
          {
            "name": "direction",
            "in": "query",
            "description": "Direction du tri",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Nombre d'éléments par page",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Numéro de page",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Filtres Ransack pour recherche avancée.\nExemples :\n- `q[email_address_cont]=example.com` : email contenant \"example.com\"\n- `q[first_name_eq]=John` : prénom exacte\n- `q[last_name_start]=Doe` : nom commençant par \"Doe\"\n- `q[external_id_eq]=ext-123` : identifiant externe exact\n",
            "required": false,
            "style": "deepObject",
            "explode": true,
            "schema": {
              "type": "object",
              "additionalProperties": true
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Liste des contacts récupérée avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Contact"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Créer un contact",
        "description": "Créer un nouveau contact",
        "tags": [
          "Contacts"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContentTypeHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactMutation"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Contact créé avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Contact"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/contacts/{id}": {
      "get": {
        "summary": "Récupérer un contact",
        "description": "Récupérer un contact spécifique par identifiant",
        "tags": [
          "Contacts"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse réussie",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Contact"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "patch": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Mettre à jour un contact",
        "description": "Mettre à jour un contact existant",
        "tags": [
          "Contacts"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          },
          {
            "$ref": "#/components/parameters/ContentTypeHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactMutation"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Contact mis à jour avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Contact"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Supprimer un contact",
        "description": "Supprimer un contact existant",
        "tags": [
          "Contacts"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          }
        ],
        "responses": {
          "204": {
            "description": "Contact supprimé avec succès"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/groups": {
      "get": {
        "summary": "Lister les groupes",
        "description": "Récupère une liste de groupes selon les paramètres de filtrage fournis.\n\n**Filtrage avancé avec Ransack :**\n- Utilisez le paramètre `q` pour des recherches avancées\n- Exemple : `q[name_cont]=admin` pour chercher les groupes contenant \"admin\"\n",
        "tags": [
          "Groups"
        ],
        "parameters": [
          {
            "name": "sort",
            "in": "query",
            "description": "Champ de tri (name, description, created_at, updated_at, etc.)",
            "required": false,
            "schema": {
              "type": "string",
              "default": "updated_at"
            }
          },
          {
            "name": "direction",
            "in": "query",
            "description": "Direction du tri",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Nombre d'éléments par page",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Numéro de page",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Filtres Ransack pour recherche avancée.\nExemples :\n- `q[name_cont]=admin` : nom contenant \"admin\"\n- `q[description_present]=true` : avec description\n- `q[external_id_eq]=ext-group` : identifiant externe exact\n",
            "required": false,
            "style": "deepObject",
            "explode": true,
            "schema": {
              "type": "object",
              "additionalProperties": true
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Liste des groupes récupérée avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Group"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Créer un groupe",
        "description": "Créer un nouveau groupe",
        "tags": [
          "Groups"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContentTypeHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GroupMutation"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Groupe créé avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/groups/{id}": {
      "get": {
        "summary": "Récupérer un groupe",
        "description": "Récupérer un groupe spécifique par identifiant",
        "tags": [
          "Groups"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse réussie",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "patch": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Mettre à jour un groupe",
        "description": "Mettre à jour un groupe existant",
        "tags": [
          "Groups"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          },
          {
            "$ref": "#/components/parameters/ContentTypeHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GroupMutation"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Groupe mis à jour avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Supprimer un groupe",
        "description": "Supprimer un groupe existant",
        "tags": [
          "Groups"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          }
        ],
        "responses": {
          "204": {
            "description": "Groupe supprimé avec succès"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/groups/{group_id}/members": {
      "get": {
        "summary": "Lister les membres d'un groupe",
        "description": "Récupère la liste des membres d'un groupe spécifique.\nRequiert les droits administrateur.\n",
        "tags": [
          "Group Members"
        ],
        "parameters": [
          {
            "name": "group_id",
            "in": "path",
            "required": true,
            "description": "Identifiant du groupe",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed887"
          }
        ],
        "responses": {
          "200": {
            "description": "Liste des membres récupérée avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/GroupMember"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Ajouter un membre à un groupe",
        "description": "Ajoute un utilisateur comme membre d'un groupe via son identifiant externe.\nL'opération est idempotente : si l'utilisateur est déjà membre, retourne le membership existant avec un statut 200.\nRequiert les droits administrateur.\n",
        "tags": [
          "Group Members"
        ],
        "parameters": [
          {
            "name": "group_id",
            "in": "path",
            "required": true,
            "description": "Identifiant du groupe",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed887"
          },
          {
            "$ref": "#/components/parameters/ContentTypeHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GroupMemberMutation"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Le membre existait déjà dans le groupe",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GroupMember"
                }
              }
            }
          },
          "201": {
            "description": "Membre ajouté avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GroupMember"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/groups/{group_id}/members/{id}": {
      "delete": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Retirer un membre d'un groupe",
        "description": "Retire un membre d'un groupe par son identifiant de membership.\nAlternativement, le paramètre `user_external_id` peut être passé pour identifier le membre à retirer.\nRequiert les droits administrateur.\n",
        "tags": [
          "Group Members"
        ],
        "parameters": [
          {
            "name": "group_id",
            "in": "path",
            "required": true,
            "description": "Identifiant du groupe",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed887"
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Identifiant du membership",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed886"
          }
        ],
        "responses": {
          "204": {
            "description": "Membre retiré du groupe avec succès"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/permissions": {
      "get": {
        "summary": "Lister les permissions",
        "description": "Récupère une liste de permissions selon les paramètres de filtrage fournis.\n\n**Filtrage avancé avec Ransack :**\n- Utilisez le paramètre `q` pour des recherches avancées\n- Exemple : `q[level_eq]=admin` pour chercher les permissions de niveau admin\n",
        "tags": [
          "Permissions"
        ],
        "parameters": [
          {
            "name": "permissible_type",
            "in": "query",
            "description": "Type de la ressource (ex. Documents::File, Documents::Folder, Documents::Workspace)",
            "required": true,
            "schema": {
              "type": "string",
              "example": "Documents::File"
            }
          },
          {
            "name": "permissible_id",
            "in": "query",
            "description": "ID de la ressource",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "019951a3-01b7-7eb9-88bb-f872a01ed886"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Champ de tri (level, actor_type, permissible_type, created_at, updated_at, etc.)",
            "required": false,
            "schema": {
              "type": "string",
              "default": "updated_at"
            }
          },
          {
            "name": "direction",
            "in": "query",
            "description": "Direction du tri",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Nombre d'éléments par page",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Numéro de page",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Filtres Ransack pour recherche avancée.\nExemples :\n- `q[level_eq]=admin` : niveau de permission exact\n- `q[actor_type_eq]=User` : type d'acteur exact\n- `q[permissible_type_eq]=Documents::File` : type de ressource exact\n- `q[actor_id_eq]=019951a3-...` : acteur spécifique\n- `q[permissible_id_eq]=019951a3-...` : ressource spécifique\n",
            "required": false,
            "style": "deepObject",
            "explode": true,
            "schema": {
              "type": "object",
              "additionalProperties": true
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Liste des permissions récupérée avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Permission"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Créer une permission",
        "description": "Créer une nouvelle permission",
        "tags": [
          "Permissions"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContentTypeHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PermissionMutation"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Permission créée avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Permission"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/permissions/{id}": {
      "get": {
        "summary": "Récupérer une permission",
        "description": "Récupérer une permission spécifique par identifiant",
        "tags": [
          "Permissions"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse réussie",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Permission"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "patch": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Mettre à jour une permission",
        "description": "Mettre à jour une permission existante",
        "tags": [
          "Permissions"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          },
          {
            "$ref": "#/components/parameters/ContentTypeHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PermissionMutation"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Permission mise à jour avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Permission"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Supprimer une permission",
        "description": "Supprimer une permission existante",
        "tags": [
          "Permissions"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          }
        ],
        "responses": {
          "204": {
            "description": "Permission supprimée avec succès"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/users": {
      "get": {
        "summary": "Lister les utilisateurs",
        "description": "Récupère une liste d'utilisateurs selon les paramètres de filtrage fournis.\n\n**Filtrage avancé avec Ransack :**\n- Utilisez le paramètre `q` pour des recherches avancées\n- Exemple : `q[role_eq]=admin` pour chercher les utilisateurs administrateurs\n",
        "tags": [
          "Users"
        ],
        "parameters": [
          {
            "name": "sort",
            "in": "query",
            "description": "Champ de tri (email_address, first_name, last_name, role, created_at, updated_at, etc.)",
            "required": false,
            "schema": {
              "type": "string",
              "default": "updated_at"
            }
          },
          {
            "name": "direction",
            "in": "query",
            "description": "Direction du tri",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Nombre d'éléments par page",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Numéro de page",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Filtres Ransack pour recherche avancée.\nExemples :\n- `q[email_address_cont]=john` : email contenant \"john\"\n- `q[role_eq]=admin` : rôle exact\n- `q[first_name_or_last_name_cont]=doe` : prénom ou nom contenant \"doe\"\n",
            "required": false,
            "style": "deepObject",
            "explode": true,
            "schema": {
              "type": "object",
              "additionalProperties": true
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Liste des utilisateurs récupérée avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/User"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Créer un utilisateur",
        "description": "Créer un nouvel utilisateur",
        "tags": [
          "Users"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContentTypeHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserMutation"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Utilisateur créé avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/users/{id}": {
      "get": {
        "summary": "Récupérer un utilisateur",
        "description": "Récupérer un utilisateur spécifique par identifiant",
        "tags": [
          "Users"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse réussie",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "patch": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Mettre à jour un utilisateur",
        "description": "Mettre à jour un utilisateur existant",
        "tags": [
          "Users"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          },
          {
            "$ref": "#/components/parameters/ContentTypeHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserMutation"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Utilisateur mis à jour avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Supprimer un utilisateur",
        "description": "Supprimer un utilisateur existant",
        "tags": [
          "Users"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          }
        ],
        "responses": {
          "204": {
            "description": "Utilisateur supprimé avec succès"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/communications/mass_communications": {
      "get": {
        "summary": "Lister les communications de masse",
        "description": "Récupère la liste des communications de masse de l'utilisateur.\n",
        "tags": [
          "Mass Communications"
        ],
        "parameters": [
          {
            "name": "q[name_cont]",
            "in": "query",
            "description": "Filtrer par nom (contient)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q[status_eq]",
            "in": "query",
            "description": "Filtrer par statut",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "sending",
                "sent",
                "failed"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Liste des communications de masse",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "mass_communications": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MassCommunication"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "security": [
          {
            "OAuth2Password": [
              "write"
            ]
          },
          {
            "OAuth2AuthCode": [
              "write"
            ]
          }
        ],
        "summary": "Créer une communication de masse",
        "description": "Crée une nouvelle communication de masse avec ses destinataires et pièces jointes.\n\n**Modes de création:**\n- **Brouillon** (par défaut): La communication est créée mais pas envoyée\n- **Envoi immédiat** (`send=true`): La communication est créée et envoyée immédiatement\n\n**Destinataires:**\nLes destinataires peuvent être fournis de deux façons:\n- Directement dans le JSON avec email, prénom, nom\n- Via un fichier CSV/Excel uploadé dans le champ `file`\n\n**Variables de template:**\nLe corps du message supporte les variables Liquid:\n- `{{ recipient.first_name }}` - Prénom du destinataire\n- `{{ recipient.last_name }}` - Nom du destinataire\n- `{{ recipient.email }}` - Email du destinataire\n- `{{ recipient.custom_attributes.xxx }}` - Attributs personnalisés\n",
        "tags": [
          "Mass Communications"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MassCommunicationMutation"
              },
              "example": {
                "mass_communication": {
                  "name": "Newsletter Janvier 2024",
                  "subject": "Actualités du mois",
                  "body": "Bonjour {{ recipient.first_name }},\n\nVoici les actualités...",
                  "communication_type": "email"
                },
                "recipients": [
                  {
                    "email": "john@example.com",
                    "first_name": "John",
                    "last_name": "Doe"
                  },
                  {
                    "email": "jane@example.com",
                    "first_name": "Jane",
                    "last_name": "Smith"
                  }
                ],
                "send": false
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "mass_communication[name]": {
                    "type": "string"
                  },
                  "mass_communication[subject]": {
                    "type": "string"
                  },
                  "mass_communication[body]": {
                    "type": "string"
                  },
                  "recipients[][email]": {
                    "type": "string"
                  },
                  "recipients[][first_name]": {
                    "type": "string"
                  },
                  "recipients[][last_name]": {
                    "type": "string"
                  },
                  "recipients[][file]": {
                    "type": "string",
                    "format": "binary",
                    "description": "Fichier CSV/Excel contenant les destinataires"
                  },
                  "attachments[][file]": {
                    "type": "string",
                    "format": "binary"
                  },
                  "attachments[][name]": {
                    "type": "string"
                  },
                  "send": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Communication de masse créée avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MassCommunicationResponse"
                },
                "example": {
                  "mass_communication": {
                    "id": "550e8400-e29b-41d4-a716-446655440000",
                    "name": "Newsletter Janvier 2024",
                    "subject": "Actualités du mois",
                    "status": "draft",
                    "recipients_count": 2,
                    "created_at": "2024-01-16T10:00:00Z"
                  },
                  "recipients": [
                    {
                      "id": "660e8400-e29b-41d4-a716-446655440001",
                      "email": "john@example.com",
                      "first_name": "John",
                      "last_name": "Doe",
                      "status": "pending"
                    }
                  ],
                  "attachments": [],
                  "skipped_recipients": [],
                  "sending": false,
                  "enqueued_count": 0
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "description": "Erreur de validation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "name_required": {
                    "summary": "Nom requis",
                    "value": {
                      "message": "Le nom est requis",
                      "error": "unprocessable_entity"
                    }
                  },
                  "recipients_required": {
                    "summary": "Destinataires requis",
                    "value": {
                      "message": "Au moins un destinataire est requis",
                      "error": "unprocessable_entity"
                    }
                  },
                  "subject_required_for_send": {
                    "summary": "Sujet requis pour l'envoi",
                    "value": {
                      "message": "Le sujet est requis pour envoyer la communication",
                      "error": "unprocessable_entity"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/communications/mass_communications/{id}": {
      "get": {
        "summary": "Récupérer une communication de masse",
        "description": "Récupère les détails d'une communication de masse avec ses destinataires et pièces jointes.\n",
        "tags": [
          "Mass Communications"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdParameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Communication de masse récupérée avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MassCommunicationResponse"
                },
                "example": {
                  "mass_communication": {
                    "id": "550e8400-e29b-41d4-a716-446655440000",
                    "name": "Newsletter Janvier 2024",
                    "subject": "Actualités du mois",
                    "body": "Bonjour {{ recipient.first_name }}...",
                    "communication_type": "email",
                    "status": "sent",
                    "recipients_count": 150,
                    "sent_count": 148,
                    "failed_count": 2,
                    "created_at": "2024-01-16T10:00:00Z",
                    "sent_at": "2024-01-16T10:05:00Z"
                  },
                  "recipients": [
                    {
                      "id": "660e8400-e29b-41d4-a716-446655440001",
                      "email": "john@example.com",
                      "first_name": "John",
                      "last_name": "Doe",
                      "status": "sent",
                      "sent_at": "2024-01-16T10:05:01Z"
                    }
                  ],
                  "attachments": [
                    {
                      "id": "770e8400-e29b-41d4-a716-446655440002",
                      "name": "document.pdf",
                      "scope_type": "all"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "IdParameter": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Identifiant de la ressource",
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "example": "019951a3-01b7-7eb9-88bb-f872a01ed886"
      },
      "ContentTypeHeader": {
        "name": "Content-Type",
        "in": "header",
        "required": true,
        "description": "Type de contenu de la requête",
        "schema": {
          "type": "string",
          "enum": [
            "application/json"
          ]
        },
        "example": "application/json"
      },
      "FolderIdParameter": {
        "name": "folder_id",
        "in": "path",
        "required": true,
        "description": "Identifiant du dossier",
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "example": "019951a3-01b7-7eb9-88bb-f872a01ed886"
      },
      "PublicAccessLimitParameter": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Borne le tableau `subtree.unstamped_items`. Défaut 50, maximum 500.\nLe décompte `subtree.unstamped` reste exact quelle que soit la borne.\n",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "maximum": 500,
          "default": 50
        }
      },
      "FileIdParameter": {
        "name": "file_id",
        "in": "path",
        "required": true,
        "description": "Identifiant du document",
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "example": "019951a3-01b7-7eb9-88bb-f872a01ed887"
      }
    },
    "schemas": {
      "Workspace": {
        "type": "object",
        "required": [
          "id",
          "label",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant unique de l'espace de travail",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed886"
          },
          "label": {
            "type": "string",
            "description": "Libellé de l'espace de travail",
            "example": "Project Alpha"
          },
          "description": {
            "type": "string",
            "description": "Description de l'espace de travail",
            "example": "Main project workspace"
          },
          "external_id": {
            "type": "string",
            "nullable": true,
            "description": "Identifiant externe pour l'intégration avec des systèmes tiers",
            "example": "ext-workspace-12345"
          },
          "classification_plan_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Identifiant du plan de classement associé",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed886"
          },
          "classification_plan_label": {
            "type": "string",
            "nullable": true,
            "description": "Libellé du plan de classement associé",
            "example": "Standard Plan"
          },
          "domain_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Identifiant du domaine associé",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed886"
          },
          "completion_percentage": {
            "type": "number",
            "description": "Pourcentage d'avancement de l'espace de travail",
            "example": 75.5
          },
          "settings": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "nullable": true,
                "description": "Code de l'espace de travail",
                "example": "PROJ-001"
              },
              "start_date": {
                "type": "string",
                "format": "date",
                "nullable": true,
                "description": "Date de début du projet",
                "example": "2023-01-01"
              },
              "color": {
                "type": "string",
                "nullable": true,
                "description": "Couleur de l'espace de travail",
                "example": "#3498db"
              },
              "icon": {
                "type": "string",
                "nullable": true,
                "description": "Icône de l'espace de travail",
                "example": "folder"
              },
              "address": {
                "type": "string",
                "nullable": true,
                "description": "Adresse du projet",
                "example": "123 Main St"
              },
              "postal_code": {
                "type": "string",
                "nullable": true,
                "description": "Code postal du projet",
                "example": "12345"
              },
              "city": {
                "type": "string",
                "nullable": true,
                "description": "Ville du projet",
                "example": "Paris"
              },
              "agency": {
                "type": "string",
                "nullable": true,
                "description": "Agence du projet",
                "example": "Construction Agency"
              },
              "project_type": {
                "type": "string",
                "nullable": true,
                "description": "Type de projet",
                "example": "Construction"
              },
              "status": {
                "type": "string",
                "nullable": true,
                "description": "Statut du projet",
                "example": "active"
              },
              "end_date": {
                "type": "string",
                "format": "date",
                "nullable": true,
                "description": "Date de fin du projet",
                "example": "2023-12-31"
              },
              "validation": {
                "type": "object",
                "properties": {
                  "default_deadline_days": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Délai par défaut en jours",
                    "example": 30
                  },
                  "auto_reminder_days": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "nullable": true,
                    "description": "Rappel automatique en jours",
                    "example": [
                      7,
                      14
                    ]
                  },
                  "super_validator_ids": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "Identifiants des super validateurs",
                    "example": [
                      "019951a3-01b7-7eb9-88bb-f872a01ed886",
                      "019951a3-01b7-7eb9-88bb-f872a01ed887"
                    ]
                  }
                }
              }
            }
          },
          "metadata_user": {
            "type": "object",
            "description": "Métadonnées définies par l'utilisateur"
          },
          "user": {
            "$ref": "#/components/schemas/User"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Horodatage de création",
            "example": "2023-01-01T00:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Horodatage de dernière modification",
            "example": "2023-01-01T00:00:00Z"
          }
        }
      },
      "Folder": {
        "type": "object",
        "required": [
          "id",
          "name",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant unique du dossier",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed886"
          },
          "name": {
            "type": "string",
            "description": "Nom du dossier",
            "example": "Documents"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Description du dossier",
            "example": "Main documents folder"
          },
          "color": {
            "type": "string",
            "nullable": true,
            "description": "Couleur du dossier",
            "example": "#e74c3c"
          },
          "icon": {
            "type": "string",
            "nullable": true,
            "description": "Icône du dossier",
            "example": "folder-open"
          },
          "documents_folder_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Identifiant du dossier parent",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed887"
          },
          "workspace_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Identifiant de l'espace de travail associé",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed888"
          },
          "external_id": {
            "type": "string",
            "nullable": true,
            "description": "Identifiant externe pour l'intégration avec des systèmes tiers",
            "example": "ext-folder-12345"
          },
          "classification_plan_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Identifiant du plan de classement (délégué via workspace)",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed891"
          },
          "workspace": {
            "type": "object",
            "allOf": [
              {
                "$ref": "#/components/schemas/Workspace"
              }
            ],
            "nullable": true,
            "description": "Espace de travail associé (si présent)"
          },
          "parent": {
            "type": "object",
            "nullable": true,
            "description": "Dossier parent (si présent)",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "example": "019951a3-01b7-7eb9-88bb-f872a01ed887"
              },
              "name": {
                "type": "string",
                "example": "Parent Folder"
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Horodatage de création",
            "example": "2023-01-01T00:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Horodatage de dernière modification",
            "example": "2023-01-01T00:00:00Z"
          }
        }
      },
      "Document": {
        "type": "object",
        "required": [
          "id",
          "name",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant unique du fichier",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed886"
          },
          "name": {
            "type": "string",
            "description": "Nom du fichier",
            "example": "document.pdf"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Description du fichier",
            "example": "Important document"
          },
          "documents_folder_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Identifiant du dossier parent",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed887"
          },
          "workspace_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Identifiant de l'espace de travail associé",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed888"
          },
          "domain_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Identifiant du domaine associé",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed889"
          },
          "document_type_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Identifiant du type de document associé",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed890"
          },
          "external_id": {
            "type": "string",
            "nullable": true,
            "description": "Identifiant externe pour l'intégration avec des systèmes tiers",
            "example": "ext-doc-12345"
          },
          "classification_plan_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Identifiant du plan de classement (délégué via workspace)",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed891"
          },
          "byte_size": {
            "type": "integer",
            "format": "int64",
            "nullable": true,
            "description": "Taille en octets du fichier attaché. `null` pour une note sans fichier. Exposée pour les connecteurs natifs (Cloud Filter API Windows / File Provider macOS), qui doivent dimensionner le placeholder avant hydratation.",
            "example": 342502
          },
          "content_type": {
            "type": "string",
            "nullable": true,
            "description": "Type MIME du fichier attaché. `null` pour une note sans fichier.",
            "example": "application/pdf"
          },
          "metadata_user": {
            "type": "object",
            "description": "Métadonnées définies par l'utilisateur"
          },
          "workspace": {
            "type": "object",
            "allOf": [
              {
                "$ref": "#/components/schemas/Workspace"
              }
            ],
            "nullable": true,
            "description": "Espace de travail associé (si présent)"
          },
          "folder": {
            "type": "object",
            "nullable": true,
            "description": "Dossier parent (si présent)",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "example": "019951a3-01b7-7eb9-88bb-f872a01ed887"
              },
              "name": {
                "type": "string",
                "example": "Documents"
              }
            }
          },
          "domain": {
            "type": "object",
            "nullable": true,
            "description": "Domaine associé (si présent)",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "example": "019951a3-01b7-7eb9-88bb-f872a01ed889"
              },
              "name": {
                "type": "string",
                "example": "Legal"
              }
            }
          },
          "document_type": {
            "type": "object",
            "nullable": true,
            "description": "Type de document associé (si présent)",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "example": "019951a3-01b7-7eb9-88bb-f872a01ed890"
              },
              "name": {
                "type": "string",
                "example": "Contract"
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Horodatage de création",
            "example": "2023-01-01T00:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Horodatage de dernière modification",
            "example": "2023-01-01T00:00:00Z"
          }
        }
      },
      "User": {
        "type": "object",
        "required": [
          "id",
          "email_address",
          "role",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant utilisateur",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed886"
          },
          "email_address": {
            "type": "string",
            "format": "email",
            "description": "Adresse email de l'utilisateur",
            "example": "john.doe@example.com"
          },
          "first_name": {
            "type": "string",
            "nullable": true,
            "description": "Prénom de l'utilisateur",
            "example": "John"
          },
          "last_name": {
            "type": "string",
            "nullable": true,
            "description": "Nom de famille de l'utilisateur",
            "example": "Doe"
          },
          "display_name": {
            "type": "string",
            "description": "Nom complet de l'utilisateur",
            "example": "John Doe"
          },
          "initials": {
            "type": "string",
            "description": "Initiales de l'utilisateur",
            "example": "JD"
          },
          "role": {
            "type": "string",
            "enum": [
              "user",
              "manager",
              "admin",
              "super_admin"
            ],
            "description": "Rôle de l'utilisateur",
            "example": "user"
          },
          "externa_id": {
            "type": "string",
            "nullable": true,
            "description": "Identifiant externe pour l'intégration avec des systèmes tiers",
            "example": "ext-user-12345"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Horodatage de création",
            "example": "2023-01-01T00:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Horodatage de dernière modification",
            "example": "2023-01-01T00:00:00Z"
          }
        }
      },
      "BulkUpload": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant unique du BulkUpload"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "in_progress",
              "completed",
              "failed"
            ],
            "description": "Statut du traitement du lot"
          },
          "source": {
            "type": "string",
            "description": "Source de l'upload (api, hubdoc-tools, manual, etc.)"
          },
          "total_files": {
            "type": "integer",
            "minimum": 0,
            "description": "Nombre total de fichiers dans le lot"
          },
          "processed_files": {
            "type": "integer",
            "minimum": 0,
            "description": "Nombre de fichiers traités"
          },
          "successful_files": {
            "type": "integer",
            "minimum": 0,
            "description": "Nombre de fichiers uploadés avec succès"
          },
          "failed_files": {
            "type": "integer",
            "minimum": 0,
            "description": "Nombre de fichiers en échec"
          },
          "progress": {
            "type": "number",
            "format": "float",
            "minimum": 0,
            "maximum": 100,
            "description": "Pourcentage de progression (processed_files / total_files * 100)"
          },
          "auto_classify": {
            "type": "boolean",
            "description": "Classification automatique activée"
          },
          "merge_to_pdf": {
            "type": "boolean",
            "description": "Lot en mode fusion (toutes les sources sont fusionnées en un seul PDF)"
          },
          "merged_file_name": {
            "type": "string",
            "nullable": true,
            "description": "Nom du document PDF fusionné (mode fusion uniquement)"
          },
          "received_files": {
            "type": "integer",
            "minimum": 0,
            "description": "Nombre de sources déjà reçues (présent uniquement en mode fusion)"
          },
          "merged_document_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "ID du document PDF fusionné, disponible quand le lot est complété (mode fusion uniquement)"
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "description": "Date et heure de début du traitement"
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Date et heure de fin du traitement"
          },
          "folder_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "ID du dossier de destination"
          },
          "workspace_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "ID du workspace"
          },
          "user_id": {
            "type": "string",
            "format": "uuid",
            "description": "ID de l'utilisateur ayant initié l'upload"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Métadonnées supplémentaires"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Date de création"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Date de dernière modification"
          }
        },
        "required": [
          "id",
          "status",
          "total_files",
          "processed_files",
          "successful_files",
          "failed_files",
          "user_id",
          "created_at",
          "updated_at"
        ]
      },
      "ChunkedUpload": {
        "type": "object",
        "description": "Session d'upload par chunks pour fichiers volumineux",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant unique de la session d'upload"
          },
          "upload_id": {
            "type": "string",
            "description": "Identifiant de la session d'upload (format hex)",
            "example": "a1b2c3d4e5f6"
          },
          "filename": {
            "type": "string",
            "description": "Nom du fichier à uploader",
            "example": "document.pdf"
          },
          "file_size": {
            "type": "integer",
            "format": "int64",
            "description": "Taille totale du fichier en octets",
            "example": 52428800
          },
          "content_type": {
            "type": "string",
            "description": "Type MIME du fichier",
            "example": "application/pdf"
          },
          "chunk_size": {
            "type": "integer",
            "description": "Taille de chaque chunk en octets",
            "example": 5242880
          },
          "total_chunks": {
            "type": "integer",
            "description": "Nombre total de chunks",
            "example": 10
          },
          "uploaded_chunks": {
            "type": "integer",
            "description": "Nombre de chunks déjà uploadés",
            "example": 5
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed",
              "cancelled",
              "expired"
            ],
            "description": "Statut de l'upload:\n- pending: En attente, aucun chunk uploadé\n- processing: Upload en cours\n- completed: Upload terminé et assemblé\n- failed: Échec de l'upload\n- cancelled: Upload annulé\n- expired: Session expirée\n",
            "example": "processing"
          },
          "progress": {
            "type": "number",
            "format": "float",
            "description": "Pourcentage de progression (0-100)",
            "example": 50
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Date d'expiration de la session (24h par défaut)",
            "example": "2024-11-10T10:00:00Z"
          },
          "expired": {
            "type": "boolean",
            "description": "Indique si la session est expirée",
            "example": false
          },
          "checksum": {
            "type": "string",
            "nullable": true,
            "description": "Checksum MD5 du fichier complet (base64)",
            "example": "1B2M2Y8AsgTpgAmY7PhCfg=="
          },
          "storage_object_key": {
            "type": "string",
            "nullable": true,
            "description": "Clé de l'objet dans le storage après assemblage",
            "example": "uploads/abc123/document.pdf"
          },
          "workspace_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "ID du workspace de destination"
          },
          "documents_folder_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "ID du dossier de destination"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Date de création"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Date de dernière modification"
          }
        },
        "required": [
          "id",
          "upload_id",
          "filename",
          "file_size",
          "chunk_size",
          "total_chunks",
          "uploaded_chunks",
          "status",
          "expires_at"
        ]
      },
      "Contact": {
        "type": "object",
        "required": [
          "id",
          "email_address",
          "type",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant unique du contact",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed886"
          },
          "email_address": {
            "type": "string",
            "format": "email",
            "description": "Adresse email du contact",
            "example": "contact@example.com"
          },
          "first_name": {
            "type": "string",
            "nullable": true,
            "description": "Prénom du contact",
            "example": "John"
          },
          "last_name": {
            "type": "string",
            "nullable": true,
            "description": "Nom du contact",
            "example": "Doe"
          },
          "external_id": {
            "type": "string",
            "nullable": true,
            "description": "Identifiant externe pour l'intégration avec des systèmes tiers",
            "example": "ext-12345"
          },
          "display_name": {
            "type": "string",
            "description": "Nom d'affichage du contact (email ou prénom/nom)",
            "example": "John Doe"
          },
          "full_name": {
            "type": "string",
            "nullable": true,
            "description": "Nom complet du contact (prénom + nom)",
            "example": "John Doe"
          },
          "type": {
            "type": "string",
            "description": "Type d'acteur",
            "example": "Contact"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Horodatage de création",
            "example": "2023-01-01T00:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Horodatage de dernière modification",
            "example": "2023-01-01T00:00:00Z"
          }
        }
      },
      "Group": {
        "type": "object",
        "required": [
          "id",
          "name",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant unique du groupe",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed886"
          },
          "name": {
            "type": "string",
            "description": "Nom du groupe",
            "example": "Administrators"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Description du groupe",
            "example": "Group for system administrators"
          },
          "external_id": {
            "type": "string",
            "nullable": true,
            "description": "Identifiant externe pour l'intégration avec des systèmes tiers",
            "example": "ext-admin-group"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Horodatage de création",
            "example": "2023-01-01T00:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Horodatage de dernière modification",
            "example": "2023-01-01T00:00:00Z"
          }
        }
      },
      "GroupMember": {
        "type": "object",
        "required": [
          "id",
          "group_id",
          "member_id",
          "member_type",
          "role",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant unique de l'adhésion au groupe",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed886"
          },
          "group_id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du groupe",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed887"
          },
          "member_id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du membre",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed888"
          },
          "member_type": {
            "type": "string",
            "description": "Type du membre (User ou Group)",
            "example": "User",
            "enum": [
              "User",
              "Group"
            ]
          },
          "role": {
            "type": "string",
            "description": "Rôle du membre dans le groupe",
            "example": "member",
            "enum": [
              "member",
              "admin",
              "owner"
            ]
          },
          "user_external_id": {
            "type": "string",
            "nullable": true,
            "description": "Identifiant externe de l'utilisateur (si le membre est un User)",
            "example": "ext-user-123"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Horodatage de création",
            "example": "2023-01-01T00:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Horodatage de dernière modification",
            "example": "2023-01-01T00:00:00Z"
          }
        }
      },
      "Permission": {
        "type": "object",
        "required": [
          "id",
          "level",
          "actor_type",
          "permissible_type",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant unique de la permission",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed886"
          },
          "level": {
            "type": "string",
            "enum": [
              "read",
              "write",
              "admin"
            ],
            "description": "Niveau de permission (read, write, admin)",
            "example": "read"
          },
          "actor_id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de l'acteur (User, Contact, Group)",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed887"
          },
          "actor_type": {
            "type": "string",
            "description": "Type d'acteur (User, Contact, Group)",
            "example": "User"
          },
          "permissible_id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la ressource protégée",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed888"
          },
          "permissible_type": {
            "type": "string",
            "description": "Type de ressource protégée (Documents::File, Documents::Folder, etc.)",
            "example": "Documents::File"
          },
          "external_id": {
            "type": "string",
            "nullable": true,
            "description": "Identifiant externe pour l'intégration avec des systèmes tiers",
            "example": "ext-perm-12345"
          },
          "actor": {
            "type": "object",
            "nullable": true,
            "description": "Acteur ayant la permission (si présent)",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "example": "019951a3-01b7-7eb9-88bb-f872a01ed887"
              },
              "display_name": {
                "type": "string",
                "example": "John Doe"
              },
              "type": {
                "type": "string",
                "example": "User"
              }
            }
          },
          "permissible": {
            "type": "object",
            "nullable": true,
            "description": "Ressource protégée (si présent)",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "example": "019951a3-01b7-7eb9-88bb-f872a01ed888"
              },
              "name": {
                "type": "string",
                "example": "document.pdf"
              },
              "type": {
                "type": "string",
                "example": "Documents::File"
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Horodatage de création",
            "example": "2023-01-01T00:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Horodatage de dernière modification",
            "example": "2023-01-01T00:00:00Z"
          }
        }
      },
      "ComposedDocument": {
        "type": "object",
        "required": [
          "id",
          "name",
          "status",
          "user_id",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant unique",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed999"
          },
          "name": {
            "type": "string",
            "description": "Nom du document composé",
            "example": "Facture Sinoia 2026-05"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "in_progress",
              "ready",
              "exported"
            ],
            "description": "Cycle de vie : draft → in_progress → ready → exported. Le passage\nà `exported` est piloté par l'action POST /export qui compile\nen PDF et attache le résultat.\n"
          },
          "assembly_mode": {
            "type": "string",
            "enum": [
              "composed",
              "pure_typst"
            ],
            "description": "Mode d'assemblage du PDF (#463) :\n- `composed` (défaut) : l'assembler combine page_settings + variables\n  + typst_source + header/footer + parts.\n- `pure_typst` : le `typst_source` EST le document, aucune injection\n  structurelle (seules les substitutions `{{var:x}}` et `{{image:slug}}`\n  sont appliquées). Évite les effets de couplage (header/margin écrasés).\n"
          },
          "typst_source": {
            "type": "string",
            "nullable": true,
            "description": "Source Typst (héritée du template à la création, modifiable). En mode\n`pure_typst`, c'est l'unique source du rendu.\n"
          },
          "variables": {
            "type": "object",
            "additionalProperties": true,
            "description": "Valeurs des variables du template (clé → valeur)",
            "example": {
              "client": "Sinoia",
              "montant": 1500
            }
          },
          "page_settings": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true
          },
          "metadata": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true
          },
          "metadata_ai": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true
          },
          "metadata_user": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true
          },
          "document_template_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Template d'origine (nullable si template supprimé avec force)"
          },
          "file_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Fichier Documents::File résultant du compile (PDF). Renseigné\nautomatiquement après le premier appel à POST /export.\n"
          },
          "folder_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "workspace_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "user_id": {
            "type": "string",
            "format": "uuid",
            "description": "Créateur"
          },
          "parts": {
            "type": "array",
            "description": "Parts (sections) composant le document",
            "items": {
              "$ref": "#/components/schemas/Part"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Part": {
        "type": "object",
        "required": [
          "id",
          "title",
          "content_format",
          "part_type",
          "position",
          "status",
          "composed_document_id",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string",
            "description": "Titre de la part (visible en édition)",
            "example": "Annexe — Conditions générales"
          },
          "content": {
            "type": "string",
            "description": "Contenu de la part. Si `content_format` = \"markdown\", parsé en\nHTML/PDF via la pipeline markdown. Si \"typst\", concaténé tel\nquel dans le source assemblé.\n"
          },
          "content_format": {
            "type": "string",
            "enum": [
              "typst",
              "markdown"
            ],
            "description": "Format du contenu",
            "default": "typst"
          },
          "content_source": {
            "type": "string",
            "enum": [
              "human",
              "agent",
              "template"
            ],
            "description": "Source du contenu : édité par humain, généré par agent IA, ou\nhérité du template par défaut.\n",
            "default": "human"
          },
          "part_type": {
            "type": "string",
            "enum": [
              "content",
              "header",
              "footer",
              "signature",
              "appendix"
            ],
            "description": "Rôle structurel de la part",
            "default": "content"
          },
          "position": {
            "type": "integer",
            "description": "Ordre dans le document (ascending)"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "editing",
              "ready",
              "validated",
              "locked"
            ],
            "description": "Cycle de vie de la part. Transitions valides limitées\n(cf. Documents::Part::VALID_TRANSITIONS).\n"
          },
          "alignment": {
            "type": "string",
            "enum": [
              "left",
              "center",
              "right"
            ],
            "default": "left"
          },
          "bsize": {
            "type": "integer",
            "description": "Largeur en grid columns (1-12)",
            "default": 12
          },
          "page_break": {
            "type": "boolean",
            "description": "Forcer un saut de page avant cette part",
            "default": false
          },
          "row_group": {
            "type": "integer",
            "nullable": true,
            "description": "Regroupement de parts sur une même ligne (grid layout)"
          },
          "key": {
            "type": "string",
            "nullable": true,
            "description": "Clé fonctionnelle (référence dans le template)"
          },
          "instructions": {
            "type": "string",
            "nullable": true,
            "description": "Instructions pour l'humain ou l'agent qui édite cette part"
          },
          "assignee_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "assignee_type": {
            "type": "string",
            "nullable": true,
            "description": "Type polymorphique : User ou Agents::Agent"
          },
          "markdown_file_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Si le contenu vient d'un fichier markdown éditable (live edit\nvia le markdown editor), référence le Documents::File source.\n"
          },
          "metadata": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true
          },
          "composed_document_id": {
            "type": "string",
            "format": "uuid"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DocumentTemplate": {
        "type": "object",
        "required": [
          "id",
          "key",
          "name",
          "typst_source",
          "category",
          "active",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant unique du template",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed999"
          },
          "key": {
            "type": "string",
            "description": "Slug unique du template (utilisé pour création depuis CLI/API)",
            "example": "facture-edf"
          },
          "name": {
            "type": "string",
            "description": "Nom affiché du template",
            "example": "Facture EDF"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Description longue du template",
            "example": "Modèle de facture standard EDF"
          },
          "category": {
            "type": "string",
            "description": "Catégorie fonctionnelle (billing, contract, report...)",
            "example": "billing"
          },
          "locale": {
            "type": "string",
            "description": "Locale de référence du template",
            "default": "fr",
            "example": "fr"
          },
          "typst_source": {
            "type": "string",
            "description": "Source Typst du template. Peut contenir des `{{placeholders}}` qui\nseront remplacés par les `variables` d'un ComposedDocument\ninstancié depuis ce template.\n",
            "example": "#set page(paper: \"a4\")\n= Facture {{client}}\n"
          },
          "variables_schema": {
            "type": "object",
            "nullable": true,
            "description": "JSON Schema décrivant les variables attendues. Format simplifié :\n`{ \"properties\": { \"key\": { \"type\": \"string\", \"title\": \"Label\" } }, \"required\": [\"key\"] }`\n",
            "additionalProperties": true,
            "example": {
              "properties": {
                "client": {
                  "type": "string",
                  "title": "Nom du client"
                },
                "montant": {
                  "type": "number"
                }
              },
              "required": [
                "client"
              ]
            }
          },
          "default_parts": {
            "type": "array",
            "description": "Liste des parts par défaut du template (header, footer, content,\nsignature, appendix). Chaque part est un objet libre avec au\nmoins title/content/content_format/position. Stockage JSONB direct\nsur le template (pas une relation séparée).\n",
            "items": {
              "type": "object",
              "additionalProperties": true
            },
            "example": [
              {
                "title": "Header",
                "content": "Logo EDF",
                "content_format": "typst",
                "part_type": "header",
                "position": 1
              },
              {
                "title": "Corps",
                "content": "Facture pour {{client}} : {{montant}}€",
                "content_format": "typst",
                "part_type": "content",
                "position": 2
              }
            ]
          },
          "page_settings": {
            "type": "object",
            "nullable": true,
            "description": "Configuration page (margins, headers, font...)",
            "additionalProperties": true
          },
          "active": {
            "type": "boolean",
            "description": "Le template est-il actif (listable via index)",
            "default": true,
            "example": true
          },
          "solution_instance_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Solution Pack qui a fourni ce template (si applicable)"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Horodatage de création"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Horodatage de dernière modification"
          }
        }
      },
      "PublicAccessState": {
        "type": "object",
        "description": "État d'accès public d'une ressource — la réponse de TOUS les endpoints\nd'accès public, lecture comme mutations : après un geste, on rend l'état\nqui en résulte.\n\n## Pourquoi un état, et pas un booléen\n\nL'accès public est une **concession estampillée et périssable** : elle est\nposée explicitement sur chaque ressource lors d'une propagation depuis un\ndossier racine, et elle cesse de valoir dès que la racine est dépubliée ou\nque la ressource change de place. Conséquence assumée (fail-closed) : **un\ndocument déposé après la propagation n'est PAS public** tant qu'on n'a pas\nrepropagé (`POST …/public_access/propagate`).\n\nC'est le piège le plus probable du modèle : sans `reason` et sans\n`subtree`, un appelant ne peut ni expliquer ni réparer un refus, et le\nfail-closed passe pour un bug.\n\n## Les deux références nullables\n\n- `root` : le dossier racine dont la ressource tient son accès public.\n  `null` si la ressource n'est pas publique.\n- `excluded_by` : le dossier ancêtre exclu le plus proche, qui coupe\n  l'accès de tout son contenu. `null` si aucun ancêtre n'est exclu.\n",
        "required": [
          "resource",
          "public",
          "published_root",
          "excluded"
        ],
        "properties": {
          "resource": {
            "$ref": "#/components/schemas/PublicAccessResourceRef"
          },
          "public": {
            "type": "boolean",
            "description": "La ressource est-elle lisible anonymement, ici et maintenant ?",
            "example": true
          },
          "reason": {
            "type": "string",
            "nullable": true,
            "enum": [
              "not_stamped",
              "excluded",
              "excluded_by_ancestor",
              "stale",
              "trashed",
              null
            ],
            "description": "`null` quand la ressource est publique. Sinon, la cause :\n\n| Valeur | Sens |\n|---|---|\n| `not_stamped` | jamais estampillée — dépôt après propagation, ou dossier jamais publié |\n| `excluded` | marqueur « jamais public ici » posé sur la ressource elle-même |\n| `excluded_by_ancestor` | un dossier ancêtre est exclu ; l'exclusion est transitive |\n| `stale` | concession PÉRIMÉE : racine dépubliée, ou ressource déplacée |\n| `trashed` | ressource à la corbeille |\n",
            "example": "not_stamped"
          },
          "published_root": {
            "type": "boolean",
            "description": "La ressource est-elle elle-même une racine publiée ? Toujours `false`\npour un document : la racine d'une publication est toujours un dossier.\n",
            "example": true
          },
          "root": {
            "$ref": "#/components/schemas/PublicAccessResourceRef"
          },
          "excluded": {
            "type": "boolean",
            "description": "Un marqueur « jamais public ici » est-il posé sur la ressource elle-même ?",
            "example": false
          },
          "excluded_by": {
            "$ref": "#/components/schemas/PublicAccessResourceRef"
          },
          "granted_count": {
            "type": "integer",
            "description": "Nombre de ressources estampillées par le geste qui vient d'avoir lieu.\nPrésent uniquement sur les réponses de publication et de propagation.\n",
            "example": 42
          },
          "subtree": {
            "type": "object",
            "nullable": true,
            "description": "Bilan du sous-arbre. Présent uniquement pour un **dossier qui participe\nà une publication** (racine publiée, ou dossier lui-même publiquement\nlisible) : ailleurs, « non estampillé » ne veut rien dire. Toujours\n`null` pour un document.\n\nLes trois catégories sont disjointes : `total = granted + excluded + unstamped`.\n",
            "required": [
              "total",
              "granted",
              "excluded",
              "unstamped",
              "unstamped_items"
            ],
            "properties": {
              "total": {
                "type": "integer",
                "description": "Ressources du sous-arbre, corbeille exclue",
                "example": 120
              },
              "granted": {
                "type": "integer",
                "description": "Ressources porteuses d'une concession valide et non exclues",
                "example": 118
              },
              "excluded": {
                "type": "integer",
                "description": "Ressources exclues, directement ou par un dossier ancêtre",
                "example": 1
              },
              "unstamped": {
                "type": "integer",
                "description": "Ressources ni estampillées ni exclues — celles qu'une repropagation\nrendrait publiques. Un nombre non nul signale une propagation en\nretard, pas une erreur.\n",
                "example": 1
              },
              "unstamped_items": {
                "type": "array",
                "description": "Échantillon des ressources non estampillées, borné par `limit`\n(défaut 50, maximum 500). `unstamped` reste le décompte exact.\n",
                "items": {
                  "$ref": "#/components/schemas/PublicAccessResourceRef"
                }
              }
            }
          }
        }
      },
      "PublicAccessResourceRef": {
        "type": "object",
        "nullable": true,
        "description": "Référence courte vers une ressource estampillable (dossier ou document).\n\nLe schéma est déclaré `nullable` parce que les champs `root` et\n`excluded_by` de `PublicAccessState` valent `null` quand aucune racine\nn'explique l'accès et qu'aucun ancêtre n'est exclu — le cas courant. Là où\nla référence est obligatoire (`resource`, éléments de `unstamped_items`),\nelle n'est jamais nulle : `nullable` y est un sur-ensemble inoffensif, et\nc'est le prix d'un OAS 3.0 qui ne sait pas rendre un `$ref` nullable au\npoint d'usage.\n",
        "required": [
          "id",
          "type"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la ressource",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed886"
          },
          "type": {
            "type": "string",
            "enum": [
              "folder",
              "file"
            ],
            "description": "Nature de la ressource",
            "example": "folder"
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Libellé de la ressource",
            "example": "Rapports publics"
          },
          "path": {
            "type": "string",
            "nullable": true,
            "description": "Chemin ltree ABSOLU de la ressource, construit sur des identifiants\n(`ltree_id`) et non sur des noms — un renommage ne le change pas.\n",
            "example": "019951a3.019951a4.019951a5"
          }
        }
      },
      "WorkspaceMutation": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "description": "Libellé de l'espace de travail",
            "example": "Project Alpha"
          },
          "description": {
            "type": "string",
            "description": "Description de l'espace de travail",
            "example": "Main project workspace"
          },
          "classification_plan_id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du plan de classement associé",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed886"
          },
          "classification_plan_csv": {
            "type": "string",
            "description": "Données CSV du plan de classement"
          },
          "classification_plan_label": {
            "type": "string",
            "description": "Libellé du plan de classement",
            "example": "Standard Plan"
          },
          "domain_id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du domaine associé",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed886"
          },
          "settings": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "Code de l'espace de travail",
                "example": "PROJ-001"
              },
              "start_date": {
                "type": "string",
                "format": "date",
                "description": "Date de début du projet",
                "example": "2023-01-01"
              },
              "color": {
                "type": "string",
                "description": "Couleur de l'espace de travail",
                "example": "#3498db"
              },
              "icon": {
                "type": "string",
                "description": "Icône de l'espace de travail",
                "example": "folder"
              },
              "address": {
                "type": "string",
                "description": "Adresse du projet",
                "example": "123 Main St"
              },
              "postal_code": {
                "type": "string",
                "description": "Code postal du projet",
                "example": "12345"
              },
              "city": {
                "type": "string",
                "description": "Ville du projet",
                "example": "Paris"
              },
              "agency": {
                "type": "string",
                "description": "Agence du projet",
                "example": "Construction Agency"
              },
              "project_type": {
                "type": "string",
                "description": "Type de projet",
                "example": "Construction"
              },
              "status": {
                "type": "string",
                "description": "Statut du projet",
                "example": "active"
              },
              "end_date": {
                "type": "string",
                "format": "date",
                "description": "Date de fin du projet",
                "example": "2023-12-31"
              },
              "validation": {
                "type": "object",
                "properties": {
                  "default_deadline_days": {
                    "type": "integer",
                    "description": "Délai par défaut en jours",
                    "example": 30
                  },
                  "auto_reminder_days": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "Rappel automatique en jours",
                    "example": [
                      7,
                      14
                    ]
                  },
                  "super_validator_ids": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "Identifiants des super validateurs",
                    "example": [
                      "019951a3-01b7-7eb9-88bb-f872a01ed886",
                      "019951a3-01b7-7eb9-88bb-f872a01ed887"
                    ]
                  }
                }
              }
            }
          },
          "metadata_user": {
            "type": "object",
            "description": "Métadonnées définies par l'utilisateur"
          }
        }
      },
      "FolderMutation": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Nom du dossier",
            "example": "Documents"
          },
          "description": {
            "type": "string",
            "description": "Description du dossier",
            "example": "Main documents folder"
          },
          "color": {
            "type": "string",
            "description": "Couleur du dossier",
            "example": "#e74c3c"
          },
          "icon": {
            "type": "string",
            "description": "Icône du dossier",
            "example": "folder-open"
          },
          "documents_folder_id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du dossier parent",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed887"
          },
          "workspace_id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de l'espace de travail associé",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed888"
          }
        }
      },
      "DocumentMutation": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Nom du fichier",
            "example": "document.pdf"
          },
          "description": {
            "type": "string",
            "description": "Description du fichier",
            "example": "Important document"
          },
          "uploaded_file": {
            "type": "string",
            "format": "binary",
            "description": "Fichier à télécharger"
          },
          "documents_folder_id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du dossier parent",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed887"
          },
          "workspace_id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de l'espace de travail associé",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed888"
          },
          "domain_id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du domaine associé",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed889"
          },
          "document_type_id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du type de document associé",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed890"
          },
          "metadata_user": {
            "type": "object",
            "description": "Métadonnées définies par l'utilisateur"
          },
          "auto_classify": {
            "type": "boolean",
            "description": "Active la classification automatique du document",
            "default": false,
            "example": true
          }
        }
      },
      "BulkUploadMutation": {
        "type": "object",
        "properties": {
          "total_files": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Nombre total de fichiers qui seront uploadés"
          },
          "documents_folder_id": {
            "type": "string",
            "format": "uuid",
            "description": "ID du dossier de destination"
          },
          "workspace_id": {
            "type": "string",
            "format": "uuid",
            "description": "ID du workspace"
          },
          "auto_classify": {
            "type": "boolean",
            "default": false,
            "description": "Activer la classification automatique des documents"
          },
          "source": {
            "type": "string",
            "description": "Source de l'upload (sera auto-détecté depuis OAuth si non fourni)",
            "example": "hubdoc-tools"
          },
          "merge_to_pdf": {
            "type": "boolean",
            "default": false,
            "description": "Fusionner toutes les sources du lot en un seul document PDF.\nChaque fichier doit alors être envoyé avec une `position` explicite ;\nla fusion démarre quand `total_files` sources ont été reçues.\nFormats acceptés : PDF, JPEG, PNG (les images deviennent une page).\n"
          },
          "merged_file_name": {
            "type": "string",
            "description": "Nom du document PDF fusionné (requis si merge_to_pdf est vrai, l'extension .pdf est ajoutée si absente)",
            "example": "dossier_complet"
          }
        },
        "required": [
          "total_files"
        ]
      },
      "ChunkedUploadMutation": {
        "type": "object",
        "description": "Paramètres pour créer une session d'upload par chunks",
        "properties": {
          "filename": {
            "type": "string",
            "description": "Nom du fichier à uploader",
            "example": "document.pdf"
          },
          "file_size": {
            "type": "integer",
            "format": "int64",
            "description": "Taille totale du fichier en octets",
            "minimum": 1,
            "maximum": 5368709120,
            "example": 52428800
          },
          "content_type": {
            "type": "string",
            "description": "Type MIME du fichier",
            "example": "application/pdf"
          },
          "chunk_size": {
            "type": "integer",
            "description": "Taille de chaque chunk en octets (optionnel, défaut 5MB)",
            "default": 5242880,
            "minimum": 1048576,
            "maximum": 104857600,
            "example": 5242880
          },
          "workspace_id": {
            "type": "string",
            "format": "uuid",
            "description": "ID du workspace de destination (optionnel)",
            "nullable": true
          },
          "documents_folder_id": {
            "type": "string",
            "format": "uuid",
            "description": "ID du dossier de destination (optionnel)",
            "nullable": true
          },
          "metadata": {
            "type": "object",
            "description": "Métadonnées additionnelles (optionnel)",
            "additionalProperties": true,
            "nullable": true
          }
        },
        "required": [
          "filename",
          "file_size",
          "content_type"
        ]
      },
      "ChunkedUploadSessionResponse": {
        "type": "object",
        "description": "Réponse de création de session d'upload par chunks",
        "properties": {
          "upload_id": {
            "type": "string",
            "description": "Identifiant unique de la session d'upload",
            "example": "a1b2c3d4e5f6"
          },
          "chunk_size": {
            "type": "integer",
            "description": "Taille de chaque chunk en octets",
            "example": 5242880
          },
          "total_chunks": {
            "type": "integer",
            "description": "Nombre total de chunks à uploader",
            "example": 10
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Date d'expiration de la session (24h)",
            "example": "2024-11-10T10:00:00Z"
          }
        },
        "required": [
          "upload_id",
          "chunk_size",
          "total_chunks",
          "expires_at"
        ]
      },
      "ChunkedUploadChunkResponse": {
        "type": "object",
        "description": "Réponse d'upload d'un chunk",
        "properties": {
          "chunk_number": {
            "type": "integer",
            "description": "Numéro du chunk uploadé",
            "example": 5
          },
          "upload_id": {
            "type": "string",
            "description": "Identifiant de la session d'upload",
            "example": "a1b2c3d4e5f6"
          },
          "progress": {
            "type": "number",
            "format": "float",
            "description": "Pourcentage de progression (0-100)",
            "example": 50
          },
          "uploaded_chunks": {
            "type": "integer",
            "description": "Nombre de chunks uploadés",
            "example": 5
          },
          "total_chunks": {
            "type": "integer",
            "description": "Nombre total de chunks",
            "example": 10
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing"
            ],
            "description": "Statut actuel de l'upload",
            "example": "processing"
          }
        },
        "required": [
          "chunk_number",
          "upload_id",
          "progress",
          "uploaded_chunks",
          "total_chunks",
          "status"
        ]
      },
      "ChunkedUploadCompleteRequest": {
        "type": "object",
        "description": "Paramètres pour finaliser un upload par chunks",
        "properties": {
          "checksum": {
            "type": "string",
            "description": "Checksum MD5 du fichier complet en base64 (optionnel mais recommandé)",
            "example": "1B2M2Y8AsgTpgAmY7PhCfg==",
            "nullable": true
          }
        }
      },
      "ChunkedUploadCompleteResponse": {
        "type": "object",
        "description": "Réponse de finalisation d'upload par chunks",
        "properties": {
          "upload_id": {
            "type": "string",
            "description": "Identifiant de la session d'upload",
            "example": "a1b2c3d4e5f6"
          },
          "status": {
            "type": "string",
            "enum": [
              "completed"
            ],
            "description": "Statut de l'upload (toujours \"completed\" en cas de succès)",
            "example": "completed"
          },
          "assembled_file_size": {
            "type": "integer",
            "format": "int64",
            "description": "Taille du fichier assemblé en octets",
            "example": 52428800
          },
          "object_key": {
            "type": "string",
            "description": "Clé de l'objet dans le storage",
            "example": "uploads/abc123/document.pdf"
          }
        },
        "required": [
          "upload_id",
          "status",
          "object_key"
        ]
      },
      "ChunkedUploadStatusResponse": {
        "type": "object",
        "description": "Réponse du statut d'upload par chunks",
        "properties": {
          "upload_id": {
            "type": "string",
            "description": "Identifiant de la session d'upload",
            "example": "a1b2c3d4e5f6"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed",
              "cancelled",
              "expired"
            ],
            "description": "Statut actuel de l'upload",
            "example": "processing"
          },
          "progress": {
            "type": "number",
            "format": "float",
            "description": "Pourcentage de progression (0-100)",
            "example": 50
          },
          "uploaded_chunks": {
            "type": "integer",
            "description": "Nombre de chunks uploadés",
            "example": 5
          },
          "total_chunks": {
            "type": "integer",
            "description": "Nombre total de chunks",
            "example": 10
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Date d'expiration de la session",
            "example": "2024-11-10T10:00:00Z"
          },
          "expired": {
            "type": "boolean",
            "description": "Indique si la session est expirée",
            "example": false
          }
        },
        "required": [
          "upload_id",
          "status",
          "progress",
          "uploaded_chunks",
          "total_chunks",
          "expires_at",
          "expired"
        ]
      },
      "ContactMutation": {
        "type": "object",
        "required": [
          "email_address"
        ],
        "properties": {
          "email_address": {
            "type": "string",
            "format": "email",
            "description": "Adresse email du contact",
            "example": "contact@example.com"
          },
          "first_name": {
            "type": "string",
            "description": "Prénom du contact",
            "example": "John"
          },
          "last_name": {
            "type": "string",
            "description": "Nom du contact",
            "example": "Doe"
          },
          "external_id": {
            "type": "string",
            "description": "Identifiant externe pour l'intégration avec des systèmes tiers",
            "example": "ext-12345"
          }
        }
      },
      "GroupMutation": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Nom du groupe",
            "example": "Administrators"
          },
          "description": {
            "type": "string",
            "description": "Description du groupe",
            "example": "Group for system administrators"
          },
          "external_id": {
            "type": "string",
            "description": "Identifiant externe pour l'intégration avec des systèmes tiers",
            "example": "ext-admin-group"
          }
        }
      },
      "GroupMemberMutation": {
        "type": "object",
        "required": [
          "user_external_id"
        ],
        "properties": {
          "user_external_id": {
            "type": "string",
            "description": "Identifiant externe de l'utilisateur à ajouter au groupe",
            "example": "ext-user-123"
          },
          "role": {
            "type": "string",
            "description": "Rôle à attribuer au membre (par défaut \"member\")",
            "example": "member",
            "enum": [
              "member",
              "admin",
              "owner"
            ],
            "default": "member"
          }
        }
      },
      "PermissionMutation": {
        "type": "object",
        "required": [
          "level",
          "actor_id",
          "actor_type",
          "permissible_id",
          "permissible_type"
        ],
        "properties": {
          "level": {
            "type": "string",
            "enum": [
              "read",
              "write",
              "admin"
            ],
            "description": "Niveau de permission (read, write, admin)",
            "example": "read"
          },
          "actor_id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de l'acteur (User, Contact, Group)",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed887"
          },
          "actor_type": {
            "type": "string",
            "description": "Type d'acteur (User, Contact, Group)",
            "example": "User"
          },
          "permissible_id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la ressource protégée",
            "example": "019951a3-01b7-7eb9-88bb-f872a01ed888"
          },
          "permissible_type": {
            "type": "string",
            "description": "Type de ressource protégée (Documents::File, Documents::Folder, etc.)",
            "example": "Documents::File"
          },
          "external_id": {
            "type": "string",
            "description": "Identifiant externe pour l'intégration avec des systèmes tiers",
            "example": "ext-perm-12345"
          }
        }
      },
      "UserMutation": {
        "type": "object",
        "required": [
          "email_address"
        ],
        "properties": {
          "email_address": {
            "type": "string",
            "format": "email",
            "description": "Adresse email de l'utilisateur",
            "example": "john.doe@example.com"
          },
          "password": {
            "type": "string",
            "format": "password",
            "minLength": 6,
            "description": "Mot de passe de l'utilisateur (minimum 6 caractères)",
            "example": "securePassword123"
          },
          "first_name": {
            "type": "string",
            "maxLength": 50,
            "description": "Prénom de l'utilisateur",
            "example": "John"
          },
          "last_name": {
            "type": "string",
            "maxLength": 50,
            "description": "Nom de famille de l'utilisateur",
            "example": "Doe"
          },
          "role": {
            "type": "string",
            "enum": [
              "user",
              "manager",
              "admin",
              "super_admin"
            ],
            "description": "Rôle de l'utilisateur",
            "example": "user",
            "default": "user"
          },
          "external_id": {
            "type": "string",
            "nullable": true,
            "description": "Identifiant externe pour l'intégration avec des systèmes tiers",
            "example": "ext-user-12345"
          }
        }
      },
      "ComposedDocumentMutation": {
        "type": "object",
        "properties": {
          "template_key": {
            "type": "string",
            "description": "(Création seulement) Slug d'un DocumentTemplate. Le composed\ndocument hérite du typst_source, variables_schema et default_parts\ndu template. La création utilise l'interactor\n`Documents::ComposedDocuments::CreateFromTemplate`.\n",
            "example": "facture-edf"
          },
          "name": {
            "type": "string",
            "description": "Nom du document (par défaut, nom du template)",
            "example": "Facture Sinoia 2026-05"
          },
          "description": {
            "type": "string"
          },
          "variables": {
            "type": "object",
            "additionalProperties": true,
            "description": "Valeurs des variables du template",
            "example": {
              "client": "Sinoia",
              "montant": 1500
            }
          },
          "page_settings": {
            "type": "object",
            "additionalProperties": true
          },
          "typst_source": {
            "type": "string",
            "description": "(Update) Source Typst complète. Combiné à `assembly_mode: pure_typst`\npour piloter le rendu sans injection (#463). Côté CLI : `set-typst`.\n"
          },
          "assembly_mode": {
            "type": "string",
            "enum": [
              "composed",
              "pure_typst"
            ],
            "description": "(Update) Bascule le mode d'assemblage (#463). Un mode invalide est\nrejeté en 422.\n"
          },
          "workspace_id": {
            "type": "string",
            "format": "uuid"
          },
          "folder_id": {
            "type": "string",
            "format": "uuid"
          },
          "metadata_user": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "PartMutation": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "content": {
            "type": "string"
          },
          "content_format": {
            "type": "string",
            "enum": [
              "typst",
              "markdown"
            ]
          },
          "content_source": {
            "type": "string",
            "enum": [
              "human",
              "agent",
              "template"
            ]
          },
          "part_type": {
            "type": "string",
            "enum": [
              "content",
              "header",
              "footer",
              "signature",
              "appendix"
            ]
          },
          "position": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "editing",
              "ready",
              "validated",
              "locked"
            ]
          },
          "alignment": {
            "type": "string",
            "enum": [
              "left",
              "center",
              "right"
            ]
          },
          "bsize": {
            "type": "integer",
            "minimum": 1,
            "maximum": 12
          },
          "page_break": {
            "type": "boolean"
          },
          "row_group": {
            "type": "integer"
          },
          "key": {
            "type": "string"
          },
          "instructions": {
            "type": "string"
          },
          "assignee_id": {
            "type": "string",
            "format": "uuid"
          },
          "assignee_type": {
            "type": "string"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "DocumentTemplateMutation": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "Slug unique du template",
            "example": "facture-edf"
          },
          "name": {
            "type": "string",
            "description": "Nom affiché du template",
            "example": "Facture EDF"
          },
          "description": {
            "type": "string",
            "description": "Description longue"
          },
          "category": {
            "type": "string",
            "description": "Catégorie fonctionnelle",
            "example": "billing"
          },
          "locale": {
            "type": "string",
            "default": "fr",
            "example": "fr"
          },
          "typst_source": {
            "type": "string",
            "description": "Source Typst du template (placeholders {{var}} supportés)"
          },
          "variables_schema": {
            "type": "object",
            "additionalProperties": true,
            "description": "JSON Schema simplifié décrivant les variables attendues"
          },
          "default_parts": {
            "type": "array",
            "description": "Parts par défaut du template (JSONB array d'objets)",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "page_settings": {
            "type": "object",
            "additionalProperties": true,
            "description": "Configuration page"
          },
          "active": {
            "type": "boolean",
            "description": "Le template est-il actif",
            "default": true
          }
        }
      },
      "MassCommunication": {
        "type": "object",
        "description": "Communication de masse (email broadcast)",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant unique",
            "example": "550e8400-e29b-41d4-a716-446655440000"
          },
          "name": {
            "type": "string",
            "description": "Nom de la communication",
            "example": "Newsletter Janvier 2024"
          },
          "subject": {
            "type": "string",
            "description": "Sujet de l'email",
            "example": "Actualités du mois"
          },
          "body": {
            "type": "string",
            "description": "Corps du message (supporte les variables Liquid)",
            "example": "Bonjour {{ recipient.first_name }},\n\nVoici les actualités..."
          },
          "communication_type": {
            "type": "string",
            "description": "Type de communication",
            "enum": [
              "email",
              "sms"
            ],
            "example": "email"
          },
          "status": {
            "type": "string",
            "description": "Statut de la communication",
            "enum": [
              "draft",
              "sending",
              "sent",
              "failed",
              "cancelled"
            ],
            "example": "draft"
          },
          "template_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "ID du template utilisé"
          },
          "settings": {
            "type": "object",
            "description": "Paramètres additionnels",
            "additionalProperties": true
          },
          "attribute_definitions": {
            "type": "array",
            "description": "Définitions des attributs personnalisés pour les destinataires",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                },
                "type": {
                  "type": "string"
                },
                "required": {
                  "type": "boolean"
                },
                "placeholder": {
                  "type": "string"
                }
              }
            }
          },
          "recipients_count": {
            "type": "integer",
            "description": "Nombre total de destinataires",
            "example": 150
          },
          "sent_count": {
            "type": "integer",
            "description": "Nombre de messages envoyés avec succès",
            "example": 148
          },
          "failed_count": {
            "type": "integer",
            "description": "Nombre d'envois échoués",
            "example": 2
          },
          "created_by_id": {
            "type": "string",
            "format": "uuid",
            "description": "ID de l'utilisateur créateur"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Date de création",
            "example": "2024-01-16T10:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Date de dernière modification"
          },
          "sent_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Date d'envoi",
            "example": "2024-01-16T10:05:00Z"
          }
        }
      },
      "MassCommunicationMutation": {
        "type": "object",
        "description": "Données pour créer une communication de masse",
        "required": [
          "mass_communication",
          "recipients"
        ],
        "properties": {
          "mass_communication": {
            "type": "object",
            "required": [
              "name"
            ],
            "properties": {
              "name": {
                "type": "string",
                "description": "Nom de la communication",
                "example": "Newsletter Janvier 2024"
              },
              "subject": {
                "type": "string",
                "description": "Sujet de l'email (requis pour l'envoi)",
                "example": "Actualités du mois"
              },
              "body": {
                "type": "string",
                "description": "Corps du message (supporte les variables Liquid)",
                "example": "Bonjour {{ recipient.first_name }},\n\nVoici les actualités..."
              },
              "communication_type": {
                "type": "string",
                "description": "Type de communication",
                "enum": [
                  "email",
                  "sms"
                ],
                "default": "email"
              },
              "template_id": {
                "type": "string",
                "format": "uuid",
                "description": "ID du template à utiliser"
              },
              "settings": {
                "type": "object",
                "description": "Paramètres additionnels",
                "additionalProperties": true
              },
              "attribute_definitions": {
                "type": "array",
                "description": "Définitions des attributs personnalisés",
                "items": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "type": {
                      "type": "string"
                    },
                    "required": {
                      "type": "boolean"
                    },
                    "placeholder": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "recipients": {
            "type": "array",
            "description": "Liste des destinataires",
            "minItems": 1,
            "items": {
              "type": "object",
              "required": [
                "email"
              ],
              "properties": {
                "email": {
                  "type": "string",
                  "format": "email",
                  "description": "Adresse email du destinataire",
                  "example": "john@example.com"
                },
                "first_name": {
                  "type": "string",
                  "description": "Prénom",
                  "example": "John"
                },
                "last_name": {
                  "type": "string",
                  "description": "Nom",
                  "example": "Doe"
                },
                "external_id": {
                  "type": "string",
                  "description": "Identifiant externe"
                },
                "custom_attributes": {
                  "type": "object",
                  "description": "Attributs personnalisés",
                  "additionalProperties": true
                }
              }
            }
          },
          "attachments": {
            "type": "array",
            "description": "Pièces jointes",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "description": "Nom du fichier"
                },
                "scope_type": {
                  "type": "string",
                  "description": "Portée de la pièce jointe",
                  "enum": [
                    "all",
                    "actor",
                    "group"
                  ],
                  "default": "all"
                },
                "actor_id": {
                  "type": "string",
                  "format": "uuid",
                  "description": "ID de l'acteur (si scope_type=actor)"
                },
                "group_id": {
                  "type": "string",
                  "format": "uuid",
                  "description": "ID du groupe (si scope_type=group)"
                }
              }
            }
          },
          "send": {
            "type": "boolean",
            "description": "Envoyer immédiatement après création",
            "default": false
          }
        }
      },
      "MassCommunicationResponse": {
        "type": "object",
        "description": "Réponse contenant une communication de masse avec ses relations",
        "properties": {
          "mass_communication": {
            "$ref": "#/components/schemas/MassCommunication"
          },
          "recipients": {
            "type": "array",
            "description": "Liste des destinataires",
            "items": {
              "$ref": "#/components/schemas/MassCommunicationRecipient"
            }
          },
          "attachments": {
            "type": "array",
            "description": "Liste des pièces jointes",
            "items": {
              "$ref": "#/components/schemas/MassCommunicationAttachment"
            }
          },
          "skipped_recipients": {
            "type": "array",
            "description": "Destinataires ignorés lors de la création",
            "items": {
              "type": "object",
              "properties": {
                "email": {
                  "type": "string",
                  "description": "Email du destinataire ignoré"
                },
                "reason": {
                  "type": "string",
                  "description": "Code de la raison",
                  "enum": [
                    "invalid_email_format",
                    "duplicate_email",
                    "missing_required_field"
                  ]
                },
                "message": {
                  "type": "string",
                  "description": "Message explicatif"
                }
              }
            }
          },
          "sending": {
            "type": "boolean",
            "description": "Indique si l'envoi est en cours",
            "example": false
          },
          "enqueued_count": {
            "type": "integer",
            "description": "Nombre de messages mis en file d'attente pour envoi",
            "example": 0
          }
        }
      },
      "MassCommunicationRecipient": {
        "type": "object",
        "description": "Destinataire d'une communication de masse",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant unique",
            "example": "660e8400-e29b-41d4-a716-446655440001"
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Adresse email",
            "example": "john@example.com"
          },
          "first_name": {
            "type": "string",
            "description": "Prénom",
            "example": "John"
          },
          "last_name": {
            "type": "string",
            "description": "Nom",
            "example": "Doe"
          },
          "external_id": {
            "type": "string",
            "nullable": true,
            "description": "Identifiant externe"
          },
          "status": {
            "type": "string",
            "description": "Statut de l'envoi",
            "enum": [
              "pending",
              "sent",
              "failed",
              "bounced",
              "opened",
              "clicked"
            ],
            "example": "pending"
          },
          "custom_attributes": {
            "type": "object",
            "description": "Attributs personnalisés",
            "additionalProperties": true
          },
          "actor": {
            "type": "object",
            "nullable": true,
            "description": "Acteur lié (si le destinataire est un utilisateur existant)",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "type": {
                "type": "string"
              }
            }
          },
          "group": {
            "type": "object",
            "nullable": true,
            "description": "Groupe lié (si le destinataire fait partie d'un groupe)",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "sent_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Date d'envoi"
          },
          "opened_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Date d'ouverture"
          },
          "error_message": {
            "type": "string",
            "nullable": true,
            "description": "Message d'erreur en cas d'échec"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Date de création"
          }
        }
      },
      "MassCommunicationAttachment": {
        "type": "object",
        "description": "Pièce jointe d'une communication de masse",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant unique",
            "example": "770e8400-e29b-41d4-a716-446655440002"
          },
          "name": {
            "type": "string",
            "description": "Nom du fichier",
            "example": "document.pdf"
          },
          "scope_type": {
            "type": "string",
            "description": "Portée de la pièce jointe",
            "enum": [
              "all",
              "actor",
              "group"
            ],
            "example": "all"
          },
          "actor_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "ID de l'acteur (si scope_type=actor)"
          },
          "group_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "ID du groupe (si scope_type=group)"
          },
          "file_url": {
            "type": "string",
            "format": "uri",
            "description": "URL de téléchargement du fichier"
          },
          "content_type": {
            "type": "string",
            "description": "Type MIME du fichier",
            "example": "application/pdf"
          },
          "file_size": {
            "type": "integer",
            "description": "Taille du fichier en octets",
            "example": 102400
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Date de création"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Type d'erreur",
            "example": "validation_error"
          },
          "message": {
            "type": "string",
            "description": "Message d'erreur lisible",
            "example": "The request contains invalid parameters"
          },
          "details": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Détails supplémentaires de l'erreur",
            "example": [
              "Name is required",
              "Workspace ID must be a valid integer"
            ]
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Requête incorrecte - paramètres invalides",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "bad_request",
              "message": "Paramètres de requête invalides"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Authentification requise",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "unauthorized",
              "message": "Authentification requise"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Accès refusé - permissions insuffisantes",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "forbidden",
              "message": "Accès refusé"
            }
          }
        }
      },
      "NotFound": {
        "description": "Ressource introuvable",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "not_found",
              "message": "Ressource introuvable"
            }
          }
        }
      },
      "UnprocessableEntity": {
        "description": "Erreurs de validation",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "validation_error",
              "message": "La validation a échoué",
              "details": [
                "Le nom est requis",
                "L'identifiant d'espace de travail doit être valide"
              ]
            }
          }
        }
      },
      "InternalError": {
        "description": "Erreur interne du serveur",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "internal_error",
              "message": "Une erreur inattendue s'est produite"
            }
          }
        }
      }
    },
    "securitySchemes": {
      "OAuth2Password": {
        "type": "oauth2",
        "description": "Flow password pour applications de confiance (mobile, CLI, tests)",
        "flows": {
          "password": {
            "tokenUrl": "/oauth/token",
            "scopes": {
              "read": "Lecture des ressources",
              "write": "Modification des ressources",
              "admin": "Accès administratif"
            }
          }
        }
      },
      "OAuth2AuthCode": {
        "type": "oauth2",
        "description": "Flow authorization code pour applications frontend/tierce",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "/oauth/authorize",
            "tokenUrl": "/oauth/token",
            "scopes": {
              "read": "Lecture des ressources",
              "write": "Modification des ressources",
              "admin": "Accès administratif"
            }
          }
        }
      }
    }
  },
  "x-tagGroups": [
    {
      "name": "GED",
      "tags": [
        "Workspaces",
        "Folders",
        "Documents"
      ]
    },
    {
      "name": "Éditique",
      "tags": [
        "Composed Documents",
        "Document Templates"
      ]
    },
    {
      "name": "Uploads",
      "tags": [
        "BulkUploads",
        "ChunkedUploads"
      ]
    },
    {
      "name": "Annuaire & accès",
      "tags": [
        "Users",
        "Contacts",
        "Groups",
        "Group Members",
        "Permissions",
        "Public Access"
      ]
    },
    {
      "name": "Communications",
      "tags": [
        "Mass Communications"
      ]
    }
  ]
}