> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zelinqa.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Créer une session

> Crée une session et l'épingle en interne sur la version de configuration
publiée au moment de l'appel. La création **ne renvoie pas encore de
candidats** : appeler `POST /v1/sessions/{session_id}/next` ensuite.

`initial_history` sert uniquement à reprendre une conversation commencée
ailleurs — migration d'un intégrateur 0.9, ou échanges antérieurs à
l'activation de NBQ. Il est consommé **une seule fois** en mémoire pour
construire l'état initial, puis n'est persisté ni dans l'état, ni dans le
journal, ni dans les logs applicatifs, et n'est jamais renvoyé. Si le
tracing LLM est activé par Zelinqa, le prompt peut apparaître dans
LangSmith EU selon la politique de diagnostic et de rétention V1.




## OpenAPI

````yaml /openapi.fr.yaml post /v1/sessions
openapi: 3.1.0
info:
  title: NBQ Engine — API V1
  version: 1.0.0
  summary: Contrat public du runtime NBQ et de la configuration des bases de questions.
  description: >
    Contrat public de NBQ Engine V1, servi sur `https://api.zelinqa.ai`.


    Ce document est la **source de vérité** pour `nbq-sdk` (Python et
    TypeScript) et

    pour `nbq-mcp`. Les SDK et le serveur MCP sont générés ou alignés depuis ce

    contrat, jamais l'inverse.


    ## Pourquoi V1 succède à 0.9


    La version publique déployée aujourd'hui est la bêta `0.9`, déjà servie sous
    le

    préfixe `/v1`. La V1 **ajoute** ses routes sous le même préfixe sans en
    modifier

    aucune : `POST /v1/next-questions` et `POST
    /v1/sessions/{session_id}/conversion`

    restent fonctionnelles et sont marquées dépréciées. La correspondance
    complète

    est décrite dans `docs/api-compat-0.9-to-v1.md`.


    ## Principes contractuels


    - **Le tenant n'est jamais fourni par le client.** L'authorizer de l'API
    Gateway
      publique résout la clé, puis injecte le tenant, le NBQ et les scopes. Tout
      header `x-tenant-id`, `x-nbq-id` ou `x-scopes` présent dans la requête entrante
      est écrasé.
    - **Toute mutation exige `Idempotency-Key`.** Même clé et même corps
    renvoient la
      réponse d'origine ; même clé et corps différent produisent
      `idempotency_key_reused`.
    - **Toute mutation d'une session existante exige `state_version`**, en
    contrôle
      optimiste.
    - **Les identifiants du tour précédent sont optionnels.** `decision_id`,
      `question_id` et `outcome` aident NBQ ; leur absence n'est pas une erreur, le
      moteur les résout depuis la décision en attente et les messages observés. Une
      valeur explicite du client reste prioritaire sur l'inférence.
    - **Le verbatim est optionnel.** `user_text` peut être omis quand une
    réponse
      structurée ou des mises à jour client suffisent : l'extraction sémantique est
      alors dégradée, signalée par `degraded_reasons`, mais le moteur reste
      fonctionnel.
    - **Aucun interne n'est exposé.** Ni score de sélection, ni preuve
    sémantique
      détaillée, ni embedding, ni prompt, ni prototype de réponse, ni matrice
      compilée n'apparaissent dans une réponse publique.
    - **Le client ne choisit pas la version de configuration.** La session
    épingle
      en interne la version publiée active au moment de sa création.

    ## Arrêts souples


    NBQ ne décide pas à la place de l'appelant. Lorsque `max_turns` est atteint,
    que

    l'objectif est déjà atteint ou que l'éligibilité normale est vide, `/next`

    renvoie **encore la meilleure question disponible** et signale la situation
    dans

    `warnings`. Un `action: stop` n'est produit que lorsqu'aucune question

    identifiable n'existe réellement. Les exclusions dures — question inactive,

    sous-objectif exclu par le client, contrainte stricte de l'appel — ne sont
    jamais

    violées, et toute question retournée porte un `question_id` valide.


    ## Scopes de clé API


    Une clé porte un ou plusieurs scopes. Studio recommande des clés séparées
    pour

    le runtime et pour la gestion, selon le principe du moindre privilège.


    | Scope | Autorise |

    |---|---|

    | `runtime` | les cinq routes de session |

    | `configuration:read` | `GET /v1/configuration` (publiée) et `GET
    /v1/configuration/questions` |

    | `configuration:write` | `POST /v1/configuration/changes` et la lecture du
    brouillon |


    Deux niveaux de contrôle se complètent. L'authorizer décide
    **statiquement**,

    à partir de la méthode et du chemin : c'est lui qui refuse une clé `runtime`

    sur `/v1/configuration/publish`. Le service ajoute un contrôle **dynamique**

    là où le scope dépend du contenu de la requête, que l'authorizer ne voit pas
    :

    aujourd'hui le seul cas est `?state=draft`, qui exige `configuration:write`

    sur une route dont l'authorizer n'exige que `configuration:read`.

    | `configuration:publish` | `POST /v1/configuration/publish` |


    Chaque opération déclare le scope exigé dans `x-required-scopes`. Un scope

    manquant produit `403 insufficient_scope`.


    ## Frontière de confiance


    L'API Configuration est implémentée par le service `bo-api` mais atteignable
    par

    deux portes qui n'ont pas la même confiance :


    - via l'**API Gateway publique** (`6nw77hkcib`, `api.zelinqa.ai`),
    l'authorizer
      Lambda authentifie la clé et **écrase** `x-tenant-id` et `x-scopes` ; ces
      headers font alors autorité ;
    - via l'**API Gateway bo-api** (`jdkpi8qlv5`, `bo-api.zelinqa.ai`),
    l'authentification
      vient des guards NestJS et du JWT Cognito ; **tous** les headers `x-tenant-id`
      et `x-scopes` fournis par l'appelant sont ignorés.

    Le service distingue obligatoirement l'origine via `requestContext.apiId`
    avant

    de construire son contexte d'autorisation. Un JWT valide sur la porte bo-api
    ne

    peut jamais obtenir un scope de publication en forgeant un header `x-*`.

    Ce document ne décrit que la surface publique `api.zelinqa.ai`.
  contact:
    name: Zelinqa
    url: https://docs.zelinqa.ai
  license:
    name: Proprietary — Zelinqa SAS
    identifier: LicenseRef-Zelinqa-Proprietary
  x-contract-status: frozen
  x-jira: NBQ-304
  x-supersedes: openapi/nbq-runtime-v2.openapi.yaml
  x-compatibility-note: docs/api-compat-0.9-to-v1.md
servers:
  - url: https://api.zelinqa.ai
    description: API publique NBQ
security:
  - ApiKeyAuth: []
tags:
  - name: sessions
    description: |
      Cycle de vie d'une conversation NBQ : création, sélection de la question
      suivante, mises à jour hors tour, reprise et retour de résultat.
  - name: configuration
    description: |
      Lecture et modification de la configuration éditoriale d'un NBQ, puis
      publication asynchrone de l'artefact moteur.
paths:
  /v1/sessions:
    post:
      tags:
        - sessions
      summary: Créer une session
      description: >
        Crée une session et l'épingle en interne sur la version de configuration

        publiée au moment de l'appel. La création **ne renvoie pas encore de

        candidats** : appeler `POST /v1/sessions/{session_id}/next` ensuite.


        `initial_history` sert uniquement à reprendre une conversation commencée

        ailleurs — migration d'un intégrateur 0.9, ou échanges antérieurs à

        l'activation de NBQ. Il est consommé **une seule fois** en mémoire pour

        construire l'état initial, puis n'est persisté ni dans l'état, ni dans
        le

        journal, ni dans les logs applicatifs, et n'est jamais renvoyé. Si le

        tracing LLM est activé par Zelinqa, le prompt peut apparaître dans

        LangSmith EU selon la politique de diagnostic et de rétention V1.
      operationId: createSession
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SessionCreateRequest'
            examples:
              minimal:
                summary: Création nominale
                value: {}
              avec_reference_client:
                summary: Corrélation avec un identifiant de l'intégrateur
                value:
                  client_reference: crm-lead-8842
                  max_turns: 10
              reprise_conversation:
                summary: Conversation déjà commencée hors NBQ
                value:
                  client_reference: crm-lead-8843
                  initial_history:
                    - role: assistant
                      message_id: msg_1
                      text: Bonjour, que recherchez-vous ?
                    - role: user
                      message_id: msg_2
                      text: Un canapé pour mon salon, plutôt contemporain.
      responses:
        '201':
          description: Session créée.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionStateResponse'
              examples:
                session_neuve:
                  $ref: '#/components/examples/SessionNeuve'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '409':
          $ref: '#/components/responses/IdempotencyConflict'
        '410':
          $ref: '#/components/responses/CompiledArtifactUnavailable'
        '422':
          $ref: '#/components/responses/InvalidChoice'
        '503':
          $ref: '#/components/responses/IdempotencyContention'
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        minLength: 8
        maxLength: 128
      description: >
        Clé unique par mutation logique, choisie par l'appelant.


        Même clé et même corps renvoient exactement la réponse d'origine, sans

        rejouer l'effet : aucun retry ne double `turn_count`, un outcome, un

        événement, un feedback ou une publication. Même clé avec un corps
        différent

        produit `409 idempotency_key_reused`.


        Les enregistrements sont isolés par tenant et par opération, et sont
        purgés

        après **24 heures**. Passé ce délai, la même clé est traitée comme
        neuve.
  schemas:
    SessionCreateRequest:
      type: object
      additionalProperties: false
      description: |
        Le client ne choisit ni la version de configuration, ni une politique
        d'accusé de réception : la session épingle en interne la version publiée
        active au moment de sa création.
      properties:
        client_reference:
          type: string
          minLength: 1
          maxLength: 128
          description: >-
            Référence opaque de l'intégrateur, renvoyée telle quelle pour
            corrélation.
        max_turns:
          type: integer
          minimum: 1
          maximum: 100
          description: >
            Surcharge la limite souple définie dans la configuration. Atteindre
            cette

            limite ne coupe pas la conversation : `/next` continue de proposer
            la

            meilleure question avec un avertissement.
        initial_history:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/InitialHistoryItem'
          description: >
            Historique de reprise, consommé une seule fois en mémoire pour
            construire

            l'état initial. Jamais persisté, jamais renvoyé.
    SessionStateResponse:
      type: object
      additionalProperties: false
      required:
        - request_id
        - session_id
        - client_reference
        - status
        - turn_count
        - max_turns
        - turns_remaining
        - question_state
        - targets
        - progress
        - pending_decision
        - degraded
        - versions
      properties:
        request_id:
          type: string
        session_id:
          type: string
        client_reference:
          type:
            - string
            - 'null'
        status:
          type: string
          enum:
            - active
            - completed
            - stopped
        turn_count:
          type: integer
          minimum: 0
        max_turns:
          type: integer
          minimum: 1
        turns_remaining:
          type: integer
          minimum: 0
        question_state:
          $ref: '#/components/schemas/PublicQuestionState'
        targets:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/PublicTargetState'
          description: >
            État sparse : seules les cibles réellement touchées sont présentes.
            Une

            cible absente est inconnue et sans couverture.
        progress:
          $ref: '#/components/schemas/ProgressView'
        pending_decision:
          oneOf:
            - $ref: '#/components/schemas/PendingDecisionView'
            - type: 'null'
        degraded:
          type: boolean
        versions:
          $ref: '#/components/schemas/VersionInfo'
    InitialHistoryItem:
      type: object
      additionalProperties: false
      required:
        - role
      properties:
        role:
          type: string
          enum:
            - user
            - assistant
        message_id:
          type: string
          minLength: 1
          maxLength: 128
          description: Référence conservée même lorsque le verbatim n'est pas persisté.
        question_id:
          type: string
          minLength: 1
          maxLength: 128
          description: >-
            Renseigné seulement si ce message correspond à une question connue
            du corpus.
        text:
          type: string
          minLength: 1
          maxLength: 8000
        structured_answer:
          $ref: '#/components/schemas/StructuredAnswer'
        occurred_at:
          type: string
          format: date-time
      anyOf:
        - required:
            - text
        - required:
            - structured_answer
    PublicQuestionState:
      type: object
      additionalProperties: false
      required:
        - outcomes
      description: >
        Aucun index `asked_ids`, `answered_ids` ou `refused_ids` n'est persisté
        ni

        exposé : ces ensembles se reconstruisent depuis `outcomes`.
      properties:
        outcomes:
          type: array
          items:
            $ref: '#/components/schemas/QuestionOutcomeRecord'
    PublicTargetState:
      type: object
      additionalProperties: false
      required:
        - kind
        - coverage
      description: >
        Vue publique d'une cible. Les preuves sémantiques détaillées, les deltas
        de

        couverture et les références d'embedding restent internes au

        moteur et ne sont jamais renvoyés.
      properties:
        kind:
          type: string
          enum:
            - data
            - exploration
          description: >
            `data` désigne une information de réussite : une donnée concrète
            dont la

            collecte permet de déclarer l'objectif atteint.


            `exploration` est la cible interne d'une question qui n'est reliée à

            aucune information de réussite. Elle mesure ce que la conversation a
            déjà

            couvert et évite les répétitions, mais ne bloque jamais la réussite
            de

            l'objectif. Les nouvelles publications sans prototypes évaluent
            directement

            les réponses réelles. Les sessions épinglées à un ancien artefact

            conservent leur méthode de couverture historique.
        status:
          type: string
          enum:
            - tentative
            - confirmed
            - conflicted
            - not_applicable
          description: >
            Présent uniquement pour `kind: data`. Une cible inconnue est absente
            de

            l'état plutôt que portée avec un statut `unknown`.


            `tentative` porte une valeur probable : elle fait monter `coverage`

            mais ne permet jamais de conclure ;

            `conflicted` signale plusieurs valeurs valides contradictoires ;

            `not_applicable` satisfait l'information sans valeur.
        value:
          description: >-
            Présente uniquement pour une information de réussite possédant une
            valeur courante.
        coverage:
          type: number
          minimum: 0
          maximum: 1
          description: >
            Ce que la conversation a déjà obtenu sur cette cible.


            Pour une information de réussite : `1` quand elle est `confirmed` ou

            `not_applicable`, `0` quand rien n'a été recueilli, et une valeur

            **intermédiaire** quand le statut est `tentative` — une valeur

            probable a été captée mais n'est pas encore assez fiable pour

            conclure. Une donnée `conflicted` retombe à `0` : des valeurs

            contradictoires n'apportent aucune certitude.


            Pour une cible d'exploration sans prototypes : meilleur soutien
            actif

            à la cible, dérivé des réponses réelles ; une réponse suffisante
            vaut

            `1`, sans devoir couvrir plusieurs exemples alternatifs. Une
            correction

            explicite peut réduire cette couverture. Pour les anciennes
            publications,

            le calcul historique sur les prototypes reste inchangé.


            ⚠️ `coverage` sert à mesurer l'avancement et à classer les
            questions,

            **jamais à décider de la réussite**. La complétion d'un

            sous-objectif se calcule à partir de `status`, où seuls `confirmed`

            et `not_applicable` comptent : une information `tentative` fait

            monter la progression visible sans jamais permettre de déclarer

            l'objectif atteint. Les deux grandeurs se ressemblent et ne se

            substituent pas l'une à l'autre.
    ProgressView:
      type: object
      additionalProperties: false
      required:
        - objective
        - sub_objectives
      properties:
        objective:
          $ref: '#/components/schemas/ObjectiveProgress'
        sub_objectives:
          type: array
          items:
            $ref: '#/components/schemas/SubObjectiveProgress'
    PendingDecisionView:
      type: object
      additionalProperties: false
      required:
        - decision_id
        - candidates
      description: >
        Décision proposée et pas encore résolue. Les candidats sont réhydratés
        depuis

        la configuration épinglée : après un crash, relire la session suffit
        pour

        reprendre exactement où l'on s'était arrêté. Aucun jeton de reprise
        n'existe.
      properties:
        decision_id:
          type: string
        candidates:
          type: array
          minItems: 1
          maxItems: 10
          items:
            $ref: '#/components/schemas/Candidate'
    VersionInfo:
      type: object
      additionalProperties: false
      required:
        - state_version
        - engine_version
        - api_version
      description: >
        La version de configuration épinglée par la session n'est pas exposée :
        le

        client ne la choisit pas et ne doit pas s'y adosser.
      properties:
        state_version:
          type: integer
          minimum: 0
          description: À renvoyer dans la mutation suivante de cette session.
        engine_version:
          type: string
        api_version:
          type: string
          const: '1.0'
    ErrorEnvelope:
      type: object
      additionalProperties: false
      required:
        - code
        - message
        - request_id
        - details
      description: Enveloppe uniforme de toutes les erreurs métier.
      properties:
        code:
          $ref: '#/components/schemas/ErrorCode'
        message:
          type: string
          minLength: 1
        request_id:
          type: string
          minLength: 1
        details:
          type: object
          additionalProperties: true
    StructuredAnswer:
      type: object
      additionalProperties: false
      required:
        - choice_ids
      description: >
        Réponse à une question à choix. Le mapping choix vers information de
        réussite

        est déterministe et compilé à la publication : aucun appel LLM n'est
        effectué

        pour l'interpréter.
      properties:
        choice_ids:
          type: array
          minItems: 1
          maxItems: 50
          uniqueItems: true
          items:
            type: string
            minLength: 1
            maxLength: 128
        free_text:
          type: string
          minLength: 1
          maxLength: 4000
          description: Complément libre, autorisé uniquement pour une question `semi_open`.
    QuestionOutcomeRecord:
      type: object
      additionalProperties: false
      required:
        - question_id
        - decision_id
        - outcome
        - source
      properties:
        question_id:
          type: string
        decision_id:
          type:
            - string
            - 'null'
          description: Nul lorsque la question a été traitée hors d'une décision NBQ.
        outcome:
          $ref: '#/components/schemas/QuestionOutcome'
        source:
          type: string
          enum:
            - client
            - inferred
          description: Indique si l'outcome a été fourni par le client ou résolu par NBQ.
        message_id:
          type:
            - string
            - 'null'
        occurred_at:
          type:
            - string
            - 'null'
          format: date-time
    ObjectiveProgress:
      type: object
      additionalProperties: false
      required:
        - computed_status
        - progress
        - client_override
        - effective_status
      properties:
        computed_status:
          $ref: '#/components/schemas/ProgressStatus'
        progress:
          type: number
          minimum: 0
          maximum: 1
          description: >
            Agrégation des sous-objectifs pondérée par leur rôle de complétion.
            Les

            sous-objectifs exclus sortent du dénominateur.
        client_override:
          oneOf:
            - $ref: '#/components/schemas/ObjectiveClientOverrideView'
            - type: 'null'
        effective_status:
          $ref: '#/components/schemas/ProgressStatus'
    SubObjectiveProgress:
      type: object
      additionalProperties: false
      required:
        - id
        - order_position
        - completion_role
        - computed_status
        - progress
        - client_override
        - effective_status
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 128
        order_position:
          type: integer
          minimum: 0
        completion_role:
          $ref: '#/components/schemas/CompletionRole'
        computed_status:
          $ref: '#/components/schemas/ProgressStatus'
        progress:
          type: number
          minimum: 0
          maximum: 1
        client_override:
          oneOf:
            - $ref: '#/components/schemas/SubObjectiveClientOverrideView'
            - type: 'null'
        effective_status:
          type: string
          enum:
            - not_started
            - in_progress
            - covered
            - blocked
            - excluded
          description: >
            Statut consommable par le client après application de
            `client_override`

            au `computed_status` : sans override, le statut calculé ; `achieved`

            force `covered` ; `not_achieved` conserve le statut calculé sauf que

            `covered` redevient `in_progress` ; `excluded` retire le
            sous-objectif de

            la sélection et des agrégations de l'objectif.
    Candidate:
      type: object
      additionalProperties: false
      required:
        - rank
        - question_id
        - text
        - type
        - choices
        - target_ids
      description: |
        **Invariant garanti par le moteur**, couvert par les tests de contrat :
        `type: open` implique `choices` vide ; tout autre type implique au moins
        deux choix.
      properties:
        rank:
          type: integer
          minimum: 1
          maximum: 10
        question_id:
          type: string
          minLength: 1
          maxLength: 128
          description: >-
            Toujours un identifiant valide du corpus publié, y compris pour un
            repli.
        text:
          type: string
          minLength: 1
          maxLength: 4000
          description: >
            Libellé publié de la question. L'agent hôte reste libre de le
            reformuler :

            NBQ sait rattacher un texte reformulé à sa question au tour suivant.
        type:
          $ref: '#/components/schemas/QuestionType'
        choices:
          type: array
          maxItems: 50
          items:
            $ref: '#/components/schemas/CandidateChoice'
          description: Vide pour une question ouverte.
        target_ids:
          type: array
          minItems: 1
          uniqueItems: true
          items:
            type: string
            minLength: 1
            maxLength: 128
          description: Cibles que cette question adresse, principale en premier.
    ErrorCode:
      type: string
      description: Catalogue fermé des erreurs métier V1.
      enum:
        - unauthorized
        - insufficient_scope
        - idempotency_contention
        - state_version_conflict
        - idempotency_key_reused
        - unknown_session
        - invalid_previous_turn
        - constraint_no_match
        - invalid_choice
        - compiled_artifact_unavailable
        - configuration_validation_failed
        - compilation_in_progress
        - unknown_configuration
        - unknown_compilation
    QuestionOutcome:
      type: string
      enum:
        - asked_answered
        - asked_no_answer
        - refused
      description: >
        Résultat d'une question effectivement posée.


        - `asked_answered` : question posée et réponse exploitable reçue ;

        - `asked_no_answer` : question posée, aucune réponse exploitable ;

        - `refused` : refus explicite de répondre.


        Une question jamais posée n'a pas de résultat : elle est simplement
        absente.

        Il n'existe pas de valeur `skipped` — une proposition que l'agent n'a
        pas

        utilisée est journalisée comme décision ignorée, jamais comme un faux

        résultat sur la question.
    ProgressStatus:
      type: string
      enum:
        - not_started
        - in_progress
        - covered
        - blocked
    ObjectiveClientOverrideView:
      type: object
      additionalProperties: false
      required:
        - status
      properties:
        status:
          $ref: '#/components/schemas/ObjectiveOverrideValue'
        updated_at:
          type: string
          format: date-time
    CompletionRole:
      type: string
      enum:
        - blocking
        - contributing
        - optional
      description: >
        Rôle d'un sous-objectif dans la réussite de l'objectif.


        - `blocking` : doit être couvert pour conclure normalement ;

        - `contributing` : participe au niveau de qualification exigé ;

        - `optional` : améliore la conversation mais ne bloque jamais la
        réussite.


        Le poids moteur est dérivé du rôle. Le client ne saisit aucun poids
        métier.
    SubObjectiveClientOverrideView:
      type: object
      additionalProperties: false
      required:
        - status
      properties:
        status:
          $ref: '#/components/schemas/SubObjectiveOverrideValue'
        updated_at:
          type: string
          format: date-time
    QuestionType:
      type: string
      enum:
        - open
        - single_choice
        - multiple_choice
        - semi_open
      description: |
        - `open` : réponse libre, aucun choix ;
        - `single_choice` : un seul choix parmi la liste ;
        - `multiple_choice` : plusieurs choix possibles ;
        - `semi_open` : choix proposés, complément libre autorisé via
          `structured_answer.free_text`.

        Le mode de sélection est porté par le type lui-même : aucun champ
        `selection_mode` séparé n'existe en V1.
    CandidateChoice:
      type: object
      additionalProperties: false
      required:
        - choice_id
        - label
      properties:
        choice_id:
          type: string
          minLength: 1
          maxLength: 128
        label:
          type: string
          minLength: 1
          maxLength: 500
    ObjectiveOverrideValue:
      type: string
      enum:
        - achieved
        - not_achieved
    SubObjectiveOverrideValue:
      type: string
      enum:
        - achieved
        - not_achieved
        - excluded
      description: >
        `achieved` force le sous-objectif à couvert. `not_achieved` empêche une

        complétion calculée prématurée. `excluded` le retire de la sélection et
        des

        dénominateurs de progression de l'objectif.
  examples:
    SessionNeuve:
      summary: Session tout juste créée, aucun tour joué
      value:
        request_id: req_1001
        session_id: ses_01J8Z
        client_reference: crm-lead-8842
        status: active
        turn_count: 0
        max_turns: 10
        turns_remaining: 10
        question_state:
          outcomes: []
        targets: {}
        progress:
          objective:
            computed_status: not_started
            progress: 0
            client_override: null
            effective_status: not_started
          sub_objectives:
            - id: so_besoin
              order_position: 0
              completion_role: blocking
              computed_status: not_started
              progress: 0
              client_override: null
              effective_status: not_started
            - id: so_budget
              order_position: 1
              completion_role: blocking
              computed_status: not_started
              progress: 0
              client_override: null
              effective_status: not_started
            - id: so_livraison
              order_position: 2
              completion_role: contributing
              computed_status: not_started
              progress: 0
              client_override: null
              effective_status: not_started
        pending_decision: null
        degraded: false
        versions:
          state_version: 0
          engine_version: 1.0.0
          api_version: '1.0'
  responses:
    Unauthorized:
      description: >
        Clé absente, malformée, inconnue, révoquée, expirée, ou rattachée à un
        NBQ

        suspendu. Le refus vient de l'authorizer de la passerelle, avant que la

        requête n'atteigne le service : le corps est donc produit par la
        passerelle

        et peut ne pas suivre l'enveloppe d'erreur métier.


        La distinction avec `403` est nette : `401` signifie « je ne sais pas
        qui

        vous êtes », `403` signifie « je sais qui vous êtes, mais cette clé ne
        porte

        pas le scope requis ».
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            code: unauthorized
            message: Clé d'intégration absente ou invalide.
            request_id: req_9000
            details: {}
    InsufficientScope:
      description: >
        La clé est valide mais ne porte pas le scope exigé par l'opération. Une
        clé

        absente ou invalide produit `401` au niveau de la passerelle, avant
        d'atteindre

        le service.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            code: insufficient_scope
            message: Cette clé ne permet pas de publier une configuration.
            request_id: req_9001
            details:
              required_scopes:
                - configuration:publish
              granted_scopes:
                - configuration:read
                - configuration:write
    IdempotencyConflict:
      description: La clé d'idempotence a déjà servi pour un corps différent.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            code: idempotency_key_reused
            message: Cette clé d'idempotence est déjà associée à une autre requête.
            request_id: req_9002
            details:
              idempotency_key: create-session-8842
    CompiledArtifactUnavailable:
      description: >
        L'artefact compilé épinglé par la session est réellement inaccessible.
        Une

        configuration simplement remplacée par une version plus récente ne
        produit pas

        cette erreur : l'ancienne version reste lisible pour terminer les
        sessions qui

        l'utilisent déjà.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            code: compiled_artifact_unavailable
            message: >-
              L'artefact moteur de cette session est temporairement
              indisponible.
            request_id: req_9010
            details:
              session_id: ses_01J8Z
    InvalidChoice:
      description: Une valeur fournie n'appartient pas au schéma ou aux choix publiés.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            code: invalid_choice
            message: >-
              La valeur fournie n'est pas valide pour cette information de
              réussite.
            request_id: req_9014
            details:
              data_id: delivery_window
              invalid_value: dans_deux_ans
    IdempotencyContention:
      description: >
        La clé d'idempotence n'a pas pu être réservée après deux tentatives, à
        cause

        d'un cycle anormal de requêtes concurrentes portant la même clé.


        Ce n'est **pas** une erreur du client et ce n'est pas un conflit :
        réessayer

        après un court délai a des chances d'aboutir. À distinguer de

        `idempotency_key_reused`, qui est définitif pour ce couple clé/corps.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            code: idempotency_contention
            message: La clé d'idempotence n'a pas pu être réservée, réessayez.
            request_id: req_9017
            details:
              idempotency_key: next-ses01J8Z-turn-4
              retry_after_seconds: 1
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: NBQ API key
      description: >
        Clé d'intégration préfixée `nbq_live_`, envoyée dans

        `Authorization: Bearer nbq_live_…` et résolue par l'authorizer Lambda de

        l'API Gateway publique. L'authorizer valide la clé, puis **injecte** en
        amont

        du service les headers de contexte `x-tenant-id`, `x-nbq-id` et
        `x-scopes` —

        `x-scopes` étant la liste des scopes de la clé séparés par des
        **virgules**,

        par exemple `runtime,configuration:read`.


        Ces headers ne sont jamais acceptés depuis le client : toute valeur
        entrante

        est écrasée. Une clé absente ou invalide produit `401` au niveau de la

        passerelle ; une clé valide sans le scope requis produit `403` avec le
        code

        `insufficient_scope`.

````