{
 "openapi": "3.1.0",
 "info": {
  "title": "Chaletiste API",
  "version": "1.0.0",
  "summary": "Comparateur français des chalets en bois en kit : données ouvertes, recherche, budget, urbanisme, devis.",
  "description": "API publique et gratuite de Chaletiste (https://chaletiste.fr). Lecture sans clé ; les fichiers /api/v1/*.json sont régénérés chaque nuit. Données sous licence CC BY 4.0, attribution « Chaletiste » avec lien. Les endpoints quote et price-alert transmettent des données personnelles : le consentement explicite de l'utilisateur humain est obligatoire (consent: true). Serveur MCP équivalent : https://chaletiste.fr/api/mcp. Documentation : https://chaletiste.fr/developpeurs/.",
  "contact": {
   "name": "Chaletiste",
   "email": "bonjour@chaletiste.fr",
   "url": "https://chaletiste.fr/developpeurs/"
  },
  "license": {
   "name": "CC BY 4.0 (données)",
   "identifier": "CC-BY-4.0"
  }
 },
 "servers": [
  {
   "url": "https://chaletiste.fr"
  }
 ],
 "security": [],
 "externalDocs": {
  "description": "Guide développeurs et agents",
  "url": "https://chaletiste.fr/developpeurs/"
 },
 "tags": [
  {
   "name": "Données",
   "description": "Fichiers JSON statiques, régénérés chaque nuit."
  },
  {
   "name": "Recherche",
   "description": "Recherche, comparaison et serveur MCP (fonctions serverless)."
  },
  {
   "name": "Calculs",
   "description": "Budget total et formalité d'urbanisme."
  },
  {
   "name": "Transactions",
   "description": "Consentement humain explicite obligatoire."
  }
 ],
 "paths": {
  "/api/v1/index.json": {
   "get": {
    "operationId": "getIndex",
    "tags": [
     "Données"
    ],
    "summary": "Point d'entrée : décrit tous les endpoints, les champs et la licence.",
    "responses": {
     "200": {
      "description": "Index",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/v1/products.json": {
   "get": {
    "operationId": "listProducts",
    "tags": [
     "Données"
    ],
    "summary": "Tous les modèles, champs normalisés.",
    "responses": {
     "200": {
      "description": "Catalogue complet",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "count": {
           "type": "integer"
          },
          "products": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/Product"
           }
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/v1/products/{id}.json": {
   "get": {
    "operationId": "getProductFile",
    "tags": [
     "Données"
    ],
    "summary": "Un modèle par son id.",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      },
      "example": "blooma-selwyn-4-9-m"
     }
    ],
    "responses": {
     "200": {
      "description": "Fiche",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Product"
        },
        "example": {
         "id": "blooma-selwyn-4-9-m",
         "title": "Blooma Selwyn 4,9 m²",
         "name": "Selwyn 4,9 m²",
         "variant": "12 mm 4,87 m²",
         "brand": {
          "id": "blooma",
          "name": "Blooma",
          "url": "https://chaletiste.fr/marques/blooma/"
         },
         "category": {
          "id": "abris-de-jardin-bois",
          "name": "Abris de jardin bois"
         },
         "subcategory": "abri",
         "habitable": false,
         "url": "https://chaletiste.fr/chalets/blooma-selwyn-4-9-m/",
         "md_url": "https://chaletiste.fr/chalets/blooma-selwyn-4-9-m.md",
         "api_url": "https://chaletiste.fr/api/v1/products/blooma-selwyn-4-9-m.json",
         "summary": "Le Selwyn 4,9 m² de Blooma coûte 480 € TTC (prix constaté chez Castorama le 27 septembre 2026), avec des parois de 12 mm (madriers emboîtés 12 mm), se monte en environ 1 jour (estimation, la marque ne communique pas de durée) et se livre sous 4 jours.",
         "price_eur": 480,
         "price_date": "2026-09-27",
         "price_note": "Prix TTC affiché le 2026-09-27, vendu et expédié par Castorama.",
         "price_per_m2_eur": 99,
         "retailer": "Castorama",
         "buy_url": "https://www.castorama.fr/abri-de-jardin-blooma-selwyn-en-bois-coloris-naturel-ep-12-mm-surface-exterieure-4-87-m-/3663602713258_CAFR.prd?utm_source=chaletiste&utm_medium=comparateur",
         "official_url": "https://www.castorama.fr/abri-de-jardin-blooma-selwyn-en-bois-coloris-naturel-ep-12-mm-surface-exterieure-4-87-m-/3663602713258_CAFR.prd",
         "surface_m2": 4.87,
         "surface_note": "Surface extérieure hors tout 4,87 m² ; surface intérieure 4 m² (fiche)",
         "footprint_m2": 4.87,
         "dimensions_m": {
          "width": null,
          "depth": null,
          "height": null,
          "note": "Dimensions détaillées non publiées sur la fiche ; poids 160 kg"
         },
         "wall_thickness_mm": 12,
         "wall_type": "madriers emboîtés 12 mm",
         "wood": "Bois d’origine Scandinavie",
         "treatment": "traité en profondeur",
         "roof": "À pignon, lames + feutre, toit 15 mm",
         "floor_included": true,
         "glazing": "polycarbonate",
         "insulation": "non isolé",
         "kit_level": "kit panneaux à monter soi-même, plancher 38 mm inclus",
         "included": "Panneaux 12 mm, plancher, double porte, 1 fenêtre polycarbonate, fixations",
         "foundations": null,
         "delivered_assembled": false,
         "assembly": {
          "hours": 8.0,
          "text": null,
          "people": 2,
          "estimated": true,
          "basis": "ratio médian de 1,60 h/m² observé sur 7 modèles comparables",
          "source": "https://www.castorama.fr/abri-de-jardin-blooma-selwyn-en-bois-coloris-naturel-ep-12-mm-surface-exterieure-4-87-m-/3663602713258_CAFR.prd"
         },
         "delivery": {
          "days_min": 4,
          "days_max": null,
          "text": "Expédition annoncée sous 4 jours (donnée structurée de la fiche) ; date et frais exacts indiqués au panier",
          "note": null,
          "source": "https://www.castorama.fr/services/livraison-domicile"
         },
         "permit": {
          "code": "aucune",
          "label": "Aucune formalité",
          "note": "Emprise au sol de 5 m² ou moins et hauteur de 12 m ou moins, hors secteur protégé. En secteur protégé (abords d’un monument historique, site patrimonial remarquable), une déclaration préalable est exigée dès le premier mètre carré."
         },
         "warranty": "Garantie fabricant 3 ans",
         "origin": null,
         "photo": "https://chaletiste.fr/static/products/blooma-selwyn-4-9-m.webp",
         "photo_credit": "Blooma",
         "similar_ids": [
          "tuindeco-najette-5-m",
          "chalet-de-jardin-nano-5-m",
          "solid-laval-4-7-m"
         ],
         "sources": [
          "https://www.castorama.fr/abri-de-jardin-blooma-selwyn-en-bois-coloris-naturel-ep-12-mm-surface-exterieure-4-87-m-/3663602713258_CAFR.prd",
          "https://www.castorama.fr/services/livraison-domicile"
         ],
         "verified_at": "2026-09-27"
        }
       }
      }
     },
     "404": {
      "description": "Inconnu"
     }
    }
   }
  },
  "/api/v1/brands.json": {
   "get": {
    "operationId": "listBrands",
    "tags": [
     "Données"
    ],
    "summary": "Toutes les marques.",
    "responses": {
     "200": {
      "description": "Marques",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/v1/categories.json": {
   "get": {
    "operationId": "listCategories",
    "tags": [
     "Données"
    ],
    "summary": "Catégories avec statistiques de prix et de surface.",
    "responses": {
     "200": {
      "description": "Catégories",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/v1/guides.json": {
   "get": {
    "operationId": "listGuides",
    "tags": [
     "Données"
    ],
    "summary": "Guides : titre, URL, phrase-réponse, FAQ, sources.",
    "responses": {
     "200": {
      "description": "Guides",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/v1/search": {
   "get": {
    "operationId": "searchChalets",
    "tags": [
     "Recherche"
    ],
    "summary": "Recherche filtrée et triée, avec explication.",
    "parameters": [
     {
      "name": "q",
      "in": "query",
      "required": false,
      "description": "Texte libre (marque, modèle, usage).",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "category",
      "in": "query",
      "required": false,
      "description": "Catégorie, sous-catégorie ou alias.",
      "schema": {
       "type": "string",
       "enum": [
        "abri",
        "abris",
        "abris-de-jardin-bois",
        "bureau",
        "bureau-de-jardin",
        "chalet-bureau",
        "chalet-habitable",
        "chalet-jardin",
        "chalet-loisirs",
        "chalets-de-jardin",
        "chalets-habitables",
        "fuste",
        "fuste-rondins",
        "habitable",
        "lodge",
        "maison",
        "maison-bois-kit",
        "pool-house",
        "pool-house-bois",
        "rondins",
        "studio",
        "studio-jardin",
        "studios-de-jardin",
        "tiny",
        "tiny-house",
        "tiny-houses"
       ]
      }
     },
     {
      "name": "brand",
      "in": "query",
      "required": false,
      "description": "Identifiant ou nom de marque.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "max_price",
      "in": "query",
      "required": false,
      "description": "Prix TTC maximal (€).",
      "schema": {
       "type": "number"
      }
     },
     {
      "name": "min_price",
      "in": "query",
      "required": false,
      "description": "Prix TTC minimal (€).",
      "schema": {
       "type": "number"
      }
     },
     {
      "name": "min_surface",
      "in": "query",
      "required": false,
      "description": "Surface minimale (m²).",
      "schema": {
       "type": "number"
      }
     },
     {
      "name": "max_surface",
      "in": "query",
      "required": false,
      "description": "Surface maximale (m²).",
      "schema": {
       "type": "number"
      }
     },
     {
      "name": "min_wall_mm",
      "in": "query",
      "required": false,
      "description": "Épaisseur minimale des parois (mm).",
      "schema": {
       "type": "number"
      }
     },
     {
      "name": "max_delivery_days",
      "in": "query",
      "required": false,
      "description": "Délai de livraison maximal (jours).",
      "schema": {
       "type": "number"
      }
     },
     {
      "name": "max_assembly_hours",
      "in": "query",
      "required": false,
      "description": "Temps de montage maximal (heures).",
      "schema": {
       "type": "number"
      }
     },
     {
      "name": "habitable",
      "in": "query",
      "required": false,
      "description": "Modèles habitables uniquement.",
      "schema": {
       "type": "boolean"
      }
     },
     {
      "name": "sort",
      "in": "query",
      "required": false,
      "description": "Tri (défaut price_per_m2, ou relevance avec q).",
      "schema": {
       "type": "string",
       "enum": [
        "price",
        "price_per_m2",
        "surface",
        "delivery",
        "assembly",
        "wall",
        "relevance"
       ]
      }
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "description": "Nombre de résultats, 1 à 50 (défaut 10).",
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 50
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Résultats",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/SearchResponse"
        }
       }
      }
     },
     "400": {
      "description": "Erreur",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/v1/compare": {
   "get": {
    "operationId": "compareChalets",
    "tags": [
     "Recherche"
    ],
    "summary": "Compare 2 à 6 modèles et désigne le meilleur par critère.",
    "parameters": [
     {
      "name": "ids",
      "in": "query",
      "required": true,
      "description": "id séparés par des virgules.",
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Comparaison",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "400": {
      "description": "Erreur",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "Erreur",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/v1/budget": {
   "get": {
    "operationId": "estimateBudget",
    "tags": [
     "Calculs"
    ],
    "summary": "Budget total : kit + fondation + montage + taxe d'aménagement 2026, avec hypothèses.",
    "parameters": [
     {
      "name": "product",
      "in": "query",
      "required": true,
      "description": "id du modèle.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "foundation",
      "in": "query",
      "required": false,
      "description": "Fondation.",
      "schema": {
       "type": "string",
       "enum": [
        "dalle",
        "plots",
        "none"
       ]
      }
     },
     {
      "name": "assembly",
      "in": "query",
      "required": false,
      "description": "Montage.",
      "schema": {
       "type": "string",
       "enum": [
        "pro",
        "self"
       ]
      }
     },
     {
      "name": "region",
      "in": "query",
      "required": false,
      "description": "Valeur forfaitaire de la taxe.",
      "schema": {
       "type": "string",
       "enum": [
        "idf",
        "hors-idf"
       ]
      }
     },
     {
      "name": "surface_taxable",
      "in": "query",
      "required": false,
      "description": "Surface taxable (m²), défaut : surface du modèle.",
      "schema": {
       "type": "number"
      }
     },
     {
      "name": "commune_rate",
      "in": "query",
      "required": false,
      "description": "Taux communal en % (défaut 5).",
      "schema": {
       "type": "number"
      }
     },
     {
      "name": "department_rate",
      "in": "query",
      "required": false,
      "description": "Taux départemental en % (défaut 2,5).",
      "schema": {
       "type": "number"
      }
     },
     {
      "name": "main_residence",
      "in": "query",
      "required": false,
      "description": "Résidence principale : abattement de 50 %.",
      "schema": {
       "type": "boolean"
      }
     },
     {
      "name": "archeology",
      "in": "query",
      "required": false,
      "description": "Inclure l'archéologie préventive (défaut true).",
      "schema": {
       "type": "boolean"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Estimation",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/BudgetResponse"
        }
       }
      }
     },
     "400": {
      "description": "Erreur",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "Erreur",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/v1/permit": {
   "get": {
    "operationId": "checkPermit",
    "tags": [
     "Calculs"
    ],
    "summary": "Formalité d'urbanisme : aucune, déclaration préalable ou permis de construire.",
    "parameters": [
     {
      "name": "surface",
      "in": "query",
      "required": true,
      "description": "Surface (m²), la plus grande de l'emprise au sol et de la surface de plancher.",
      "schema": {
       "type": "number"
      }
     },
     {
      "name": "extension",
      "in": "query",
      "required": false,
      "description": "1 si extension d'une maison existante.",
      "schema": {
       "type": "integer",
       "enum": [
        0,
        1
       ]
      }
     },
     {
      "name": "zone_urbaine_plu",
      "in": "query",
      "required": false,
      "description": "1 si zone urbaine couverte par un PLU.",
      "schema": {
       "type": "integer",
       "enum": [
        0,
        1
       ]
      }
     },
     {
      "name": "secteur_protege",
      "in": "query",
      "required": false,
      "description": "1 si secteur protégé.",
      "schema": {
       "type": "integer",
       "enum": [
        0,
        1
       ]
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Formalité",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/PermitResponse"
        }
       }
      }
     },
     "400": {
      "description": "Erreur",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/v1/quote": {
   "post": {
    "operationId": "requestQuote",
    "tags": [
     "Transactions"
    ],
    "summary": "Demande de devis au nom de l'utilisateur (consentement humain explicite obligatoire).",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "product_id",
         "name",
         "email",
         "postcode",
         "consent"
        ],
        "properties": {
         "product_id": {
          "type": "string",
          "description": "id du modèle (voir /api/v1/search)."
         },
         "email": {
          "type": "string",
          "format": "email",
          "description": "E-mail de l'utilisateur humain."
         },
         "consent": {
          "type": "boolean",
          "const": true,
          "description": "OBLIGATOIRE, doit valoir true : l'utilisateur humain a explicitement accepté que ses coordonnées soient transmises à Chaletiste et au vendeur. Un agent ne doit jamais le fixer à true sans ce consentement."
         },
         "agent_name": {
          "type": "string",
          "description": "Nom de l'agent ou de l'assistant qui transmet la demande."
         },
         "dry_run": {
          "type": "boolean",
          "description": "true : valide la demande sans rien envoyer."
         },
         "name": {
          "type": "string"
         },
         "phone": {
          "type": "string"
         },
         "postcode": {
          "type": "string",
          "pattern": "^[0-9]{5}$"
         },
         "horizon": {
          "type": "string"
         },
         "message": {
          "type": "string",
          "maxLength": 2000
         }
        }
       },
       "example": {
        "product_id": "blooma-selwyn-4-9-m",
        "name": "Camille Martin",
        "email": "camille@example.fr",
        "postcode": "44000",
        "horizon": "moins de 3 mois",
        "message": "Terrain plat, dalle existante.",
        "consent": true,
        "agent_name": "Assistant de Camille",
        "dry_run": true
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Demande transmise",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Ack"
        }
       }
      }
     },
     "400": {
      "description": "Erreur",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "Erreur",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "502": {
      "description": "Erreur",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/v1/price-alert": {
   "post": {
    "operationId": "createPriceAlert",
    "tags": [
     "Transactions"
    ],
    "summary": "Alerte de baisse de prix (consentement humain explicite obligatoire).",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "product_id",
         "email",
         "consent"
        ],
        "properties": {
         "product_id": {
          "type": "string",
          "description": "id du modèle (voir /api/v1/search)."
         },
         "email": {
          "type": "string",
          "format": "email",
          "description": "E-mail de l'utilisateur humain."
         },
         "consent": {
          "type": "boolean",
          "const": true,
          "description": "OBLIGATOIRE, doit valoir true : l'utilisateur humain a explicitement accepté que ses coordonnées soient transmises à Chaletiste et au vendeur. Un agent ne doit jamais le fixer à true sans ce consentement."
         },
         "agent_name": {
          "type": "string",
          "description": "Nom de l'agent ou de l'assistant qui transmet la demande."
         },
         "dry_run": {
          "type": "boolean",
          "description": "true : valide la demande sans rien envoyer."
         },
         "target_price_eur": {
          "type": "number"
         }
        }
       },
       "example": {
        "product_id": "blooma-selwyn-4-9-m",
        "email": "camille@example.fr",
        "target_price_eur": 432,
        "consent": true,
        "dry_run": true
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Alerte enregistrée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Ack"
        }
       }
      }
     },
     "400": {
      "description": "Erreur",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "Erreur",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "502": {
      "description": "Erreur",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/mcp": {
   "post": {
    "operationId": "mcp",
    "tags": [
     "Recherche"
    ],
    "summary": "Serveur MCP (Streamable HTTP, JSON-RPC 2.0, protocole 2025-06-18).",
    "description": "Outils : search_chalets, get_chalet, compare_chalets, estimate_total_budget, check_permit, list_guides, get_guide, request_quote, search, fetch.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       },
       "example": {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/list"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Réponse JSON-RPC",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "202": {
      "description": "Notification acceptée"
     }
    }
   }
  }
 },
 "components": {
  "schemas": {
   "Product": {
    "type": "object",
    "required": [
     "id",
     "title",
     "url",
     "verified_at"
    ],
    "properties": {
     "id": {
      "type": "string",
      "description": "Identifiant stable du modèle (slug de l'URL de la fiche)."
     },
     "title": {
      "type": "string",
      "description": "Marque + nom du modèle."
     },
     "name": {
      "type": "string",
      "description": "Nom du modèle tel que publié par la marque."
     },
     "variant": {
      "type": [
       "string",
       "null"
      ],
      "description": "Variante relevée (épaisseur, référence)."
     },
     "brand": {
      "type": "object",
      "description": "{id, name, url} de la marque."
     },
     "category": {
      "type": "object",
      "description": "{id, name} de la catégorie Chaletiste."
     },
     "subcategory": {
      "type": "string",
      "description": "Sous-catégorie : abri, chalet-loisirs, chalet-bureau, pool-house, studio-jardin, chalet-habitable, maison-bois-kit, fuste-rondins, tiny-house, lodge."
     },
     "habitable": {
      "type": "boolean",
      "description": "Conçu pour être habité."
     },
     "url": {
      "type": "string",
      "description": "Fiche HTML sur chaletiste.fr."
     },
     "md_url": {
      "type": "string",
      "description": "Version markdown de la fiche."
     },
     "api_url": {
      "type": "string",
      "description": "Cette fiche en JSON."
     },
     "summary": {
      "type": "string",
      "description": "Phrase-réponse citable résumant les quatre chiffres."
     },
     "price_eur": {
      "type": [
       "number",
       "null"
      ],
      "description": "Prix TTC constaté du kit, en euros."
     },
     "price_date": {
      "type": [
       "string",
       "null"
      ],
      "format": "date",
      "description": "Date du relevé de prix."
     },
     "price_note": {
      "type": [
       "string",
       "null"
      ],
      "description": "Précision sur le prix (promotion, contenu)."
     },
     "price_per_m2_eur": {
      "type": [
       "number",
       "null"
      ],
      "description": "Prix TTC divisé par la surface, arrondi à l'euro."
     },
     "retailer": {
      "type": [
       "string",
       "null"
      ],
      "description": "Marchand chez qui le prix a été relevé."
     },
     "buy_url": {
      "type": [
       "string",
       "null"
      ],
      "description": "Lien vers la page du marchand (peut être un lien d'affiliation)."
     },
     "official_url": {
      "type": [
       "string",
       "null"
      ],
      "description": "Page officielle de la marque."
     },
     "surface_m2": {
      "type": [
       "number",
       "null"
      ],
      "description": "Surface annoncée en m²."
     },
     "surface_note": {
      "type": [
       "string",
       "null"
      ],
      "description": "Ce que mesure la surface (intérieure, hors tout)."
     },
     "footprint_m2": {
      "type": [
       "number",
       "null"
      ],
      "description": "Emprise au sol (largeur × profondeur), celle qui compte pour l'urbanisme."
     },
     "dimensions_m": {
      "type": "object",
      "description": "Dimensions en mètres.",
      "properties": {
       "width": {
        "type": [
         "number",
         "null"
        ]
       },
       "depth": {
        "type": [
         "number",
         "null"
        ]
       },
       "height": {
        "type": [
         "number",
         "null"
        ]
       },
       "note": {
        "type": [
         "string",
         "null"
        ]
       }
      }
     },
     "wall_thickness_mm": {
      "type": [
       "number",
       "null"
      ],
      "description": "Épaisseur des parois en millimètres."
     },
     "wall_type": {
      "type": [
       "string",
       "null"
      ],
      "description": "Type de paroi (madrier massif, double madrier, ossature isolée…)."
     },
     "wood": {
      "type": [
       "string",
       "null"
      ],
      "description": "Essence de bois."
     },
     "treatment": {
      "type": [
       "string",
       "null"
      ],
      "description": "Traitement du bois."
     },
     "roof": {
      "type": [
       "string",
       "null"
      ],
      "description": "Toiture et couverture."
     },
     "floor_included": {
      "type": [
       "boolean",
       "null"
      ],
      "description": "Plancher inclus dans le kit."
     },
     "glazing": {
      "type": [
       "string",
       "null"
      ],
      "description": "Menuiseries et vitrage."
     },
     "insulation": {
      "type": [
       "string",
       "null"
      ],
      "description": "Isolation fournie."
     },
     "kit_level": {
      "type": [
       "string",
       "null"
      ],
      "description": "Niveau de kit : kit brut, kit madriers, hors d'eau hors d'air, livré monté, clé en main."
     },
     "included": {
      "type": [
       "string",
       "null"
      ],
      "description": "Contenu du kit."
     },
     "foundations": {
      "type": [
       "string",
       "null"
      ],
      "description": "Fondations fournies ou recommandées."
     },
     "delivered_assembled": {
      "type": "boolean",
      "description": "Livré monté ou clé en main (pas de montage à prévoir)."
     },
     "assembly": {
      "type": "object",
      "description": "{hours, text, people, estimated, basis, source} : temps de montage. estimated = true : estimation Chaletiste (ratio h/m² de modèles comparables), jamais présentée comme donnée du fabricant.",
      "properties": {
       "hours": {
        "type": [
         "number",
         "null"
        ]
       },
       "text": {
        "type": [
         "string",
         "null"
        ]
       },
       "people": {
        "type": [
         "string",
         "number",
         "null"
        ]
       },
       "estimated": {
        "type": "boolean"
       },
       "basis": {
        "type": [
         "string",
         "null"
        ]
       },
       "source": {
        "type": [
         "string",
         "null"
        ]
       }
      }
     },
     "delivery": {
      "type": "object",
      "properties": {
       "days_min": {
        "type": [
         "number",
         "null"
        ]
       },
       "days_max": {
        "type": [
         "number",
         "null"
        ]
       },
       "text": {
        "type": [
         "string",
         "null"
        ]
       },
       "note": {
        "type": [
         "string",
         "null"
        ]
       },
       "source": {
        "type": [
         "string",
         "null"
        ]
       }
      }
     },
     "permit": {
      "type": [
       "object",
       "null"
      ],
      "properties": {
       "code": {
        "type": "string",
        "enum": [
         "aucune",
         "dp",
         "pc"
        ]
       },
       "label": {
        "type": "string"
       },
       "note": {
        "type": "string"
       }
      }
     },
     "warranty": {
      "type": [
       "string",
       "null"
      ],
      "description": "Garantie annoncée."
     },
     "origin": {
      "type": [
       "string",
       "null"
      ],
      "description": "Pays de fabrication."
     },
     "photo": {
      "type": [
       "string",
       "null"
      ],
      "description": "URL de la photo (propriété de la marque, voir photo_credit)."
     },
     "photo_credit": {
      "type": [
       "string",
       "null"
      ],
      "description": "Crédit de la photo."
     },
     "similar_ids": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Identifiants de modèles similaires (même catégorie, surface proche)."
     },
     "sources": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "URL des sources consultées."
     },
     "verified_at": {
      "type": "string",
      "format": "date",
      "description": "Date de la dernière vérification de la fiche."
     }
    }
   },
   "ProductSummary": {
    "type": "object",
    "properties": {
     "id": {
      "type": [
       "string",
       "number",
       "boolean",
       "null"
      ]
     },
     "title": {
      "type": [
       "string",
       "number",
       "boolean",
       "null"
      ]
     },
     "brand": {
      "type": [
       "string",
       "number",
       "boolean",
       "null"
      ]
     },
     "category": {
      "type": [
       "string",
       "number",
       "boolean",
       "null"
      ]
     },
     "price_eur": {
      "type": [
       "string",
       "number",
       "boolean",
       "null"
      ]
     },
     "price_per_m2_eur": {
      "type": [
       "string",
       "number",
       "boolean",
       "null"
      ]
     },
     "surface_m2": {
      "type": [
       "string",
       "number",
       "boolean",
       "null"
      ]
     },
     "wall_thickness_mm": {
      "type": [
       "string",
       "number",
       "boolean",
       "null"
      ]
     },
     "assembly_hours": {
      "type": [
       "string",
       "number",
       "boolean",
       "null"
      ]
     },
     "assembly_estimated": {
      "type": [
       "string",
       "number",
       "boolean",
       "null"
      ]
     },
     "delivery_days_max": {
      "type": [
       "string",
       "number",
       "boolean",
       "null"
      ]
     },
     "permit": {
      "type": [
       "string",
       "number",
       "boolean",
       "null"
      ]
     },
     "retailer": {
      "type": [
       "string",
       "number",
       "boolean",
       "null"
      ]
     },
     "price_date": {
      "type": [
       "string",
       "number",
       "boolean",
       "null"
      ]
     },
     "url": {
      "type": [
       "string",
       "number",
       "boolean",
       "null"
      ]
     },
     "api_url": {
      "type": [
       "string",
       "number",
       "boolean",
       "null"
      ]
     },
     "md_url": {
      "type": [
       "string",
       "number",
       "boolean",
       "null"
      ]
     },
     "photo": {
      "type": [
       "string",
       "number",
       "boolean",
       "null"
      ]
     }
    }
   },
   "SearchResponse": {
    "type": "object",
    "properties": {
     "ok": {
      "type": "boolean"
     },
     "query": {
      "type": "object"
     },
     "sort": {
      "type": "string"
     },
     "total": {
      "type": "integer"
     },
     "count": {
      "type": "integer"
     },
     "explanation": {
      "type": "string"
     },
     "results": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ProductSummary"
      }
     },
     "data_updated": {
      "type": "string"
     },
     "license": {
      "type": "string"
     },
     "attribution": {
      "type": "string"
     }
    }
   },
   "BudgetResponse": {
    "type": "object",
    "properties": {
     "ok": {
      "type": "boolean"
     },
     "total_eur": {
      "type": "number"
     },
     "range_eur": {
      "type": "object",
      "properties": {
       "low": {
        "type": "number"
       },
       "high": {
        "type": "number"
       }
      }
     },
     "lines": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "item": {
         "type": "string"
        },
        "label": {
         "type": "string"
        },
        "amount_eur": {
         "type": "number"
        },
        "low_eur": {
         "type": "number"
        },
        "high_eur": {
         "type": "number"
        },
        "source": {
         "type": "string"
        }
       }
      }
     },
     "assumptions": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "explanation": {
      "type": "string"
     },
     "sources": {
      "type": "array"
     }
    }
   },
   "PermitResponse": {
    "type": "object",
    "properties": {
     "ok": {
      "type": "boolean"
     },
     "permit": {
      "type": "object",
      "properties": {
       "code": {
        "type": "string",
        "enum": [
         "aucune",
         "dp",
         "pc"
        ]
       },
       "label": {
        "type": "string"
       }
      }
     },
     "rule": {
      "type": "string"
     },
     "instruction_delay": {
      "type": [
       "string",
       "null"
      ]
     },
     "notes": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "sources": {
      "type": "array"
     },
     "explanation": {
      "type": "string"
     }
    }
   },
   "Ack": {
    "type": "object",
    "properties": {
     "ok": {
      "type": "boolean"
     },
     "reference": {
      "type": [
       "string",
       "null"
      ]
     },
     "type": {
      "type": "string",
      "enum": [
       "quote",
       "price_alert"
      ]
     },
     "message": {
      "type": "string"
     },
     "dry_run": {
      "type": "boolean"
     },
     "received_at": {
      "type": "string",
      "format": "date-time"
     }
    }
   },
   "Error": {
    "type": "object",
    "properties": {
     "ok": {
      "type": "boolean",
      "const": false
     },
     "error": {
      "type": "object",
      "properties": {
       "code": {
        "type": "string"
       },
       "message": {
        "type": "string"
       }
      }
     }
    }
   }
  }
 }
}