{
  "openapi": "3.1.0",
  "info": {
    "title": "HALTE NUISIBLES : devis instantané et réservation",
    "version": "1.0.0",
    "summary": "Prix instantané et rendez-vous pour une dératisation ou une désinsectisation.",
    "description": "API publique de HALTE NUISIBLES pour les assistants IA et les applications : questionnaire, prix instantané TTC, identification d'un nuisible sur photo, créneaux libres et réservation.\n\nMarche à suivre : 1) GET /api/agent/questionnaire ; 2) POST /api/agent/estimer après chaque réponse du client, jusqu'à `complet` vrai ; 3) annoncer le prix TTC ; 4) POST /api/agent/creneaux ; 5) recueillir coordonnées et adresse ; 6) AVANT POST /api/agent/reserver, annoncer au client le prix TTC et le créneau, obtenir son accord explicite et son consentement RGPD (`consentement_rgpd`).\n\nAprès la réservation, le client reçoit le devis à signer par e-mail ; le rendez-vous n'est définitif qu'après sa signature électronique. Aucun paiement n'est pris par l'API.\n\nLimites par adresse IP et par heure : identifier-photo 10, reserver 5, autres routes 120. Les prix viennent toujours de l'ERP : ne les modifiez jamais.\n\nLe même service est disponible en MCP (Streamable HTTP) : https://halte-nuisibles.fr/mcp",
    "contact": {
      "name": "HALTE NUISIBLES",
      "url": "https://halte-nuisibles.fr/contact",
      "email": "contact@halte-nuisibles.fr"
    }
  },
  "servers": [
    {
      "url": "https://halte-nuisibles.fr"
    }
  ],
  "externalDocs": {
    "description": "Réserver depuis un assistant IA",
    "url": "https://halte-nuisibles.fr/assistants-ia"
  },
  "tags": [
    {
      "name": "Réservation",
      "description": "Devis instantané et rendez-vous"
    }
  ],
  "paths": {
    "/api/agent/questionnaire": {
      "get": {
        "operationId": "questionnaire",
        "tags": [
          "Réservation"
        ],
        "summary": "Nuisibles pris en charge et questions du devis",
        "description": "À appeler en premier. Les questions sont à poser au client une par une.",
        "responses": {
          "200": {
            "description": "Questionnaire",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Erreur"
          },
          "409": {
            "$ref": "#/components/responses/Erreur"
          },
          "422": {
            "$ref": "#/components/responses/Erreur"
          },
          "429": {
            "$ref": "#/components/responses/Erreur"
          },
          "502": {
            "$ref": "#/components/responses/Erreur"
          },
          "503": {
            "$ref": "#/components/responses/Erreur"
          }
        }
      }
    },
    "/api/agent/zone": {
      "post": {
        "operationId": "verifierZone",
        "tags": [
          "Réservation"
        ],
        "summary": "Vérifier qu'un code postal est dans la zone d'intervention",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "code_postal"
                ],
                "properties": {
                  "code_postal": {
                    "type": "string",
                    "pattern": "^\\d{5}$"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Zone",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Zone"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Erreur"
          },
          "409": {
            "$ref": "#/components/responses/Erreur"
          },
          "422": {
            "$ref": "#/components/responses/Erreur"
          },
          "429": {
            "$ref": "#/components/responses/Erreur"
          },
          "502": {
            "$ref": "#/components/responses/Erreur"
          },
          "503": {
            "$ref": "#/components/responses/Erreur"
          }
        }
      }
    },
    "/api/agent/estimer": {
      "post": {
        "operationId": "estimerPrix",
        "tags": [
          "Réservation"
        ],
        "summary": "Prix instantané TTC à partir des réponses du client",
        "description": "À rappeler après chaque réponse. `questions` donne les questions à poser, `manquantes` ce qui empêche le calcul. `mode` vaut `devis` (prix ferme) ou `diagnostic` (diagnostic payant sur place).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "reponses"
                ],
                "properties": {
                  "reponses": {
                    "$ref": "#/components/schemas/Reponses"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Estimation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Estimation"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Erreur"
          },
          "409": {
            "$ref": "#/components/responses/Erreur"
          },
          "422": {
            "$ref": "#/components/responses/Erreur"
          },
          "429": {
            "$ref": "#/components/responses/Erreur"
          },
          "502": {
            "$ref": "#/components/responses/Erreur"
          },
          "503": {
            "$ref": "#/components/responses/Erreur"
          }
        }
      }
    },
    "/api/agent/creneaux": {
      "post": {
        "operationId": "creneauxDisponibles",
        "tags": [
          "Réservation"
        ],
        "summary": "Créneaux d'intervention libres (6 au plus)",
        "description": "Exige une estimation complète. Aucun nom de technicien n'est renvoyé.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "reponses"
                ],
                "properties": {
                  "reponses": {
                    "$ref": "#/components/schemas/Reponses"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Créneaux",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Creneaux"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Erreur"
          },
          "409": {
            "$ref": "#/components/responses/Erreur"
          },
          "422": {
            "$ref": "#/components/responses/Erreur"
          },
          "429": {
            "$ref": "#/components/responses/Erreur"
          },
          "502": {
            "$ref": "#/components/responses/Erreur"
          },
          "503": {
            "$ref": "#/components/responses/Erreur"
          }
        }
      }
    },
    "/api/agent/identifier-photo": {
      "post": {
        "operationId": "identifierPhoto",
        "tags": [
          "Réservation"
        ],
        "summary": "Identifier un nuisible sur 1 à 3 photos",
        "description": "Chaque image est une URL https publique (JPEG, PNG ou WebP, 5 Mo maximum) ou du base64. Rien n'est conservé. Confiance faible ou photo inexploitable : nuisible `inconnu` (diagnostic sur place). Limité à 10 appels par heure et par adresse IP.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "images"
                ],
                "properties": {
                  "images": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 3,
                    "items": {
                      "$ref": "#/components/schemas/ImageEntree"
                    }
                  },
                  "description": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 1000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Identification",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Identification"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Erreur"
          },
          "409": {
            "$ref": "#/components/responses/Erreur"
          },
          "422": {
            "$ref": "#/components/responses/Erreur"
          },
          "429": {
            "$ref": "#/components/responses/Erreur"
          },
          "502": {
            "$ref": "#/components/responses/Erreur"
          },
          "503": {
            "$ref": "#/components/responses/Erreur"
          }
        }
      }
    },
    "/api/agent/reserver": {
      "post": {
        "operationId": "reserverIntervention",
        "tags": [
          "Réservation"
        ],
        "summary": "Réserver un créneau et créer le devis à signer",
        "description": "ACTION ENGAGEANTE. À n'appeler qu'après avoir annoncé au client le prix TTC et le créneau, obtenu son accord explicite et son consentement RGPD. Le client reçoit le devis à signer par e-mail ; le rendez-vous n'est définitif qu'après sa signature. Rejouer la même `idempotency_key` ne réserve jamais deux fois. Limité à 5 appels par heure et par adresse IP. Erreurs possibles : `creneau_indisponible` (409, redemander les créneaux), `hors_zone`, `reponses_incompletes` (avec `manquantes`), `consentement_requis`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReserverEntree"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Confirmation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Confirmation"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Erreur"
          },
          "409": {
            "$ref": "#/components/responses/Erreur"
          },
          "422": {
            "$ref": "#/components/responses/Erreur"
          },
          "429": {
            "$ref": "#/components/responses/Erreur"
          },
          "502": {
            "$ref": "#/components/responses/Erreur"
          },
          "503": {
            "$ref": "#/components/responses/Erreur"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Reponses": {
        "type": "object",
        "description": "Réponses du client au questionnaire. Toutes facultatives : renvoyez celles que vous avez, la réponse de estimer_prix liste les `questions` à poser et les `manquantes`. Ne renseignez jamais une réponse que le client n'a pas donnée.",
        "additionalProperties": false,
        "properties": {
          "nuisibles": {
            "type": "array",
            "maxItems": 12,
            "items": {
              "type": "string",
              "enum": [
                "punaises",
                "rats",
                "souris",
                "blattes",
                "guepes",
                "puces",
                "fourmis",
                "mites",
                "desinfection",
                "bois",
                "pigeons",
                "inconnu"
              ]
            },
            "description": "Nuisibles concernés (identifiants du questionnaire). Au-delà de 2 nuisibles, un diagnostic sur place est proposé. Utilisez \"inconnu\" si le client ne sait pas ce que c'est."
          },
          "lieu": {
            "type": "string",
            "enum": [
              "logement",
              "local_pro",
              "parties_communes"
            ],
            "description": "Type de lieu à traiter."
          },
          "code_postal": {
            "type": "string",
            "pattern": "^\\d{5}$",
            "description": "Code postal du lieu à traiter (5 chiffres). Doit être celui de l'adresse d'intervention."
          },
          "surface": {
            "type": "number",
            "minimum": 0,
            "maximum": 100000,
            "description": "Surface à traiter, à peu près, en m²."
          },
          "methode_punaises": {
            "type": "string",
            "enum": [
              "geraniol",
              "canon_chaleur",
              "canon_geraniol"
            ],
            "description": "Punaises de lit : méthode souhaitée (géraniol par défaut)."
          },
          "vapeur": {
            "type": "array",
            "maxItems": 4,
            "items": {
              "type": "string",
              "enum": [
                "lit_simple",
                "lit_double",
                "canape",
                "fauteuil"
              ]
            },
            "description": "Punaises de lit : literie et sièges à passer à la vapeur."
          },
          "etancheite": {
            "type": "string",
            "enum": [
              "oui",
              "non"
            ],
            "description": "Rats : un trou ou un passage d'entrée a été repéré (\"oui\" = à reboucher)."
          },
          "methode_blattes": {
            "type": "string",
            "enum": [
              "gel",
              "pulve_gel"
            ],
            "description": "Blattes : gel seul ou pulvérisation avec gel (conseillé)."
          },
          "espece_nid": {
            "type": "string",
            "enum": [
              "guepe",
              "frelon_asiatique"
            ],
            "description": "Nid : guêpe ou frelon européen, ou frelon asiatique."
          },
          "hauteur_nid": {
            "type": "string",
            "enum": [
              "homme",
              "hors_portee"
            ],
            "description": "Nid : à hauteur d'homme, ou trop haut (nacelle)."
          },
          "nb_nids": {
            "type": "number",
            "minimum": 1,
            "maximum": 50,
            "description": "Nombre de nids."
          },
          "intensite_puces": {
            "type": "string",
            "enum": [
              "faible",
              "forte"
            ],
            "description": "Puces : par endroits (faible) ou partout (forte)."
          },
          "insalubrite": {
            "type": "string",
            "enum": [
              "oui",
              "non"
            ],
            "description": "Désinfection : logement très encombré ou insalubre."
          }
        }
      },
      "ImageEntree": {
        "oneOf": [
          {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "url"
            ],
            "properties": {
              "url": {
                "type": "string",
                "format": "uri",
                "description": "Adresse https publique de l'image (JPEG, PNG ou WebP, 5 Mo maximum). Le serveur la télécharge."
              }
            }
          },
          {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "data"
            ],
            "properties": {
              "data": {
                "type": "string",
                "description": "Image en base64 (avec ou sans préfixe data:), 5 Mo maximum décodée."
              },
              "media_type": {
                "type": "string",
                "enum": [
                  "image/jpeg",
                  "image/png",
                  "image/webp"
                ],
                "description": "Type de l'image (facultatif : détecté automatiquement)."
              }
            }
          }
        ]
      },
      "Contact": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "prenom",
          "nom",
          "telephone",
          "email"
        ],
        "properties": {
          "civilite": {
            "type": [
              "string",
              "null"
            ],
            "description": "Civilité (facultatif)."
          },
          "prenom": {
            "type": "string"
          },
          "nom": {
            "type": "string"
          },
          "telephone": {
            "type": "string",
            "description": "Numéro français, par exemple 0612345678."
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Adresse e-mail du client : le devis à signer y est envoyé."
          },
          "societe": {
            "type": [
              "string",
              "null"
            ],
            "description": "Raison sociale si le client commande pour une société."
          },
          "siret": {
            "type": [
              "string",
              "null"
            ],
            "description": "SIRET (14 chiffres), facultatif."
          }
        }
      },
      "Adresse": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "ligne1",
          "code_postal",
          "ville"
        ],
        "properties": {
          "ligne1": {
            "type": "string",
            "description": "Numéro et rue de l'intervention."
          },
          "complement": {
            "type": [
              "string",
              "null"
            ],
            "description": "Bâtiment, étage, digicode, etc."
          },
          "code_postal": {
            "type": "string",
            "pattern": "^\\d{5}$"
          },
          "ville": {
            "type": "string"
          }
        }
      },
      "Identification": {
        "type": "object",
        "properties": {
          "nuisible": {
            "type": "string",
            "description": "Identifiant à reporter dans Reponses.nuisibles, ou `inconnu`."
          },
          "espece": {
            "type": "string"
          },
          "confiance": {
            "type": "string",
            "enum": [
              "elevee",
              "moyenne",
              "faible"
            ]
          },
          "indices": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "conseil": {
            "type": "string"
          },
          "photo_exploitable": {
            "type": "boolean"
          }
        }
      },
      "Zone": {
        "type": "object",
        "properties": {
          "couvert": {
            "type": "boolean"
          },
          "agence": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "idf",
              "vendee",
              null
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "Estimation": {
        "type": "object",
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "devis",
              "diagnostic"
            ]
          },
          "complet": {
            "type": "boolean"
          },
          "manquantes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "motifs": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "questions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Identifiants des questions à poser, dans l'ordre."
          },
          "lignes": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "titre": {
                  "type": "string"
                },
                "quantite": {
                  "type": "number"
                },
                "unite": {
                  "type": "string"
                },
                "prix_unitaire_ht": {
                  "type": "number"
                },
                "tva": {
                  "type": "number"
                },
                "total_ht": {
                  "type": "number"
                }
              }
            }
          },
          "total_ht": {
            "type": "number"
          },
          "total_tva": {
            "type": "number"
          },
          "total_ttc": {
            "type": "number",
            "description": "Prix à annoncer au client, en euros TTC."
          },
          "duree_estimee_min": {
            "type": [
              "integer",
              "null"
            ]
          },
          "reservable": {
            "type": "boolean",
            "description": "Un rendez-vous peut être pris en ligne."
          }
        }
      },
      "Creneaux": {
        "type": "object",
        "properties": {
          "creneaux": {
            "type": "array",
            "maxItems": 6,
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "examples": [
                    "2026-10-28|08:30|10:30"
                  ]
                },
                "date": {
                  "type": "string",
                  "format": "date"
                },
                "debut": {
                  "type": "string"
                },
                "fin": {
                  "type": "string"
                },
                "libelle": {
                  "type": "string",
                  "examples": [
                    "mardi 28 octobre matin"
                  ]
                }
              }
            }
          },
          "raison": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ReserverEntree": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "reponses",
          "creneau_id",
          "contact",
          "adresse",
          "consentement_rgpd"
        ],
        "properties": {
          "reponses": {
            "$ref": "#/components/schemas/Reponses"
          },
          "creneau_id": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}\\|\\d{2}:\\d{2}\\|\\d{2}:\\d{2}$",
            "description": "Identifiant d'un créneau renvoyé par creneaux_disponibles, par exemple 2026-10-28|08:30|10:30."
          },
          "contact": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "prenom",
              "nom",
              "telephone",
              "email"
            ],
            "properties": {
              "civilite": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Civilité (facultatif)."
              },
              "prenom": {
                "type": "string"
              },
              "nom": {
                "type": "string"
              },
              "telephone": {
                "type": "string",
                "description": "Numéro français, par exemple 0612345678."
              },
              "email": {
                "type": "string",
                "format": "email",
                "description": "Adresse e-mail du client : le devis à signer y est envoyé."
              },
              "societe": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Raison sociale si le client commande pour une société."
              },
              "siret": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "SIRET (14 chiffres), facultatif."
              }
            }
          },
          "adresse": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "ligne1",
              "code_postal",
              "ville"
            ],
            "properties": {
              "ligne1": {
                "type": "string",
                "description": "Numéro et rue de l'intervention."
              },
              "complement": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Bâtiment, étage, digicode, etc."
              },
              "code_postal": {
                "type": "string",
                "pattern": "^\\d{5}$"
              },
              "ville": {
                "type": "string"
              }
            }
          },
          "message": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 1000,
            "description": "Précision utile au technicien (accès, étage, contexte)."
          },
          "photos": {
            "type": "array",
            "maxItems": 3,
            "items": {
              "oneOf": [
                {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "url"
                  ],
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri",
                      "description": "Adresse https publique de l'image (JPEG, PNG ou WebP, 5 Mo maximum). Le serveur la télécharge."
                    }
                  }
                },
                {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "string",
                      "description": "Image en base64 (avec ou sans préfixe data:), 5 Mo maximum décodée."
                    },
                    "media_type": {
                      "type": "string",
                      "enum": [
                        "image/jpeg",
                        "image/png",
                        "image/webp"
                      ],
                      "description": "Type de l'image (facultatif : détecté automatiquement)."
                    }
                  }
                }
              ]
            },
            "description": "Photos du problème à joindre au dossier (3 au maximum)."
          },
          "identification": {
            "type": [
              "object",
              "null"
            ],
            "description": "Résultat renvoyé par identifier_nuisible_photo, à transmettre tel quel s'il existe.",
            "properties": {
              "nuisible": {
                "type": "string"
              },
              "espece": {
                "type": "string"
              },
              "confiance": {
                "type": "string",
                "enum": [
                  "elevee",
                  "moyenne",
                  "faible"
                ]
              },
              "indices": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "conseil": {
                "type": "string"
              },
              "photo_exploitable": {
                "type": "boolean"
              }
            }
          },
          "consentement_rgpd": {
            "type": "boolean",
            "description": "true uniquement si le client a accepté EXPLICITEMENT que HALTE NUISIBLES traite ses données pour répondre à sa demande (politique de confidentialité : https://halte-nuisibles.fr/politique-confidentialite). Ne jamais mettre true sans accord clair du client."
          },
          "idempotency_key": {
            "type": "string",
            "minLength": 8,
            "maxLength": 80,
            "description": "Clé unique de la tentative (un UUID). Réutilisez la même en cas de nouvel essai pour ne jamais réserver deux fois."
          },
          "canal": {
            "type": "string",
            "enum": [
              "site",
              "agent_ia"
            ],
            "description": "Laissez `agent_ia` (valeur par défaut)."
          },
          "agent": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 60,
            "description": "Nom de l'assistant, par exemple ChatGPT ou Claude."
          }
        }
      },
      "Confirmation": {
        "type": "object",
        "properties": {
          "reference_rdv": {
            "type": "string"
          },
          "reference_devis": {
            "type": "string"
          },
          "date": {
            "type": "string"
          },
          "debut": {
            "type": "string"
          },
          "fin": {
            "type": "string"
          },
          "total_ttc": {
            "type": "number"
          },
          "mode": {
            "type": "string",
            "enum": [
              "devis",
              "diagnostic"
            ]
          },
          "devis_envoye": {
            "type": "boolean"
          },
          "lien_signature": {
            "type": [
              "string",
              "null"
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "Erreur": {
        "type": "object",
        "required": [
          "ok",
          "error",
          "message"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string",
            "examples": [
              "creneau_indisponible",
              "hors_zone",
              "reponses_incompletes",
              "consentement_requis",
              "validation",
              "trop_de_requetes"
            ]
          },
          "message": {
            "type": "string",
            "description": "Phrase en français utilisable telle quelle avec le client."
          },
          "manquantes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      }
    },
    "responses": {
      "Erreur": {
        "description": "Erreur",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Erreur"
            }
          }
        }
      }
    }
  }
}