{
  "openapi": "3.1.0",
  "info": {
    "title": "API publique Verdiseo",
    "version": "1.0.0",
    "summary": "Diagnostic de visibilité locale et IA (GEO) d'un site de commerce.",
    "description": "API publique de Verdiseo.\n\nCette spécification ne décrit QUE les points d'entrée réellement ouverts et appelables. Les autres routes sous `/api/` (compte, facturation, tableau de bord, webhooks, administration) sont internes à l'application, exigent une session authentifiée, et ne sont volontairement pas documentées ici.\n\nParcours type : obtenir un jeton invité via `GET /api/auth/token`, puis appeler `POST /api/audit/scan` avec l'en-tête `x-guest-token`.",
    "contact": { "name": "Verdiseo", "url": "https://verdiseo.fr" },
    "license": { "name": "Conditions générales d'utilisation", "url": "https://verdiseo.fr/cgu" }
  },
  "servers": [{ "url": "https://verdiseo.fr", "description": "Production" }],
  "tags": [
    { "name": "Authentification", "description": "Obtention d'un jeton d'appel." },
    { "name": "Diagnostic", "description": "Analyse de visibilité d'un domaine." },
    { "name": "Référentiel", "description": "Données de référence françaises." },
    { "name": "Exploitation", "description": "État du service." }
  ],
  "paths": {
    "/api/auth/token": {
      "get": {
        "tags": ["Authentification"],
        "operationId": "getGuestToken",
        "summary": "Obtenir un jeton invité",
        "description": "Émet un jeton lié à l'adresse IP appelante. Aucune authentification préalable n'est requise. Le jeton est à transmettre dans l'en-tête `x-guest-token` de `POST /api/audit/scan` ; il n'est valable que depuis la même adresse IP.",
        "security": [],
        "responses": {
          "200": {
            "description": "Jeton émis.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["guestToken"],
                  "properties": { "guestToken": { "type": "string", "description": "À placer dans l'en-tête `x-guest-token`." } }
                },
                "example": { "guestToken": "eyJpcCI6..." }
              }
            }
          },
          "500": { "$ref": "#/components/responses/Erreur" }
        }
      }
    },
    "/api/audit/scan": {
      "post": {
        "tags": ["Diagnostic"],
        "operationId": "scanDomain",
        "summary": "Diagnostiquer la visibilité d'un domaine",
        "description": "Analyse un site de commerce et rend deux scores : `geoScore` (visibilité auprès des moteurs génératifs) et `seoScore` (référencement local). L'appel est limité en débit ; un diagnostic déjà produit pour le même domaine dans les 24 h est renvoyé depuis le cache (`fromCache: true`).\n\n⚠️ La réponse est la version GRATUITE du rapport : les livrables de l'audit payant en sont retirés côté serveur.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["domain"],
                "properties": {
                  "domain": { "type": "string", "description": "Nom de domaine ou URL du commerce.", "examples": ["boulangerie-exemple.fr"] },
                  "ville": { "type": "string", "description": "Commune, pour situer les concurrents locaux." },
                  "secteur": { "type": "string", "description": "Secteur d'activité, pour le barème sectoriel." }
                }
              },
              "example": { "domain": "boulangerie-exemple.fr", "ville": "Nantes", "secteur": "Boulangerie / Pâtisserie" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Diagnostic produit.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["success", "auditId", "domain", "report"],
                  "properties": {
                    "success": { "type": "boolean", "const": true },
                    "auditId": { "type": "string", "format": "uuid" },
                    "domain": { "type": "string" },
                    "fromCache": { "type": "boolean", "description": "Présent et vrai si le diagnostic vient du cache 24 h." },
                    "report": {
                      "type": "object",
                      "description": "Rapport gratuit. Contient notamment `geoScore` et `seoScore` (entiers), et la liste des critères évalués.",
                      "properties": {
                        "geoScore": { "type": "integer", "description": "Visibilité auprès des moteurs génératifs." },
                        "seoScore": { "type": "integer", "description": "Référencement local." }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/Erreur" },
          "401": {
            "description": "Jeton invité, clé d'API ou session absent ou invalide.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erreur" } } }
          },
          "429": {
            "description": "Limite d'appels atteinte.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/Erreur" },
                    { "type": "object", "properties": { "limitReached": { "type": "boolean" } } }
                  ]
                }
              }
            }
          },
          "500": { "$ref": "#/components/responses/Erreur" }
        }
      }
    },
    "/api/communes": {
      "get": {
        "tags": ["Référentiel"],
        "operationId": "searchCommunes",
        "summary": "Rechercher une commune française",
        "description": "Aide à la saisie. Rend un tableau vide — et non une erreur — lorsque la requête est trop courte, trop longue ou sans résultat.",
        "security": [],
        "parameters": [
          { "name": "q", "in": "query", "required": true, "schema": { "type": "string", "minLength": 2, "maxLength": 60 }, "description": "Début du nom de la commune. Hors bornes ⇒ tableau vide." },
          { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 10, "default": 5 } }
        ],
        "responses": {
          "200": {
            "description": "Communes correspondantes, éventuellement aucune.",
            "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object" } } } }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "tags": ["Exploitation"],
        "operationId": "getHealth",
        "summary": "État du service",
        "description": "Point d'état, cible de la relation `status` du catalogue d'API (RFC 9727).",
        "security": [],
        "responses": {
          "200": {
            "description": "Service disponible.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["status"],
                  "properties": {
                    "status": { "type": "string", "enum": ["pass"] },
                    "version": { "type": "string" }
                  }
                },
                "example": { "status": "pass", "version": "1.0.0" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "jetonInvite": { "type": "apiKey", "in": "header", "name": "x-guest-token", "description": "Jeton obtenu via `GET /api/auth/token`, lié à l'adresse IP appelante." }
    },
    "schemas": {
      "Erreur": { "type": "object", "required": ["error"], "properties": { "error": { "type": "string" } } }
    },
    "responses": {
      "Erreur": { "description": "Requête invalide ou erreur serveur.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erreur" } } } }
    }
  },
  "security": [{ "jetonInvite": [] }]
}
