{
  "openapi": "3.1.0",
  "info": {
    "title": "API Trame v1",
    "version": "1.0.0",
    "description": "⚠️ GÉNÉRÉ par scripts/_gen-api-docs.mjs — ne pas éditer à la main. API publique de Trame : conversations, envoi, brouillons, contacts, agenda, tâches, analytics et point d’entrée MCP. Authentification par clé API (en-tête X-Api-Key), isolation par espace (tenant), scopes explicites par route."
  },
  "servers": [
    {
      "url": "https://{base}/public",
      "description": "Base de l’API de votre espace Trame, fournie à l’activation.",
      "variables": {
        "base": {
          "default": "<base-api>",
          "description": "domaine de votre espace Trame."
        }
      }
    }
  ],
  "tags": [
    {
      "name": "Conversations"
    },
    {
      "name": "Envoi de message"
    },
    {
      "name": "Brouillons"
    },
    {
      "name": "Contacts"
    },
    {
      "name": "Agenda"
    },
    {
      "name": "Tâches"
    },
    {
      "name": "Signatures et analytics"
    },
    {
      "name": "Webhooks"
    },
    {
      "name": "MCP"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "paths": {
    "/v1/conversations": {
      "get": {
        "operationId": "listConversations",
        "summary": "Liste les conversations de la boîte demandée, de la plus récente à la plus ancienne.",
        "tags": [
          "Conversations"
        ],
        "description": "Liste les conversations de la boîte demandée, de la plus récente à la plus ancienne.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "read:conversations",
        "parameters": [
          {
            "name": "mailbox",
            "in": "query",
            "required": true,
            "description": "boîte ciblée, obligatoirement accessible à la clé.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "1 à 100, défaut 25.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "pageToken",
            "in": "query",
            "required": false,
            "description": "jeton de pagination renvoyé par l’appel précédent.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"mailbox\", \"conversations\": [...], \"nextPageToken\": string|null }"
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conversations/{id}": {
      "get": {
        "operationId": "getConversation",
        "summary": "Détail d’une conversation (fil complet avec ses messages).",
        "tags": [
          "Conversations"
        ],
        "description": "Détail d’une conversation (fil complet avec ses messages).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "read:conversations",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "identifiant de la ressource.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mailbox",
            "in": "query",
            "required": true,
            "description": "boîte ciblée, obligatoirement accessible à la clé.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"mailbox\", \"conversation\": { \"id\", \"subject\", \"messages\": [...] } }"
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "conversation introuvable ou hors de la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conversations/{id}/assign": {
      "post": {
        "operationId": "assignConversation",
        "summary": "Assigne la conversation à une personne, avec un statut et une équipe optionnels.",
        "tags": [
          "Conversations"
        ],
        "description": "Assigne la conversation à une personne, avec un statut et une équipe optionnels.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "write:conversations",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "identifiant de la ressource.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mailbox": {
                    "type": "string",
                    "description": "boîte ciblée, obligatoirement accessible à la clé."
                  },
                  "assignee": {
                    "type": "string",
                    "description": "adresse de la personne assignée, ou `null` pour désassigner."
                  },
                  "status": {
                    "type": "string",
                    "description": "statut applicatif à poser sur le fil."
                  },
                  "teamId": {
                    "type": "string",
                    "description": "identifiant d’équipe destinataire."
                  }
                },
                "required": [
                  "mailbox"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ \"ok\": true, \"id\", \"result\" }"
          },
          "202": {
            "description": "{ \"approvalRequired\": true, \"approval\": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400)."
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conversations/{id}/comment": {
      "post": {
        "operationId": "commentConversation",
        "summary": "Ajoute un commentaire interne au fil — visible par l’équipe, jamais envoyé au correspondant.",
        "tags": [
          "Conversations"
        ],
        "description": "Ajoute un commentaire interne au fil — visible par l’équipe, jamais envoyé au correspondant.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "write:conversations",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "identifiant de la ressource.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "8 à 200 caractères imprimables ; un rejeu avec la même valeur **et la même charge** ne produit jamais un second effet et renvoie le premier résultat. Réutiliser la même valeur avec une charge différente est refusé `409`, jamais silencieusement accepté. Sur les routes d’envoi, une valeur qui se heurte à une réservation posée par une autre voie d’envoi de l’espace est également refusée `409`, avec un code distinct — voir la note « Portée exacte de la garantie d’idempotence » de la route.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mailbox": {
                    "type": "string",
                    "description": "boîte ciblée, obligatoirement accessible à la clé."
                  },
                  "text": {
                    "type": "string",
                    "description": "contenu du commentaire, 10 000 caractères maximum."
                  }
                },
                "required": [
                  "mailbox",
                  "text"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ \"ok\": true, \"id\", \"result\" }"
          },
          "202": {
            "description": "{ \"approvalRequired\": true, \"approval\": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400)."
          },
          "400": {
            "description": "text absent, trop long, ou en-tête Idempotency-Key manquant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conversations/{id}/labels": {
      "post": {
        "operationId": "labelConversation",
        "summary": "Ajoute et/ou retire des étiquettes sur le fil.",
        "tags": [
          "Conversations"
        ],
        "description": "Ajoute et/ou retire des étiquettes sur le fil.\n\nAu moins un des deux tableaux doit être non vide.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "write:conversations",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "identifiant de la ressource.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mailbox": {
                    "type": "string",
                    "description": "boîte ciblée, obligatoirement accessible à la clé."
                  },
                  "add": {
                    "type": "array",
                    "items": {},
                    "description": "étiquettes à ajouter (50 maximum par requête, 128 caractères chacune)."
                  },
                  "remove": {
                    "type": "array",
                    "items": {},
                    "description": "étiquettes à retirer (mêmes plafonds)."
                  }
                },
                "required": [
                  "mailbox"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ \"ok\": true, \"id\", \"result\" }"
          },
          "202": {
            "description": "{ \"approvalRequired\": true, \"approval\": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400)."
          },
          "400": {
            "description": "ni add ni remove, ou plafonds dépassés.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conversations/{id}/archive": {
      "post": {
        "operationId": "archiveConversation",
        "summary": "Archive la conversation : elle quitte la boîte de réception sans être supprimée.",
        "tags": [
          "Conversations"
        ],
        "description": "Archive la conversation : elle quitte la boîte de réception sans être supprimée.\n\nAction idempotente : archiver une conversation déjà archivée ne produit aucun effet supplémentaire.\n\nL’en-tête `Idempotency-Key` est facultatif pour une clé humaine (l’action est naturellement idempotente) mais **obligatoire** pour une clé de type `agent` à approbation requise, qui pose une demande de validation : sans lui, la réponse est `400`.\n\nAucun message n’est supprimé — la conversation reste consultable et retrouvable par la recherche.\n\nAucun événement sortant n’est publié pour cette action.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "write:conversations",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "identifiant de la ressource.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mailbox": {
                    "type": "string",
                    "description": "boîte ciblée, obligatoirement accessible à la clé."
                  }
                },
                "required": [
                  "mailbox"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ \"ok\": true, \"id\", \"result\": { \"action\": \"archive\", \"applied\": true } }"
          },
          "202": {
            "description": "{ \"approvalRequired\": true, \"approval\": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400)."
          },
          "400": {
            "description": "en-tête Idempotency-Key absent alors que la clé est de type agent à approbation requise.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "conversation introuvable dans la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conversations/{id}/reopen": {
      "post": {
        "operationId": "reopenConversation",
        "summary": "Remet la conversation dans la boîte de réception (opération inverse de l’archivage).",
        "tags": [
          "Conversations"
        ],
        "description": "Remet la conversation dans la boîte de réception (opération inverse de l’archivage).\n\nComme pour l’archivage, l’en-tête `Idempotency-Key` est facultatif pour une clé humaine et **obligatoire** pour une clé de type `agent` à approbation requise.\n\nLe nom `reopen` est conservé pour la parité avec les intégrations existantes ; dans l’interface, l’action s’appelle « remettre dans la boîte de réception ».\n\nCette action ne sort pas une conversation de la corbeille ni des indésirables : elle rétablit uniquement la boîte de réception.\n\nAucun événement sortant n’est publié pour cette action.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "write:conversations",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "identifiant de la ressource.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mailbox": {
                    "type": "string",
                    "description": "boîte ciblée, obligatoirement accessible à la clé."
                  }
                },
                "required": [
                  "mailbox"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ \"ok\": true, \"id\", \"result\": { \"action\": \"reopen\", \"applied\": true, \"inbox\": true } }"
          },
          "202": {
            "description": "{ \"approvalRequired\": true, \"approval\": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400)."
          },
          "400": {
            "description": "en-tête Idempotency-Key absent alors que la clé est de type agent à approbation requise.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "conversation introuvable dans la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conversations/{id}/snooze": {
      "post": {
        "operationId": "snoozeConversation",
        "summary": "Reporte la conversation : elle est mise de côté puis revient à l’échéance indiquée.",
        "tags": [
          "Conversations"
        ],
        "description": "Reporte la conversation : elle est mise de côté puis revient à l’échéance indiquée.\n\nLe report ne déplace ni ne supprime aucun message : la conversation est masquée des vues jusqu’à l’échéance, puis revient.\n\nUn report est **nominatif** : il est posé pour le membre auquel la clé est rattachée, comme lorsqu’il reporte depuis l’interface.\n\nReposer exactement le même report est sans effet (`\"changed\": false`) ; indiquer une autre échéance décale le report existant au lieu d’en créer un second.\n\nUne clé de type `agent` à approbation requise fait passer l’action par la file de validation : si l’échéance demandée est déjà dépassée au moment de la validation, l’opération est refusée plutôt que reportée à une date arbitraire.\n\nAucun événement sortant n’est publié pour cette action.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "write:conversations",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "identifiant de la ressource.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mailbox": {
                    "type": "string",
                    "description": "boîte ciblée, obligatoirement accessible à la clé."
                  },
                  "until": {
                    "type": "string",
                    "description": "échéance du report, date ISO 8601 avec fuseau explicite (`…Z` ou `±hh:mm`), strictement future et à 365 jours au maximum."
                  }
                },
                "required": [
                  "mailbox",
                  "until"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ \"ok\": true, \"id\", \"result\": { \"action\": \"snooze\", \"until\", \"reminderId\", \"created\", \"changed\" } }"
          },
          "202": {
            "description": "{ \"approvalRequired\": true, \"approval\": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400)."
          },
          "400": {
            "description": "until absent, sans fuseau, dans le passé, au-delà du plafond de 365 jours — ou en-tête Idempotency-Key absent alors que la clé est de type agent à approbation requise.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "la clé n’est rattachée à aucun membre identifié : le report est nominatif.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "le report a été modifié entre-temps par un autre appareil ou une autre intégration — relire puis réessayer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/search": {
      "get": {
        "operationId": "searchConversations",
        "summary": "Recherche dans les conversations d’une boîte, via le moteur de recherche de l’espace.",
        "tags": [
          "Conversations"
        ],
        "description": "Recherche dans les conversations d’une boîte, via le moteur de recherche de l’espace.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "read:conversations",
        "parameters": [
          {
            "name": "mailbox",
            "in": "query",
            "required": true,
            "description": "boîte ciblée, obligatoirement accessible à la clé.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "texte recherché, 500 caractères maximum.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "1 à 100, défaut 25.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"mailbox\", \"query\", \"results\": [...] }"
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conversations/{id}/send": {
      "post": {
        "operationId": "sendInThread",
        "summary": "Envoie une réponse dans un fil existant, au nom de la boîte indiquée.",
        "tags": [
          "Envoi de message"
        ],
        "description": "Envoie une réponse dans un fil existant, au nom de la boîte indiquée.\n\nLe fil ciblé vient de l’URL : le corps ne peut pas le contredire.\n\nChaque champ d’adresses (`to`, `cc`, `bcc`) accepte une chaîne (`\"a@x.fr, b@y.fr\"`), un tableau de chaînes, ou un tableau d’objets `{ \"email\", \"name\" }`. **Deux bornes** s’appliquent, avec les mêmes valeurs pour toutes ces formes : une par champ et une par MESSAGE (`to` + `cc` + `bcc` réunis) — `to` et `cc` peuvent donc être chacun sous la borne par champ et le message être refusé quand même. Un dépassement est refusé en `400`, jamais tronqué en silence. Voir « Plafonds des routes d’envoi ».\n\nUne adresse dont la **forme** n’est pas exploitable est refusée en `400` avant tout envoi : objet sans champ `email` (ou `address`), valeur qui n’est ni une chaîne ni un objet, ou entrée unique portant deux adresses (`Nom <a@x.fr> <b@y.fr>`). Aucune adresse n’est devinée ni convertie de force : la requête entière est refusée. Comme toutes les erreurs des routes d’envoi, la réponse porte une **phrase** explicative, pas un code machine — testez le statut `400`, pas le texte.\n\nPortée exacte de la garantie d’idempotence. La règle appliquée est : **le service ne rend jamais le reçu d’un message dont il ne peut pas prouver qu’il correspond à votre demande.** La preuve est une **empreinte de la charge utile**, enregistrée avec la réservation par les routes d’envoi de l’API. Trois issues, jamais deux : (a) empreinte identique → `200` + le reçu d’origine (`X-EPB-Idempotency: hit` ou `hit-mem`) ; (b) empreinte comparable mais différente → `409 IDEMPOTENCY_KEY_REUSED` (`X-EPB-Idempotency: key-reused`) ; (c) **rien de comparable** → `409 IDEMPOTENCY_KEY_UNVERIFIABLE` (`X-EPB-Idempotency: key-unverifiable`). Aucun message n’est envoyé dans les cas (b) et (c). Le cas (c) se produit quand votre valeur se heurte à une réservation posée par une **autre voie d’envoi de l’espace** — l’application elle-même partage le même verrou et la même formule de clé, sans empreinte — ou par une version antérieure de l’empreinte : rejouez alors avec une **nouvelle** valeur, votre message n’a jamais été envoyé. Contrepartie assumée : après un changement de l’algorithme d’empreinte, un rejeu par ailleurs légitime peut recevoir ce même `409` pendant la durée de vie des réservations ; repartir avec une nouvelle valeur peut alors produire un doublon — **visible**, plutôt qu’une perte silencieuse. Le code distinct des deux `409` rend ce cas diagnosticable. En pratique, utilisez une valeur unique par message (un identifiant aléatoire, jamais un compteur ni un identifiant métier partagé avec un autre système) : le cas (c) devient inatteignable.\n\nPlafond d’envoi : voir « Limites de fréquence ». Il est appliqué **par instance de service**, donc le plafond agrégé observé peut être supérieur à la valeur affichée ; ne vous en servez pas comme d’une garantie de débit maximal.\n\nCe qui garde l’envoi, précisément — (1) le scope `mail:send` est **strictement opt-in** : jamais pré-coché, jamais impliqué par un autre scope, accordé uniquement par un administrateur de l’espace ; (2) une clé de type `agent` est refusée en `403` sur toute route d’envoi, sans exception ; (3) un **interrupteur d’envoi global**, qui doit être ouvert pour que le moindre message parte — mais cet interrupteur est **le même que celui de l’application** : dès que les membres de l’espace peuvent envoyer du courrier depuis Trame, il est ouvert. Ne le comptez donc pas comme une barrière propre à l’API : les deux vraies portes côté intégration sont le scope et le type de la clé. Fermé, il fait répondre `403` avant tout appel sortant.\n\nClé de type `agent` : refus `403` sur toute route d’envoi, **quel que soit son mode d’approbation** et même si elle porte déjà `mail:send`. Deux barrières distinctes, à ne pas confondre : accorder `mail:send` à une clé `agent` est refusé en `400` à la création comme à l’édition des droits ; et si une clé de ce type porte malgré tout le scope (droit accordé avant cette règle, ou clé importée), l’envoi est refusé au moment de l’appel. L’envoi n’entre jamais dans la file d’approbation humaine : il n’y a donc aucune approbation capable de le débloquer. Pour un droit d’envoi, créez une clé de type humain.\n\nÉtat de déploiement (juillet 2026) : les routes d’envoi de l’API publique ne sont **pas encore actives en production** — le service qui sert l’API publique n’a pas été redéployé depuis leur ajout, elles répondent donc `404` comme une route inconnue. C’est un état conjoncturel, pas une garantie : au premier déploiement de ce service, elles s’ouvrent, gardées uniquement par le scope et le type de la clé décrits ci-dessus.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "mail:send",
        "x-trame-send-limits": {
          "maxAttachments": 50,
          "maxAttachmentBytes": 26214400,
          "maxRecipientsPerField": 500,
          "maxRecipientsTotal": 500,
          "maxAddressChars": 640
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "identifiant de la ressource.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "8 à 200 caractères imprimables ; un rejeu avec la même valeur **et la même charge** ne produit jamais un second effet et renvoie le premier résultat. Réutiliser la même valeur avec une charge différente est refusé `409`, jamais silencieusement accepté. Sur les routes d’envoi, une valeur qui se heurte à une réservation posée par une autre voie d’envoi de l’espace est également refusée `409`, avec un code distinct — voir la note « Portée exacte de la garantie d’idempotence » de la route.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mailbox": {
                    "type": "string",
                    "description": "boîte ciblée, obligatoirement accessible à la clé."
                  },
                  "to": {
                    "type": "array",
                    "items": {},
                    "description": "destinataires principaux."
                  },
                  "cc": {
                    "type": "array",
                    "items": {},
                    "description": "destinataires en copie."
                  },
                  "bcc": {
                    "type": "array",
                    "items": {},
                    "description": "destinataires en copie cachée."
                  },
                  "subject": {
                    "type": "string",
                    "description": "objet du message."
                  },
                  "body": {
                    "type": "string",
                    "description": "corps du message (HTML accepté)."
                  },
                  "inReplyTo": {
                    "type": "string",
                    "description": "identifiant du message auquel on répond."
                  },
                  "priority": {
                    "type": "string",
                    "description": "priorité déclarée du message."
                  },
                  "attachments": {
                    "type": "array",
                    "items": {},
                    "description": "pièces jointes. Chaque entrée est un objet `{ \"contentBase64\", \"filename\", \"mimeType\" }` : `contentBase64` est **obligatoire** et doit être du base64 canonique strict (alphabet standard, longueur multiple de 4, bits de bourrage nuls — sinon `400`) ; `filename` accepte l’alias `name` et `mimeType` l’alias `type`, tous deux facultatifs. Le type déclaré n’est **pas** repris tel quel : le serveur inspecte les premiers octets et impose le type réellement détecté. Plafonds : voir « Plafonds des routes d’envoi »."
                  },
                  "dlpWarningToken": {
                    "type": "string",
                    "description": "jeton à renvoyer pour confirmer un envoi malgré un avertissement de prévention de fuite de données."
                  }
                },
                "required": [
                  "mailbox",
                  "to",
                  "subject",
                  "body"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ \"id\", \"threadId\", \"accepted\", \"providerMessageId\", \"receiptSource\", \"dlpStatus\" }. dlpStatus vaut \"ok\", ou \"warning_required\" quand un avertissement de prévention de fuite de données a mordu **et a été acquitté** via dlpWarningToken : dans ce cas aussi, le message est bien parti. Le corps porte \"webhookDeferred\": true quand l’événement message.sent n’a pas pu être mis en file (le message est parti, la notification sortante est perdue). Un rejeu à l’identique renvoie le même corps, avec l’en-tête X-EPB-Idempotency: hit (rejeu reconnu par le verrou partagé) ou hit-mem (rejeu servi par le cache de l’instance qui a traité l’envoi initial). Le premier envoi ne porte pas cet en-tête, ou la valeur miss."
          },
          "400": {
            "description": "to ou subject manquant, en-tête Idempotency-Key absent / hors format (8 à 200 caractères imprimables), pièce jointe en base64 non canonique, adresse dont la forme n’est pas exploitable, ou liste d’adresses hors plafond — par champ (to/cc/bcc) **ou au total du message**.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "interrupteur d’envoi fermé, politique de prévention de fuite de données bloquante, clé sans droit d’écriture sur la boîte, ou clé de type agent (refus systématique sur toute route d’envoi, quel que soit son mode d’approbation et même si elle porte mail:send).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "quatre cas distincts, tous **sans aucun envoi** : (a) même Idempotency-Key rejouée avec une charge différente — corps { \"code\": \"IDEMPOTENCY_KEY_REUSED\", \"idempotencyKeyReused\": true } et en-tête X-EPB-Idempotency: key-reused ; (a bis) Idempotency-Key déjà enregistrée par une **autre voie d’envoi de l’espace** (ou par une version antérieure de l’empreinte de charge utile) : impossible de prouver que le reçu enregistré correspond à votre demande — corps { \"code\": \"IDEMPOTENCY_KEY_UNVERIFIABLE\", \"idempotencyKeyUnverifiable\": true } et en-tête X-EPB-Idempotency: key-unverifiable, à rejouer avec une **nouvelle** valeur ; voir la note sur la portée exacte de cette garantie ; (b) envoi déjà en cours pour cette clé — en-tête X-EPB-Idempotency: duplicate-suppressed, corps { \"duplicate\": true, \"outcomeUnknown\": bool } ; (c) avertissement de prévention de fuite de données à confirmer — { \"dlpStatus\": \"warning_required\", \"dlpWarningToken\" }, à renvoyer dans dlpWarningToken pour confirmer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "pièces jointes hors plafond (nombre ou volume cumulé) — refus avant tout appel sortant, aucun message n’est parti.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "le fournisseur n’a pas rendu de reçu exploitable : { \"error\": \"reçu d’envoi indisponible\", \"outcomeUnknown\": true }. **L’issue est ambiguë** — le message a pu partir. Ne pas réessayer avec une nouvelle Idempotency-Key (doublon garanti) : rejouer la MÊME clé, ou vérifier le fil avant toute relance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service d’envoi indisponible, verrou d’envoi occupé, ou autorisation d’écriture indisponible — réessayable. Un { \"outcomeUnknown\": true } dans le corps signale ici aussi une issue ambiguë (message parti, confirmation perdue).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/send": {
      "post": {
        "operationId": "sendMessage",
        "summary": "Envoie un nouveau message au nom de la boîte indiquée.",
        "tags": [
          "Envoi de message"
        ],
        "description": "Envoie un nouveau message au nom de la boîte indiquée.\n\nMêmes garde-fous que `POST /v1/conversations/{id}/send` (scope opt-in, refus des clés de type `agent`, interrupteur d’envoi partagé avec l’application, idempotence, plafond dédié appliqué par instance de service) et même état de déploiement.\n\nChaque champ d’adresses (`to`, `cc`, `bcc`) accepte une chaîne (`\"a@x.fr, b@y.fr\"`), un tableau de chaînes, ou un tableau d’objets `{ \"email\", \"name\" }`. **Deux bornes** s’appliquent, avec les mêmes valeurs pour toutes ces formes : une par champ et une par MESSAGE (`to` + `cc` + `bcc` réunis) — `to` et `cc` peuvent donc être chacun sous la borne par champ et le message être refusé quand même. Un dépassement est refusé en `400`, jamais tronqué en silence. Voir « Plafonds des routes d’envoi ».\n\nUne adresse dont la **forme** n’est pas exploitable est refusée en `400` avant tout envoi : objet sans champ `email` (ou `address`), valeur qui n’est ni une chaîne ni un objet, ou entrée unique portant deux adresses (`Nom <a@x.fr> <b@y.fr>`). Aucune adresse n’est devinée ni convertie de force : la requête entière est refusée. Comme toutes les erreurs des routes d’envoi, la réponse porte une **phrase** explicative, pas un code machine — testez le statut `400`, pas le texte.\n\nPortée exacte de la garantie d’idempotence. La règle appliquée est : **le service ne rend jamais le reçu d’un message dont il ne peut pas prouver qu’il correspond à votre demande.** La preuve est une **empreinte de la charge utile**, enregistrée avec la réservation par les routes d’envoi de l’API. Trois issues, jamais deux : (a) empreinte identique → `200` + le reçu d’origine (`X-EPB-Idempotency: hit` ou `hit-mem`) ; (b) empreinte comparable mais différente → `409 IDEMPOTENCY_KEY_REUSED` (`X-EPB-Idempotency: key-reused`) ; (c) **rien de comparable** → `409 IDEMPOTENCY_KEY_UNVERIFIABLE` (`X-EPB-Idempotency: key-unverifiable`). Aucun message n’est envoyé dans les cas (b) et (c). Le cas (c) se produit quand votre valeur se heurte à une réservation posée par une **autre voie d’envoi de l’espace** — l’application elle-même partage le même verrou et la même formule de clé, sans empreinte — ou par une version antérieure de l’empreinte : rejouez alors avec une **nouvelle** valeur, votre message n’a jamais été envoyé. Contrepartie assumée : après un changement de l’algorithme d’empreinte, un rejeu par ailleurs légitime peut recevoir ce même `409` pendant la durée de vie des réservations ; repartir avec une nouvelle valeur peut alors produire un doublon — **visible**, plutôt qu’une perte silencieuse. Le code distinct des deux `409` rend ce cas diagnosticable. En pratique, utilisez une valeur unique par message (un identifiant aléatoire, jamais un compteur ni un identifiant métier partagé avec un autre système) : le cas (c) devient inatteignable.\n\nClé de type `agent` : refus `403` sur toute route d’envoi, **quel que soit son mode d’approbation** et même si elle porte déjà `mail:send`. Deux barrières distinctes, à ne pas confondre : accorder `mail:send` à une clé `agent` est refusé en `400` à la création comme à l’édition des droits ; et si une clé de ce type porte malgré tout le scope (droit accordé avant cette règle, ou clé importée), l’envoi est refusé au moment de l’appel. L’envoi n’entre jamais dans la file d’approbation humaine : il n’y a donc aucune approbation capable de le débloquer. Pour un droit d’envoi, créez une clé de type humain.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "mail:send",
        "x-trame-send-limits": {
          "maxAttachments": 50,
          "maxAttachmentBytes": 26214400,
          "maxRecipientsPerField": 500,
          "maxRecipientsTotal": 500,
          "maxAddressChars": 640
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "8 à 200 caractères imprimables ; un rejeu avec la même valeur **et la même charge** ne produit jamais un second effet et renvoie le premier résultat. Réutiliser la même valeur avec une charge différente est refusé `409`, jamais silencieusement accepté. Sur les routes d’envoi, une valeur qui se heurte à une réservation posée par une autre voie d’envoi de l’espace est également refusée `409`, avec un code distinct — voir la note « Portée exacte de la garantie d’idempotence » de la route.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mailbox": {
                    "type": "string",
                    "description": "boîte ciblée, obligatoirement accessible à la clé."
                  },
                  "to": {
                    "type": "array",
                    "items": {},
                    "description": "destinataires principaux."
                  },
                  "cc": {
                    "type": "array",
                    "items": {},
                    "description": "destinataires en copie."
                  },
                  "bcc": {
                    "type": "array",
                    "items": {},
                    "description": "destinataires en copie cachée."
                  },
                  "subject": {
                    "type": "string",
                    "description": "objet du message."
                  },
                  "body": {
                    "type": "string",
                    "description": "corps du message (HTML accepté)."
                  },
                  "threadId": {
                    "type": "string",
                    "description": "fil auquel rattacher le message."
                  },
                  "inReplyTo": {
                    "type": "string",
                    "description": "identifiant du message auquel on répond."
                  },
                  "priority": {
                    "type": "string",
                    "description": "priorité déclarée du message."
                  },
                  "attachments": {
                    "type": "array",
                    "items": {},
                    "description": "pièces jointes. Chaque entrée est un objet `{ \"contentBase64\", \"filename\", \"mimeType\" }` : `contentBase64` est **obligatoire** et doit être du base64 canonique strict (alphabet standard, longueur multiple de 4, bits de bourrage nuls — sinon `400`) ; `filename` accepte l’alias `name` et `mimeType` l’alias `type`, tous deux facultatifs. Le type déclaré n’est **pas** repris tel quel : le serveur inspecte les premiers octets et impose le type réellement détecté. Plafonds : voir « Plafonds des routes d’envoi »."
                  },
                  "dlpWarningToken": {
                    "type": "string",
                    "description": "jeton confirmant un envoi malgré un avertissement de prévention de fuite de données."
                  }
                },
                "required": [
                  "mailbox",
                  "to",
                  "subject",
                  "body"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "même corps que la route précédente (dlpStatus, webhookDeferred), mêmes en-têtes d’idempotence (hit / hit-mem)."
          },
          "400": {
            "description": "mêmes refus de charge utile que la route précédente (champs obligatoires, Idempotency-Key, base64, forme des adresses, listes d’adresses hors plafond par champ ou au total du message).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "interrupteur d’envoi fermé, contenu bloqué, clé en lecture seule sur la boîte, ou clé de type agent (refus systématique sur toute route d’envoi, quel que soit son mode d’approbation et même si elle porte mail:send).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "mêmes conflits que la route précédente (clé d’idempotence réutilisée avec une autre charge, clé déjà enregistrée par une autre voie d’envoi et donc invérifiable, envoi déjà en cours, avertissement de prévention de fuite de données à confirmer) — aucun n’envoie de message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "pièces jointes hors plafond — aucun message n’est parti.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "reçu d’envoi indisponible, outcomeUnknown: true — issue ambiguë, ne pas relancer avec une nouvelle clé d’idempotence.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service d’envoi indisponible ou verrou occupé — réessayable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/drafts": {
      "get": {
        "operationId": "listDrafts",
        "summary": "Liste les brouillons de la boîte.",
        "tags": [
          "Brouillons"
        ],
        "description": "Liste les brouillons de la boîte.\n\nLa lecture des brouillons exige le même scope `drafts:write` que leur écriture — il n’existe pas de scope de lecture séparé.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "drafts:write",
        "parameters": [
          {
            "name": "mailbox",
            "in": "query",
            "required": true,
            "description": "boîte ciblée, obligatoirement accessible à la clé.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "1 à 100, défaut 30.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"mailbox\", \"drafts\": [...] }"
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createDraft",
        "summary": "Crée un brouillon dans la boîte.",
        "tags": [
          "Brouillons"
        ],
        "description": "Crée un brouillon dans la boîte.\n\n`drafts:write` n’autorise **que** l’écriture de brouillons : il ne donne aucun droit d’envoi.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "drafts:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mailbox": {
                    "type": "string",
                    "description": "boîte ciblée, obligatoirement accessible à la clé."
                  },
                  "draft": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "contenu du brouillon (`to`, `subject`, `body`…). À défaut, le corps entier est utilisé."
                  }
                },
                "required": [
                  "mailbox"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{ \"draft\": { ... } }"
          },
          "202": {
            "description": "{ \"approvalRequired\": true, \"approval\": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400)."
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/drafts/{id}": {
      "patch": {
        "operationId": "updateDraft",
        "summary": "Met à jour un brouillon existant.",
        "tags": [
          "Brouillons"
        ],
        "description": "Met à jour un brouillon existant.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "drafts:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "identifiant de la ressource.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mailbox": {
                    "type": "string",
                    "description": "boîte ciblée, obligatoirement accessible à la clé."
                  },
                  "draft": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "champs à écrire."
                  }
                },
                "required": [
                  "mailbox"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ \"draft\": { ... } }"
          },
          "202": {
            "description": "{ \"approvalRequired\": true, \"approval\": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400)."
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteDraft",
        "summary": "Supprime un brouillon.",
        "tags": [
          "Brouillons"
        ],
        "description": "Supprime un brouillon.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "drafts:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "identifiant de la ressource.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mailbox",
            "in": "query",
            "required": true,
            "description": "boîte ciblée, obligatoirement accessible à la clé.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"ok\": true, \"id\" }"
          },
          "202": {
            "description": "{ \"approvalRequired\": true, \"approval\": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400)."
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/contacts": {
      "get": {
        "operationId": "listContacts",
        "summary": "Liste les contacts des carnets accessibles à la clé.",
        "tags": [
          "Contacts"
        ],
        "description": "Liste les contacts des carnets accessibles à la clé.\n\nCette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est `403 { \"error\": \"identité membre incomplète pour cette route\" }`, avant tout accès aux données.\n\nLes erreurs de cette route portent un **code machine** (`{ \"error\": \"CODE\" }`), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "read:contacts",
        "parameters": [
          {
            "name": "bookId",
            "in": "query",
            "required": false,
            "description": "restreint la liste à un carnet.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "group",
            "in": "query",
            "required": false,
            "description": "restreint la liste à un groupe (comparaison insensible à la casse).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "filtre texte sur le nom affiché, l’organisation et les adresses.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "`name` (défaut, ordre alphabétique français) ou `updated` (plus récemment modifiés d’abord).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"contacts\": [...], \"total\": nombre } — total est le nombre de contacts correspondant au filtre, contacts en contient **au plus 2 000** (troncature ; il n’y a pas de pagination sur cette route). Les contacts fusionnés sont exclus, et le résumé de fusion (mergeLog) n’est rendu que par la route de détail."
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createContact",
        "summary": "Crée un contact dans un carnet.",
        "tags": [
          "Contacts"
        ],
        "description": "Crée un contact dans un carnet.\n\nLes champs du contact s’écrivent à la racine du corps, ou dans un objet `contact` — les deux formes sont acceptées.\n\nUn contact doit porter **au moins** un nom, une adresse ou une organisation, sinon `400 EMPTY_CONTACT`.\n\nCréer deux fois le même contact dans le même carnet ne produit jamais de doublon : le second appel renvoie `200` avec `\"existed\": true`. Testez donc `existed`, pas le code `201`, si vous devez distinguer une création d’une mise à jour.\n\nL’omission de `bookId` n’est pas traitée comme une erreur de validation : elle produit une erreur générique. Fournissez toujours `bookId`.\n\nCette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est `403 { \"error\": \"identité membre incomplète pour cette route\" }`, avant tout accès aux données.\n\nLes erreurs de cette route portent un **code machine** (`{ \"error\": \"CODE\" }`), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "write:contacts",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "bookId": {
                    "type": "string",
                    "description": "carnet destinataire, obligatoire ; la clé doit pouvoir y écrire. Un carnet inconnu répond `404 BOOK_NOT_FOUND`."
                  },
                  "displayName": {
                    "type": "string",
                    "description": "nom affiché, 120 caractères maximum ; il peut différer de l’état civil. À défaut, il est déduit de `givenName`/`familyName`, puis de la première adresse, puis de `organization`."
                  },
                  "givenName": {
                    "type": "string",
                    "description": "prénom, 80 caractères maximum."
                  },
                  "familyName": {
                    "type": "string",
                    "description": "nom de famille, 80 caractères maximum."
                  },
                  "emails": {
                    "type": "array",
                    "items": {},
                    "description": "adresses, sous la forme `[{ \"value\": \"jean@exemple.com\", \"type\": \"work\" }]` — **objets, pas des chaînes** : une entrée sans `value` valide est ignorée. 10 entrées maximum, doublons retirés."
                  },
                  "phones": {
                    "type": "array",
                    "items": {},
                    "description": "téléphones, même forme `{ \"value\", \"type\" }`, 10 entrées maximum."
                  },
                  "organization": {
                    "type": "string",
                    "description": "organisation, 120 caractères maximum."
                  },
                  "jobTitle": {
                    "type": "string",
                    "description": "intitulé de poste, 120 caractères maximum."
                  },
                  "groups": {
                    "type": "array",
                    "items": {},
                    "description": "groupes (chaînes), 20 maximum, 60 caractères chacun."
                  },
                  "notes": {
                    "type": "string",
                    "description": "notes libres, 2 000 caractères maximum."
                  }
                },
                "required": [
                  "bookId"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ \"contact\": { ... }, \"existed\": true } — **fusion, pas création** : un contact du même carnet portait déjà la même clé de déduplication (adresse principale normalisée, ou nom affiché à défaut d’adresse). Les champs fournis écrasent les anciens, l’identifiant et l’auteur d’origine sont conservés."
          },
          "201": {
            "description": "{ \"contact\": { ... } } — contact créé."
          },
          "202": {
            "description": "{ \"approvalRequired\": true, \"approval\": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400)."
          },
          "400": {
            "description": "contact vide (EMPTY_CONTACT : ni nom, ni adresse, ni organisation) ou charge refusée (VALIDATION_FAILED).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "404": {
            "description": "BOOK_NOT_FOUND — bookId inconnu dans cet espace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          }
        }
      }
    },
    "/v1/contacts/{id}": {
      "get": {
        "operationId": "getContact",
        "summary": "Détail d’un contact.",
        "tags": [
          "Contacts"
        ],
        "description": "Détail d’un contact.\n\nCette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est `403 { \"error\": \"identité membre incomplète pour cette route\" }`, avant tout accès aux données.\n\nLes erreurs de cette route portent un **code machine** (`{ \"error\": \"CODE\" }`), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "read:contacts",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "identifiant de la ressource.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"contact\": { ... } } — forme complète, mergeLog inclus."
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateContact",
        "summary": "Met à jour un contact.",
        "tags": [
          "Contacts"
        ],
        "description": "Met à jour un contact.\n\nLe carnet d’appartenance est **immuable** par cette route : `bookId` fourni ici est ignoré.\n\nLa piste d’audit de fusion (`mergedFrom`, `mergeLog`) et l’auteur d’origine sont préservés.\n\nCette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est `403 { \"error\": \"identité membre incomplète pour cette route\" }`, avant tout accès aux données.\n\nLes erreurs de cette route portent un **code machine** (`{ \"error\": \"CODE\" }`), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "write:contacts",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "identifiant de la ressource.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "displayName": {
                    "type": "string",
                    "description": "mêmes champs qu’à la création (racine du corps ou objet `contact`)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ \"contact\": { ... } }"
          },
          "202": {
            "description": "{ \"approvalRequired\": true, \"approval\": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400)."
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "404": {
            "description": "CONTACT_NOT_FOUND — contact inconnu ou fusionné.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteContact",
        "summary": "Supprime un contact.",
        "tags": [
          "Contacts"
        ],
        "description": "Supprime un contact.\n\nCette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est `403 { \"error\": \"identité membre incomplète pour cette route\" }`, avant tout accès aux données.\n\nLes erreurs de cette route portent un **code machine** (`{ \"error\": \"CODE\" }`), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "write:contacts",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "identifiant de la ressource.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"ok\": true }"
          },
          "202": {
            "description": "{ \"approvalRequired\": true, \"approval\": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400)."
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "404": {
            "description": "CONTACT_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          }
        }
      }
    },
    "/v1/calendars": {
      "get": {
        "operationId": "listCalendars",
        "summary": "Liste les calendriers accessibles à la clé.",
        "tags": [
          "Agenda"
        ],
        "description": "Liste les calendriers accessibles à la clé.\n\nCette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est `403 { \"error\": \"identité membre incomplète pour cette route\" }`, avant tout accès aux données.\n\nLes erreurs de cette route portent un **code machine** (`{ \"error\": \"CODE\" }`), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "read:calendar",
        "responses": {
          "200": {
            "description": "{ \"calendars\": [...], \"me\": \"adresse de la clé\" } — calendriers shared de l’espace et calendriers personal du membre auquel la clé est rattachée."
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createCalendar",
        "summary": "Crée un calendrier.",
        "tags": [
          "Agenda"
        ],
        "description": "Crée un calendrier.\n\nCette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est `403 { \"error\": \"identité membre incomplète pour cette route\" }`, avant tout accès aux données.\n\nLes erreurs de cette route portent un **code machine** (`{ \"error\": \"CODE\" }`), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "write:calendar",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "nom du calendrier, obligatoire, 80 caractères maximum — vide ou absent : `400 INVALID_ARGUMENT`."
                  },
                  "scope": {
                    "type": "string",
                    "description": "`personal` (défaut) ou `shared`. Un calendrier `shared` est visible par tout l’espace et sa création est réservée à la clé de l’administrateur de l’espace (sinon `403 CALENDAR_SCOPE_FORBIDDEN`)."
                  },
                  "color": {
                    "type": "string",
                    "description": "couleur d’affichage, 20 caractères maximum."
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{ \"calendar\": { \"id\", \"name\", \"color\", \"scope\", \"ownerUid\", ... } }"
          },
          "202": {
            "description": "{ \"approvalRequired\": true, \"approval\": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400)."
          },
          "400": {
            "description": "INVALID_ARGUMENT — name vide ou absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "403": {
            "description": "CALENDAR_SCOPE_FORBIDDEN — scope: \"shared\" demandé par une clé non administratrice.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          }
        }
      }
    },
    "/v1/calendars/{id}": {
      "delete": {
        "operationId": "deleteCalendar",
        "summary": "Supprime un calendrier **et tous ses événements**.",
        "tags": [
          "Agenda"
        ],
        "description": "Supprime un calendrier **et tous ses événements**.\n\nSuppression **en cascade** : tous les événements du calendrier sont supprimés, et `deletedEvents` en donne le compte. Il n’y a pas de corbeille pour cette opération.\n\nUn calendrier ne se lit ni ne se modifie unitairement par cette API : seules la liste, la création et la suppression sont exposées.\n\nCette route n’est pas prise en charge par la file d’approbation : une clé de type `agent` à approbation requise reçoit `404`, jamais `202`.\n\nCette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est `403 { \"error\": \"identité membre incomplète pour cette route\" }`, avant tout accès aux données.\n\nLes erreurs de cette route portent un **code machine** (`{ \"error\": \"CODE\" }`), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "write:calendar",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "identifiant de la ressource.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"ok\": true, \"deletedEvents\": nombre }"
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "403": {
            "description": "FORBIDDEN — calendrier shared : seule la clé de l’administrateur de l’espace peut le supprimer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "404": {
            "description": "CALENDAR_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          }
        }
      }
    },
    "/v1/calendar-events": {
      "get": {
        "operationId": "listCalendarEvents",
        "summary": "Liste les événements des calendriers accessibles, sur une fenêtre de temps.",
        "tags": [
          "Agenda"
        ],
        "description": "Liste les événements des calendriers accessibles, sur une fenêtre de temps.\n\nCette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est `403 { \"error\": \"identité membre incomplète pour cette route\" }`, avant tout accès aux données.\n\nLes erreurs de cette route portent un **code machine** (`{ \"error\": \"CODE\" }`), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "read:calendar",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "début de la fenêtre, en **millisecondes depuis epoch** (pas une date ISO). Défaut : il y a 30 jours.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "fin de la fenêtre, en millisecondes depuis epoch. Défaut : dans 60 jours.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "calendarId",
            "in": "query",
            "required": false,
            "description": "restreint la liste à un calendrier (inconnu ou hors de portée : `404` / `403`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"events\": [...], \"occurrences\": [...], \"window\": { \"fromMs\", \"toMs\" }, \"remindersDue\": [...], \"me\" }. events porte les événements (récurrents inclus, **au plus 1 000**) ; occurrences développe les récurrences dans la fenêtre sous la forme { \"eventId\", \"calendarId\", \"startMs\", \"endMs\" }, triées par début et **plafonnées à 5 000**. Les événements annulés sont exclus. Ces deux plafonds sont des troncatures : il n’y a pas de pagination — resserrez from/to pour rester en deçà."
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createCalendarEvent",
        "summary": "Crée un événement dans un calendrier.",
        "tags": [
          "Agenda"
        ],
        "description": "Crée un événement dans un calendrier.\n\nLes champs de l’événement s’écrivent à la racine du corps, ou dans un objet `event` — les deux formes sont acceptées.\n\nToutes les dates de cette API sont des **millisecondes depuis epoch**, jamais des chaînes ISO 8601.\n\nCette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est `403 { \"error\": \"identité membre incomplète pour cette route\" }`, avant tout accès aux données.\n\nLes erreurs de cette route portent un **code machine** (`{ \"error\": \"CODE\" }`), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "write:calendar",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "calendarId": {
                    "type": "string",
                    "description": "calendrier destinataire, obligatoire ; la clé doit pouvoir y écrire. Inconnu : `404 CALENDAR_NOT_FOUND`."
                  },
                  "title": {
                    "type": "string",
                    "description": "intitulé, obligatoire, 200 caractères maximum — vide : `400 TITLE_REQUIRED`."
                  },
                  "startMs": {
                    "type": "integer",
                    "description": "début, en **millisecondes depuis epoch** (nombre). Ce n’est pas une date ISO 8601 : une chaîne `\"2026-08-01T09:00:00Z\"` est refusée (`400 INVALID_DATES`)."
                  },
                  "endMs": {
                    "type": "integer",
                    "description": "fin, en millisecondes depuis epoch, strictement supérieure à `startMs` (sinon `400 END_BEFORE_START`) et à 366 jours au maximum (sinon `400 DURATION_TOO_LONG`)."
                  },
                  "allDay": {
                    "type": "boolean",
                    "description": "journée entière : les bornes sont recadrées sur des jours UTC pleins, fin exclusive."
                  },
                  "description": {
                    "type": "string",
                    "description": "description, 5 000 caractères maximum."
                  },
                  "location": {
                    "type": "string",
                    "description": "lieu, 200 caractères maximum."
                  },
                  "conferenceLink": {
                    "type": "string",
                    "description": "lien de visioconférence ; doit commencer par `http://` ou `https://` (sinon `400 INVALID_CONFERENCE_LINK`)."
                  },
                  "timezone": {
                    "type": "string",
                    "description": "fuseau de l’heure murale, défaut `Europe/Paris`."
                  },
                  "attendees": {
                    "type": "array",
                    "items": {},
                    "description": "participants. **Aucune invitation n’est envoyée par courriel** : la réponse se fait dans l’application."
                  },
                  "reminders": {
                    "type": "array",
                    "items": {},
                    "description": "rappels, sous la forme `[{ \"minutes\": 15 }]`."
                  },
                  "recurrence": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "règle de répétition."
                  }
                },
                "required": [
                  "calendarId",
                  "title",
                  "startMs",
                  "endMs"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ \"event\": { ... }, \"existed\": true, \"conflicts\": [...] } — **fusion, pas création** : un événement importé portant le même identifiant externe existait déjà."
          },
          "201": {
            "description": "{ \"event\": { ... }, \"conflicts\": [...] } — événement créé. conflicts liste les chevauchements détectés dans le même calendrier ; ils sont **signalés, jamais bloquants**."
          },
          "202": {
            "description": "{ \"approvalRequired\": true, \"approval\": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400)."
          },
          "400": {
            "description": "TITLE_REQUIRED, INVALID_DATES, END_BEFORE_START, DURATION_TOO_LONG ou INVALID_CONFERENCE_LINK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "404": {
            "description": "CALENDAR_NOT_FOUND — calendarId inconnu.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          }
        }
      }
    },
    "/v1/calendar-events/{id}": {
      "get": {
        "operationId": "getCalendarEvent",
        "summary": "Détail d’un événement.",
        "tags": [
          "Agenda"
        ],
        "description": "Détail d’un événement.\n\nCette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est `403 { \"error\": \"identité membre incomplète pour cette route\" }`, avant tout accès aux données.\n\nLes erreurs de cette route portent un **code machine** (`{ \"error\": \"CODE\" }`), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "read:calendar",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "identifiant de la ressource.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"event\": { ... }, \"me\" }"
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "404": {
            "description": "EVENT_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateCalendarEvent",
        "summary": "Met à jour un événement.",
        "tags": [
          "Agenda"
        ],
        "description": "Met à jour un événement.\n\nLe calendrier d’appartenance est **immuable** par cette route.\n\nPoser `\"status\": \"cancelled\"` annule l’événement : il sort des listes sans être supprimé.\n\nCette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est `403 { \"error\": \"identité membre incomplète pour cette route\" }`, avant tout accès aux données.\n\nLes erreurs de cette route portent un **code machine** (`{ \"error\": \"CODE\" }`), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "write:calendar",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "identifiant de la ressource.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "mêmes champs qu’à la création (racine du corps ou objet `event`) ; les champs absents gardent leur valeur."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ \"event\": { ... }, \"conflicts\": [...] }"
          },
          "202": {
            "description": "{ \"approvalRequired\": true, \"approval\": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400)."
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "404": {
            "description": "EVENT_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteCalendarEvent",
        "summary": "Supprime un événement.",
        "tags": [
          "Agenda"
        ],
        "description": "Supprime un événement.\n\nCette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est `403 { \"error\": \"identité membre incomplète pour cette route\" }`, avant tout accès aux données.\n\nLes erreurs de cette route portent un **code machine** (`{ \"error\": \"CODE\" }`), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "write:calendar",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "identifiant de la ressource.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"ok\": true }"
          },
          "202": {
            "description": "{ \"approvalRequired\": true, \"approval\": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400)."
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "404": {
            "description": "EVENT_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          }
        }
      }
    },
    "/v1/tasks": {
      "get": {
        "operationId": "listTasks",
        "summary": "Liste les tâches de l’espace.",
        "tags": [
          "Tâches"
        ],
        "description": "Liste les tâches de l’espace.\n\nCette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est `403 { \"error\": \"identité membre incomplète pour cette route\" }`, avant tout accès aux données.\n\nLes erreurs de cette route portent un **code machine** (`{ \"error\": \"CODE\" }`), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "read:tasks",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "filtre par statut.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "assignee",
            "in": "query",
            "required": false,
            "description": "filtre par adresse assignée.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "teamId",
            "in": "query",
            "required": false,
            "description": "filtre par équipe.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "threadId",
            "in": "query",
            "required": false,
            "description": "filtre par fil de conversation rattaché.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "filtre texte sur l’intitulé et les notes.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"tasks\": [...], \"groups\": { ... }, \"remindersDue\": [...], \"me\" } — tasks est trié et **tronqué à 2 000 entrées** (pas de pagination) ; groups compte les tâches par statut sur l’ensemble filtré."
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createTask",
        "summary": "Crée une tâche.",
        "tags": [
          "Tâches"
        ],
        "description": "Crée une tâche.\n\nLes champs de la tâche s’écrivent à la racine du corps, ou dans un objet `task` — les deux formes sont acceptées.\n\nLa déduplication ne joue que si `threadId` est fourni : sans lui, deux appels identiques créent deux tâches.\n\nCette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est `403 { \"error\": \"identité membre incomplète pour cette route\" }`, avant tout accès aux données.\n\nLes erreurs de cette route portent un **code machine** (`{ \"error\": \"CODE\" }`), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "write:tasks",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "intitulé, obligatoire, 200 caractères maximum — vide : `400 TITLE_REQUIRED`."
                  },
                  "notes": {
                    "type": "string",
                    "description": "notes libres, 5 000 caractères maximum."
                  },
                  "status": {
                    "type": "string",
                    "description": "statut initial ; une valeur inconnue retombe sur `todo`."
                  },
                  "priority": {
                    "type": "string",
                    "description": "priorité ; une valeur inconnue retombe sur `normal`."
                  },
                  "assignee": {
                    "type": "string",
                    "description": "adresse de la personne assignée ; une adresse invalide est ignorée (champ vidé)."
                  },
                  "teamId": {
                    "type": "string",
                    "description": "équipe destinataire."
                  },
                  "dueMs": {
                    "type": "integer",
                    "description": "échéance en **millisecondes depuis epoch** (pas une date ISO). Obligatoire si `recurrence` est fourni, sinon `400 RECURRENCE_NEEDS_DUE`."
                  },
                  "threadId": {
                    "type": "string",
                    "description": "fil de conversation rattaché. Avec `title`, il forme la clé de déduplication de la tâche."
                  },
                  "recurrence": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "règle de répétition ; l’occurrence suivante est créée à la complétion."
                  },
                  "reminders": {
                    "type": "array",
                    "items": {},
                    "description": "rappels, sous la forme `[{ \"minutes\": 60 }]`."
                  },
                  "source": {
                    "type": "string",
                    "description": "provenance ; seules `manual` (défaut) et `mail` sont acceptées d’un client. Toute autre valeur est refusée en `403 SOURCE_FORBIDDEN`."
                  }
                },
                "required": [
                  "title"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ \"task\": { ... }, \"existed\": true } — **aucune création** : une tâche portant le même threadId et le même intitulé existait déjà ; elle est renvoyée telle quelle, sans être modifiée."
          },
          "201": {
            "description": "{ \"task\": { ... } } — tâche créée."
          },
          "202": {
            "description": "{ \"approvalRequired\": true, \"approval\": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400)."
          },
          "400": {
            "description": "TITLE_REQUIRED ou RECURRENCE_NEEDS_DUE.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "403": {
            "description": "SOURCE_FORBIDDEN — source réservée au serveur.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          }
        }
      }
    },
    "/v1/tasks/{id}": {
      "get": {
        "operationId": "getTask",
        "summary": "Détail d’une tâche.",
        "tags": [
          "Tâches"
        ],
        "description": "Détail d’une tâche.\n\nCette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est `403 { \"error\": \"identité membre incomplète pour cette route\" }`, avant tout accès aux données.\n\nLes erreurs de cette route portent un **code machine** (`{ \"error\": \"CODE\" }`), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "read:tasks",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "identifiant de la ressource.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"task\": { ... }, \"me\" }"
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "404": {
            "description": "TASK_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateTask",
        "summary": "Met à jour une tâche.",
        "tags": [
          "Tâches"
        ],
        "description": "Met à jour une tâche.\n\nLa provenance (`source`) est **immuable** : une tâche posée par le serveur ne peut pas être « blanchie » en `manual`.\n\nCette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est `403 { \"error\": \"identité membre incomplète pour cette route\" }`, avant tout accès aux données.\n\nLes erreurs de cette route portent un **code machine** (`{ \"error\": \"CODE\" }`), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "write:tasks",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "identifiant de la ressource.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "mêmes champs qu’à la création (racine du corps ou objet `task`) ; les champs absents gardent leur valeur."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ \"task\": { ... } }"
          },
          "202": {
            "description": "{ \"approvalRequired\": true, \"approval\": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400)."
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "404": {
            "description": "TASK_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteTask",
        "summary": "Supprime une tâche.",
        "tags": [
          "Tâches"
        ],
        "description": "Supprime une tâche.\n\nCette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est `403 { \"error\": \"identité membre incomplète pour cette route\" }`, avant tout accès aux données.\n\nLes erreurs de cette route portent un **code machine** (`{ \"error\": \"CODE\" }`), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "write:tasks",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "identifiant de la ressource.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"ok\": true }"
          },
          "202": {
            "description": "{ \"approvalRequired\": true, \"approval\": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400)."
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "404": {
            "description": "TASK_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCode"
                }
              }
            }
          }
        }
      }
    },
    "/v1/signatures": {
      "get": {
        "operationId": "listSignatures",
        "summary": "Liste les signatures configurées pour l’espace.",
        "tags": [
          "Signatures et analytics"
        ],
        "description": "Liste les signatures configurées pour l’espace.\n\nCette route relève de la famille « conversations » : elle exige `read:conversations`, pas un scope dédié.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "read:conversations",
        "responses": {
          "200": {
            "description": "{ \"signatures\": [...] }"
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/analytics/summary": {
      "get": {
        "operationId": "analyticsSummary",
        "summary": "Résumé analytique de l’espace (volumes).",
        "tags": [
          "Signatures et analytics"
        ],
        "description": "Résumé analytique de l’espace (volumes).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "read:analytics",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "début de période (date ISO).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "fin de période (date ISO).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"summary\": { \"volumes\": {...}, \"metricAvailability\": \"volumes_only\" } }"
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks/test": {
      "get": {
        "operationId": "testWebhook",
        "summary": "Émet un événement de test vers les souscriptions webhook de l’espace.",
        "tags": [
          "Webhooks"
        ],
        "description": "Émet un événement de test vers les souscriptions webhook de l’espace.\n\nLa gestion des souscriptions elles-mêmes ne passe pas par cette API à clé, mais par les réglages de l’espace.\n\nC’est la seule route `GET` considérée comme une écriture : elle produit un effet observable (livraison sortante). C’est aussi la seule route `GET` qui peut répondre `202` à une clé de type `agent` à approbation requise.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "write:conversations",
        "responses": {
          "200": {
            "description": "{ \"ok\": true, \"event\": { ... } }"
          },
          "202": {
            "description": "{ \"approvalRequired\": true, \"approval\": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400)."
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur **toute** route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "operationId": "mcpJsonRpc",
        "summary": "Point d’entrée JSON-RPC 2.0 du serveur MCP (agents IA).",
        "tags": [
          "MCP"
        ],
        "description": "Point d’entrée JSON-RPC 2.0 du serveur MCP (agents IA).\n\nLe scope exigé n’est pas fixe : il est dérivé de l’**outil** appelé (par exemple `read:conversations` pour une recherche, `drafts:write` pour la création d’un brouillon). Un agent n’a donc jamais plus de droits que sa clé.\n\nAucun outil MCP ne peut envoyer de message.\n\nLe gate d’approbation REST ne s’applique pas ici : le serveur MCP gère lui-même la file d’approbation d’une clé de type `agent`. Cette route ne répond donc jamais `202`.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "x-trame-scope": "mcp",
        "responses": {
          "200": {
            "description": "réponse JSON-RPC de l’outil appelé."
          },
          "400": {
            "description": "requête invalide (paramètre manquant, valeur hors plafond), ou en-tête `Idempotency-Key` absent : toute écriture demandée par une clé de type `agent` à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, `400` couvre aussi les listes d’adresses hors plafond — **par champ ET au total du message** — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est **tracé** : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type `agent` sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "plafond de fréquence dépassé pour cette clé. Ce plafond est compté **par instance de service** : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le `401`, ce refus est **tracé** : toujours dans le journal du service, et dans le journal d’audit de votre espace **si la clé a pu être identifiée** — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "erreur interne — le message ne contient jamais de détail d’implémentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service MCP indisponible sur cet espace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "Clé API de l’espace, montrée une seule fois à sa création."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Le message est rédigé pour un humain et ne contient jamais de détail d’implémentation. N’écrivez pas de test sur son texte : il peut être reformulé.",
        "properties": {
          "error": {
            "type": "string",
            "description": "message d’erreur lisible, sans détail d’implémentation."
          }
        },
        "required": [
          "error"
        ]
      },
      "ErrorCode": {
        "type": "object",
        "description": "Le champ error porte un identifiant stable, pas une phrase : testez-le tel quel plutôt que d’afficher sa valeur à un utilisateur final. Tout code hors liste blanche est remplacé par le code de repli du domaine.",
        "properties": {
          "error": {
            "type": "string",
            "description": "identifiant d’erreur stable, en majuscules.",
            "enum": [
              "BOOK_NOT_FOUND",
              "CALENDAR_FAILED",
              "CALENDAR_NOT_FOUND",
              "CALENDAR_SCOPE_FORBIDDEN",
              "CONTACTS_FAILED",
              "CONTACT_NOT_FOUND",
              "DURATION_TOO_LONG",
              "EMPTY_CONTACT",
              "END_BEFORE_START",
              "EVENT_NOT_FOUND",
              "FORBIDDEN",
              "INVALID_ARGUMENT",
              "INVALID_CONFERENCE_LINK",
              "INVALID_DATES",
              "METHOD_NOT_ALLOWED",
              "RECURRENCE_NEEDS_DUE",
              "SOURCE_FORBIDDEN",
              "TASKS_FAILED",
              "TASK_NOT_FOUND",
              "TITLE_REQUIRED",
              "VALIDATION_FAILED"
            ]
          }
        },
        "required": [
          "error"
        ]
      }
    }
  },
  "x-trame-rate-limits": {
    "default": 120,
    "mail:send": 10
  },
  "x-trame-send-limits": {
    "maxAttachments": 50,
    "maxAttachmentBytes": 26214400,
    "maxRecipientsPerField": 500,
    "maxRecipientsTotal": 500,
    "maxAddressChars": 640
  }
}
