> ## 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.

# Publier le brouillon

> Valide le brouillon, crée un job de compilation et retourne immédiatement
`202 Accepted`. La compilation ne s'exécute **jamais** dans la requête HTTP :
un corpus important demande plusieurs lots LLM, bien au-delà de la limite de
la passerelle.

Le brouillon ne devient la configuration active qu'à la fin de la
compilation. Une ancienne version compilée reste lisible pour terminer les
sessions qui l'utilisent déjà.

Un second publish du même NBQ alors qu'un job est `queued` ou `running`
retourne `409 compilation_in_progress` avec le `compilation_id` en cours.

Suivre l'avancement avec
`GET /v1/configuration/compilations/{compilation_id}`.




## OpenAPI

````yaml /openapi.fr.yaml post /v1/configuration/publish
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/configuration/publish:
    post:
      tags:
        - configuration
      summary: Publier le brouillon
      description: >
        Valide le brouillon, crée un job de compilation et retourne
        immédiatement

        `202 Accepted`. La compilation ne s'exécute **jamais** dans la requête
        HTTP :

        un corpus important demande plusieurs lots LLM, bien au-delà de la
        limite de

        la passerelle.


        Le brouillon ne devient la configuration active qu'à la fin de la

        compilation. Une ancienne version compilée reste lisible pour terminer
        les

        sessions qui l'utilisent déjà.


        Un second publish du même NBQ alors qu'un job est `queued` ou `running`

        retourne `409 compilation_in_progress` avec le `compilation_id` en
        cours.


        Suivre l'avancement avec

        `GET /v1/configuration/compilations/{compilation_id}`.
      operationId: publishConfiguration
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublishRequest'
            examples:
              publication_simple:
                summary: Publier la révision courante du brouillon
                value: {}
              publication_verrouillee:
                summary: >-
                  Refuser de publier si le brouillon a bougé depuis la dernière
                  lecture
                value:
                  expected_draft_revision: 42
      responses:
        '202':
          description: Job de compilation créé.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompilationStatus'
              example:
                request_id: req_82bd
                compilation_id: cmp_01K2QF
                status: queued
                draft_revision: 42
                created_at: '2026-09-01T09:20:11Z'
                updated_at: '2026-09-01T09:20:11Z'
                progress: 0
                error: null
                configuration_version: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '409':
          $ref: '#/components/responses/PublishConflict'
        '422':
          $ref: '#/components/responses/ConfigurationValidationFailed'
        '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:
    PublishRequest:
      type: object
      additionalProperties: false
      properties:
        expected_draft_revision:
          type: integer
          minimum: 0
          description: >
            Refuse la publication si le brouillon a changé depuis cette
            révision.

            Recommandé depuis Studio pour éviter de publier le travail d'un
            autre

            éditeur.
    CompilationStatus:
      type: object
      additionalProperties: false
      required:
        - request_id
        - compilation_id
        - status
        - draft_revision
        - created_at
        - updated_at
        - progress
        - error
        - configuration_version
      properties:
        request_id:
          type: string
        compilation_id:
          type: string
          minLength: 1
          maxLength: 128
        status:
          type: string
          enum:
            - queued
            - running
            - succeeded
            - failed
        draft_revision:
          type: integer
          minimum: 0
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        progress:
          type: number
          minimum: 0
          maximum: 1
          description: Avancement indicatif, sans garantie de linéarité.
        error:
          oneOf:
            - $ref: '#/components/schemas/CompilationError'
            - type: 'null'
        configuration_version:
          type:
            - string
            - 'null'
          description: Version désormais active, renseignée uniquement en `succeeded`.
    CompilationError:
      type: object
      additionalProperties: false
      required:
        - code
        - message
      description: >
        Cause bornée d'un échec de compilation. Aucune trace interne, aucun
        prompt et

        aucun contenu de lot n'est exposé.


        `details` n'est renseigné que pour `validation_failed`, dont les erreurs
        sont

        corrigeables par l'éditeur ; pour `llm_unavailable`, `timeout` et

        `internal_error`, il reste absent.
      properties:
        code:
          type: string
          enum:
            - validation_failed
            - llm_unavailable
            - timeout
            - internal_error
        message:
          type: string
          minLength: 1
        details:
          type: array
          items:
            $ref: '#/components/schemas/ConfigurationIssue'
    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
    ConfigurationIssue:
      type: object
      additionalProperties: false
      required:
        - code
        - message
      description: >-
        Anomalie de configuration, structurée pour être affichée telle quelle
        dans Studio.
      properties:
        code:
          type: string
          minLength: 1
          maxLength: 100
          description: >
            Identifiant stable et lisible de l'anomalie, en minuscules avec des

            traits de soulignement.


            **Volontairement non figé en énumération.** Les validations de

            publication se stabiliseront avec NBQ-108, NBQ-306 et NBQ-311, en

            particulier celles liées à la génération des cibles d'exploration :

            fermer le catalogue maintenant imposerait une modification du
            contrat

            à chaque validation découverte, et certains codes envisagés se

            révéleraient prématurés. Il sera figé avant la génération des SDK.


            Codes déjà employés, à traiter comme un socle et non comme une liste

            exhaustive : `question_without_sub_objective`,

            `question_without_target`, `inactive_primary_question`,

            `success_information_without_schema`,

            `success_information_without_active_question`,

            `success_information_only_in_optional_sub_objective`,

            `invalid_choice_mapping`, `duplicate_id`, `unknown_reference`,

            `draft_revision_mismatch`.


            Un client doit afficher `message` et ne jamais présumer qu'il

            connaît tous les codes.
        message:
          type: string
          minLength: 1
        entity:
          type: string
          enum:
            - objective
            - sub_objective
            - success_information
            - question
        entity_id:
          type: string
          maxLength: 128
        change_index:
          type: integer
          minimum: 0
          description: >-
            Position de l'opération fautive dans `changes`, lorsque l'anomalie
            vient d'un changement.
    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
  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
    PublishConflict:
      description: >-
        Une compilation est déjà en cours, ou la clé d'idempotence a été
        réutilisée.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            compilation_in_progress:
              summary: Un job est déjà queued ou running pour ce NBQ
              value:
                code: compilation_in_progress
                message: Une compilation est déjà en cours pour cette configuration.
                request_id: req_9005
                details:
                  compilation_id: cmp_01K2QF
                  status: running
            idempotency_key_reused:
              summary: Même clé, corps différent
              value:
                code: idempotency_key_reused
                message: Cette clé d'idempotence est déjà associée à une autre requête.
                request_id: req_9006
                details:
                  idempotency_key: publish-rev-42
    ConfigurationValidationFailed:
      description: |
        La validation du brouillon a échoué. Toutes les anomalies sont renvoyées
        ensemble, avec la position de l'opération fautive lorsqu'elle vient d'un
        changement, afin que Studio puisse les afficher d'un seul coup.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            validation_publication:
              summary: Le brouillon n'est pas publiable
              value:
                code: configuration_validation_failed
                message: La configuration comporte 2 anomalies bloquantes.
                request_id: req_9015
                details:
                  issues:
                    - code: success_information_without_active_question
                      message: >-
                        L'information « Fenêtre de livraison souhaitée » n'a pas
                        de question active.
                      entity: success_information
                      entity_id: delivery_window
                    - code: success_information_only_in_optional_sub_objective
                      message: >-
                        L'information « Budget annuel » ne dépend que d'un
                        sous-objectif optionnel.
                      entity: success_information
                      entity_id: annual_budget
            revision_perimee:
              summary: Le brouillon a changé depuis la lecture du client
              value:
                code: configuration_validation_failed
                message: Le brouillon a été modifié depuis votre dernière lecture.
                request_id: req_9016
                details:
                  issues:
                    - code: draft_revision_mismatch
                      message: Révision attendue 42, révision courante 44.
                      entity: objective
    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`.

````