{
  "openapi": "3.1.0",
  "info": {
    "title": "API Galet",
    "version": "1",
    "description": "Les routes publiques de Galet. La collecte s'authentifie par la clé du canal ; la lecture par un jeton `Authorization: Bearer`. Les erreurs ont la forme `{ error, code }`."
  },
  "servers": [
    {
      "url": "https://api.galet.dev"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Jeton OAuth de Galet (celui de la connexion MCP) : scope `feedbacks:read` pour lire, `feedbacks:write` pour changer un statut. Les clés d'API d'organisation utiliseront le même en-tête."
      }
    },
    "schemas": {
      "QualificationStep": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identifiant de la qualification, valable 15 minutes après la dernière réponse."
          },
          "status": {
            "type": "string",
            "enum": [
              "ask",
              "answer",
              "drop",
              "restart",
              "resolved"
            ],
            "description": "`ask` : répondre à `questions`. `answer` : Galet AI répond depuis le centre d'aide ; `questions` demande si la réponse suffit, et sinon la qualification continue. `resolved` : la réponse a suffi, rien n'est enregistré. `drop` : hors sujet, rien n'est enregistré. `restart` : la personne a changé de sujet ; rien n'est enregistré, recommencez avec un nouveau retour."
          },
          "questions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QualificationQuestion"
            }
          },
          "source": {
            "type": "object",
            "properties": {
              "title": {
                "type": "string"
              },
              "url": {
                "type": "string"
              }
            },
            "required": [
              "title",
              "url"
            ],
            "description": "`answer` : la page d'aide d'où vient la réponse."
          },
          "message": {
            "type": "string",
            "description": "`drop`, `restart` et `resolved` : le message à afficher."
          }
        },
        "required": [
          "id",
          "status"
        ]
      },
      "QualificationQuestion": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "prompt": {
            "type": "string",
            "description": "La question, à afficher telle quelle."
          },
          "mode": {
            "type": "string",
            "enum": [
              "text",
              "single",
              "multi",
              "yes_no",
              "scale"
            ],
            "description": "`text` : réponse libre. `single`, `yes_no`, `scale` : un choix. `multi` : un ou plusieurs choix."
          },
          "choices": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                }
              },
              "required": [
                "id",
                "label"
              ]
            }
          },
          "allowTextFallback": {
            "type": "boolean",
            "description": "Une réponse libre est aussi acceptée."
          }
        },
        "required": [
          "id",
          "prompt",
          "mode"
        ]
      },
      "ReportResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "ticketId": {
            "type": "string",
            "description": "Identifiant du feedback créé."
          },
          "merged": {
            "type": "boolean",
            "description": "`false` à la création : le regroupement avec un feedback existant a lieu ensuite, en arrière-plan."
          },
          "statusId": {
            "type": "string",
            "description": "Identifiant de suivi du retour."
          },
          "statusToken": {
            "type": "string",
            "description": "Jeton qui donne accès à la page de suivi."
          },
          "statusUrl": {
            "type": "string",
            "description": "Page de suivi du retour, à transmettre à la personne qui a écrit."
          }
        },
        "required": [
          "ok",
          "ticketId",
          "merged"
        ]
      },
      "QualifiedReportResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ReportResponse"
          },
          {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "Identifiant de la qualification."
              },
              "status": {
                "type": "string",
                "enum": [
                  "ready"
                ]
              }
            },
            "required": [
              "id",
              "status"
            ]
          }
        ]
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Message lisible, en anglais ; en français avec `Accept-Language: fr`."
          },
          "code": {
            "type": "string",
            "description": "Code stable, à tester côté client."
          }
        },
        "required": [
          "error",
          "code"
        ]
      },
      "ReportRequest": {
        "type": "object",
        "properties": {
          "publicKey": {
            "type": "string",
            "minLength": 1,
            "description": "Clé du canal API de l'app.",
            "example": "pk_live_3f9c…"
          },
          "message": {
            "type": "string",
            "minLength": 1,
            "description": "Le texte du retour, 10000 caractères au plus.",
            "example": "Export CSV vide"
          },
          "kind": {
            "type": "string",
            "enum": [
              "problem",
              "idea",
              "praise",
              "issue",
              "bug",
              "greeting"
            ],
            "description": "Type du retour : `problem`, `idea` ou `praise`. `issue`, `bug` et `greeting` sont acceptés pour compatibilité.",
            "example": "problem"
          },
          "title": {
            "type": "string",
            "description": "Un titre ; sinon Galet AI le rédige."
          },
          "observed": {
            "type": "string",
            "description": "Ce qui s'est passé."
          },
          "expected": {
            "type": "string",
            "description": "Ce qui était attendu."
          },
          "url": {
            "type": "string",
            "description": "L'adresse concernée, gardée comme contexte.",
            "example": "https://app.acme.com/exports"
          },
          "user": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "Identifiant dans votre app."
              },
              "email": {
                "type": "string",
                "description": "Ignoré s'il n'a pas la forme d'un e-mail.",
                "example": "lea@acme.com"
              },
              "name": {
                "type": "string",
                "description": "Nom affiché."
              }
            },
            "description": "La personne qui a écrit."
          },
          "qualify": {
            "type": "boolean",
            "description": "`true` : Galet AI pose d'abord les questions qui manquent (voir `POST /v1/reports/{id}/answers`). Le retour est enregistré quand il est assez précis."
          }
        },
        "required": [
          "publicKey",
          "message",
          "kind"
        ]
      },
      "QualificationAnswer": {
        "type": "object",
        "properties": {
          "publicKey": {
            "type": "string",
            "minLength": 1,
            "description": "La clé utilisée pour créer le retour."
          },
          "text": {
            "type": "string",
            "description": "Réponse libre."
          },
          "choiceIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Identifiants des choix retenus parmi `questions[].choices`."
          }
        },
        "required": [
          "publicKey"
        ]
      },
      "FeedbackPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FeedbackSummary"
            }
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "À passer en `cursor` pour la page suivante ; `null` à la dernière."
          }
        },
        "required": [
          "data",
          "nextCursor"
        ]
      },
      "FeedbackSummary": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "Clé lisible.",
            "example": "FIF-12"
          },
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "problem",
              "idea",
              "praise"
            ]
          },
          "severity": {
            "type": "string",
            "enum": [
              "P0",
              "P1",
              "P2",
              "P3"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "triage",
              "prioritized",
              "in_development",
              "fixed",
              "delivered",
              "dismissed"
            ]
          },
          "app": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "id"
            ]
          },
          "feature": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "path": {
                "type": "string",
                "example": "Exports → CSV"
              }
            },
            "required": [
              "id",
              "path"
            ],
            "description": "La fonctionnalité rattachée, avec son chemin dans la carte."
          },
          "reports": {
            "type": "integer",
            "description": "Nombre de retours regroupés."
          },
          "createdAt": {
            "type": "string",
            "description": "ISO 8601."
          },
          "lastActivityAt": {
            "type": "string",
            "description": "Dernier retour reçu, ISO 8601."
          },
          "trackerIssue": {
            "type": "string",
            "description": "Clé du ticket Jira ou Linear lié.",
            "example": "SVC-42"
          },
          "url": {
            "type": "string",
            "description": "Le feedback dans Galet."
          }
        },
        "required": [
          "key",
          "id",
          "title",
          "kind",
          "status",
          "app",
          "reports",
          "createdAt",
          "lastActivityAt",
          "url"
        ]
      },
      "Feedback": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "Clé lisible.",
            "example": "FIF-12"
          },
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "problem",
              "idea",
              "praise"
            ]
          },
          "severity": {
            "type": "string",
            "enum": [
              "P0",
              "P1",
              "P2",
              "P3"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "triage",
              "prioritized",
              "in_development",
              "fixed",
              "delivered",
              "dismissed"
            ]
          },
          "app": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "id"
            ]
          },
          "feature": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "path": {
                "type": "string",
                "example": "Exports → CSV"
              }
            },
            "required": [
              "id",
              "path"
            ],
            "description": "La fonctionnalité rattachée, avec son chemin dans la carte."
          },
          "reports": {
            "type": "integer",
            "description": "Nombre de retours regroupés."
          },
          "createdAt": {
            "type": "string",
            "description": "ISO 8601."
          },
          "lastActivityAt": {
            "type": "string",
            "description": "Dernier retour reçu, ISO 8601."
          },
          "url": {
            "type": "string",
            "description": "Le feedback dans Galet."
          },
          "nature": {
            "type": "string"
          },
          "summary": {
            "type": "string"
          },
          "observed": {
            "type": "string"
          },
          "expected": {
            "type": "string"
          },
          "dueAt": {
            "type": "string"
          },
          "owner": {
            "type": "string",
            "description": "E-mail du responsable."
          },
          "trackerIssue": {
            "type": "object",
            "properties": {
              "key": {
                "type": "string"
              },
              "provider": {
                "type": "string"
              },
              "status": {
                "type": "string"
              },
              "url": {
                "type": "string"
              }
            },
            "required": [
              "key",
              "provider",
              "status"
            ]
          },
          "reportList": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "source": {
                  "type": "string",
                  "description": "widget, slack, api, form…"
                },
                "author": {
                  "type": "string"
                },
                "at": {
                  "type": "string",
                  "description": "ISO 8601."
                },
                "text": {
                  "type": "string"
                }
              },
              "required": [
                "source",
                "at",
                "text"
              ]
            }
          },
          "fullContext": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": {},
            "description": "Avec Silex (plans Croissance et Entreprise) : diagnostic, fonctionnalité et fichiers rattachés, conversation de qualification, page, navigateur, console, erreurs réseau, capture et replay (liens signés, valables peu de temps). `null` sans Silex."
          }
        },
        "required": [
          "key",
          "id",
          "title",
          "kind",
          "status",
          "app",
          "reports",
          "createdAt",
          "lastActivityAt",
          "url",
          "reportList",
          "fullContext"
        ]
      },
      "App": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "version": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "kind"
        ]
      },
      "Feature": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "path": {
            "type": "string"
          },
          "kind": {
            "type": "string"
          },
          "severityFloor": {
            "type": "string"
          },
          "cues": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "code": {
            "type": "array",
            "items": {},
            "description": "Fichiers de code rattachés."
          },
          "openFeedbacks": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "name",
          "path",
          "openFeedbacks"
        ]
      },
      "StatusChange": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "triage",
              "prioritized",
              "in_development",
              "fixed",
              "delivered",
              "dismissed"
            ],
            "description": "`triage`, `prioritized`, `in_development`, `fixed`, `delivered` (prévient les personnes qui suivent le feedback) ou `dismissed`."
          },
          "comment": {
            "type": "string",
            "maxLength": 5000,
            "description": "Ajouté au feedback ; pour `dismissed`, la raison."
          }
        },
        "required": [
          "status"
        ]
      }
    },
    "parameters": {}
  },
  "paths": {
    "/v1/reports": {
      "post": {
        "tags": [
          "Collecte"
        ],
        "summary": "Envoyer un retour depuis un serveur",
        "description": "Crée un retour sur le canal API d'une app. À appeler depuis un serveur, jamais depuis un navigateur : quiconque connaît la clé peut envoyer des retours sur ce canal. Les URL autorisées de l'app ne s'appliquent pas à ce canal. Ni capture, ni replay, ni conversation : les champs inconnus sont ignorés. Le quota de feedbacks du plan s'applique. `POST /v1/ingest`, l'ancien nom de cette route, reste accepté.",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "maxLength": 255,
              "description": "Facultatif. Une valeur unique par retour (un UUID, par exemple). Une nouvelle tentative avec la même clé et le même corps renvoie la première réponse au lieu de créer un doublon, pendant 24 heures. La réponse rejouée porte l'en-tête `Idempotent-Replayed: true`.",
              "example": "7f1c2e9a-5b8d-4c3e-9a1f-2d6b8e4c1a7f"
            },
            "required": false,
            "description": "Facultatif. Une valeur unique par retour (un UUID, par exemple). Une nouvelle tentative avec la même clé et le même corps renvoie la première réponse au lieu de créer un doublon, pendant 24 heures. La réponse rejouée porte l'en-tête `Idempotent-Replayed: true`.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Avec `qualify` : une question à poser, ou la fin sans enregistrement.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QualificationStep"
                }
              }
            }
          },
          "201": {
            "description": "Retour créé. Avec `qualify`, quand il était déjà assez précis, la réponse porte aussi `id` et `status: \"ready\"`.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ReportResponse"
                    },
                    {
                      "$ref": "#/components/schemas/QualifiedReportResponse"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json` : corps illisible. `invalid_ingest` : champ manquant ou invalide. `invalid_idempotency_key` : clé vide ou trop longue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`channel_mismatch` : la clé n'est ni celle d'un canal API ni celle d'un formulaire. `channel_inactive` : canal désactivé.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`unknown_app` : clé inconnue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_in_progress` : une requête avec la même `Idempotency-Key` est encore en cours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`message_too_long` : message trop long. `payload_too_large` : corps de plus de 256 Ko.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`idempotency_key_reused` : la même `Idempotency-Key` a servi pour un autre corps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` : plus de 120 requêtes par minute depuis la même adresse IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`qualification_failed` : Galet AI n'a pas répondu ; renvoyez le retour sans `qualify`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/reports/{id}/answers": {
      "post": {
        "tags": [
          "Collecte"
        ],
        "summary": "Répondre à une question de qualification",
        "description": "Envoie la réponse de la personne à la question en attente. La réponse est soit la question suivante (200), soit le retour enregistré (201).",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "L'`id` renvoyé par l'étape précédente."
            },
            "required": true,
            "description": "L'`id` renvoyé par l'étape précédente.",
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QualificationAnswer"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Question suivante, ou fin sans enregistrement.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QualificationStep"
                }
              }
            }
          },
          "201": {
            "description": "Retour qualifié et enregistré.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QualifiedReportResponse"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`, `invalid_answer` : corps invalide. `missing_answer` : ni `text` ni `choiceIds`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`qualification_not_found` : identifiant inconnu, autre clé, ou qualification expirée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` : trop de requêtes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`qualification_failed` : Galet AI n'a pas répondu.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/feedbacks": {
      "get": {
        "tags": [
          "Lecture"
        ],
        "summary": "Lister les feedbacks",
        "description": "Les feedbacks de l'organisation, du plus récent au plus ancien, filtrés côté serveur et paginés par curseur.",
        "security": [
          {
            "bearer": [
              "feedbacks:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "closed",
                "all",
                "triage",
                "prioritized",
                "in_development",
                "fixed",
                "delivered",
                "dismissed"
              ],
              "description": "`open` (défaut), `closed`, `all` ou un statut précis."
            },
            "required": false,
            "description": "`open` (défaut), `closed`, `all` ou un statut précis.",
            "name": "status",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "problem",
                "idea",
                "praise"
              ]
            },
            "required": false,
            "name": "kind",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "P0",
                "P1",
                "P2",
                "P3"
              ]
            },
            "required": false,
            "name": "severity",
            "in": "query"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": false,
            "name": "appId",
            "in": "query"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": false,
            "name": "featureId",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "maxLength": 200,
              "description": "Texte cherché dans la clé, le titre, le résumé et les retours."
            },
            "required": false,
            "description": "Texte cherché dans la clé, le titre, le résumé et les retours.",
            "name": "query",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "description": "20 par défaut, 100 au plus."
            },
            "required": false,
            "description": "20 par défaut, 100 au plus.",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "Le `nextCursor` de la page précédente."
            },
            "required": false,
            "description": "Le `nextCursor` de la page précédente.",
            "name": "cursor",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Une page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeedbackPage"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_query` : filtre inconnu ou invalide. `invalid_cursor` : curseur illisible.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` : jeton absent. `invalid_token` : jeton inconnu, expiré ou révoqué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` : le jeton n'a pas le scope `feedbacks:read`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/feedbacks/{key}": {
      "get": {
        "tags": [
          "Lecture"
        ],
        "summary": "Lire un feedback",
        "description": "Un feedback, ses retours et, avec Silex, le contexte complet pour le corriger.",
        "security": [
          {
            "bearer": [
              "feedbacks:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "Clé lisible (FIF-12) ou identifiant."
            },
            "required": true,
            "description": "Clé lisible (FIF-12) ou identifiant.",
            "name": "key",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Le feedback.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Feedback"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` : jeton absent. `invalid_token` : jeton inconnu, expiré ou révoqué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` : le jeton n'a pas le scope `feedbacks:read`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` : aucun feedback avec cette clé dans l'organisation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Écriture"
        ],
        "summary": "Changer le statut d'un feedback",
        "description": "Même effet que depuis l'inbox, au nom de la personne qui a autorisé le jeton : le ticket Jira ou Linear lié suit, et `delivered` prévient les personnes qui suivent le feedback.",
        "security": [
          {
            "bearer": [
              "feedbacks:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "Clé lisible (FIF-12) ou identifiant."
            },
            "required": true,
            "description": "Clé lisible (FIF-12) ou identifiant.",
            "name": "key",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StatusChange"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Le feedback à jour.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Feedback"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`, `invalid_body` : corps invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` : jeton absent. `invalid_token` : jeton inconnu, expiré ou révoqué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` : le jeton n'a pas le scope `feedbacks:write`. `forbidden` : le rôle Lecteur ne peut pas modifier.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` : aucun feedback avec cette clé dans l'organisation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/apps": {
      "get": {
        "tags": [
          "Lecture"
        ],
        "summary": "Lister les apps",
        "security": [
          {
            "bearer": [
              "feedbacks:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Les apps de l'organisation.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/App"
                      }
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` : jeton absent. `invalid_token` : jeton inconnu, expiré ou révoqué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` : le jeton n'a pas le scope `feedbacks:read`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/apps/{id}/features": {
      "get": {
        "tags": [
          "Lecture"
        ],
        "summary": "Lister les fonctionnalités d'une app",
        "description": "La carte des fonctionnalités, à plat, avec le nombre de feedbacks ouverts sur chacune.",
        "security": [
          {
            "bearer": [
              "feedbacks:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Les fonctionnalités.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Feature"
                      }
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` : jeton absent. `invalid_token` : jeton inconnu, expiré ou révoqué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` : le jeton n'a pas le scope `feedbacks:read`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` : app inconnue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {}
}
