{
  "openapi": "3.0.4",
  "info": {
    "title": "WattMind Energy API",
    "description": "API d'intelligence carbone & énergétique du réseau électrique français.\n\nAgrège 13 signaux RTE (Ecowatt, Tempo, génération, prix, marges...) en un score 0-100 sur 4 dimensions (prix, carbone, réseau, adéquation), actionnable en temps réel et en prévision horaire.\n\n**Rate limiting** : chaque réponse porte des en-têtes de quota.\n- `X-RateLimit-Limit-Minute` / `X-RateLimit-Remaining-Minute` — sur toutes les réponses authentifiées.\n- `X-RateLimit-Limit-Month` / `X-RateLimit-Remaining-Month` — **uniquement sur le plan gratuit**, le seul où le volume mensuel est une limite appliquée. Les publier sur un plan payant annoncerait une limite qui n'existe pas et un `Remaining` qui atteindrait 0 sans conséquence.\n- `Retry-After` (secondes) en cas de refus (HTTP 429).\n- `X-WattMind-Confidence` : `degraded` si certaines sources sont indisponibles.\n\n**Débit** : 10 requêtes/minute sur le plan gratuit, 60 sur les plans payants.\n\n**Volume mensuel** : sur le plan gratuit c'est un plafond — au-delà, `429 upgrade_required`. Sur les plans payants, le volume de référence est un repère d'usage équitable, pas un compteur bloquant : le dépassement est servi, jamais facturé en supplément.\n\nLa grille complète (débit, clés, horizon de prévision, profondeur d'historique, prix) est servie par `GET /api/plans`, sans clé API. La profondeur d'historique réellement disponible est exposée par `GET /api/coverage`.",
    "contact": {
      "name": "WattMind",
      "email": "contact@wattmind.fr"
    },
    "version": "v1"
  },
  "paths": {
    "/v1/carbon/intensity": {
      "get": {
        "tags": [
          "CarbonIntensity"
        ],
        "summary": "Intensité carbone temps réel du réseau électrique français.\nRetourne le pourcentage de production décarbonée, la part renouvelable,\net le facteur d'émission estimé (gCO2eq/kWh).",
        "description": "\n<b>Nature exacte de la donnée</b> : intensité <b>location-based à résolution horaire</b> du\n            réseau français, calculée sur le <b>mix de production</b> national pondéré par les facteurs\n            d'émission ADEME Base Carbone® par filière (analyse de cycle de vie, frontière production).\n\n<b>Ce que le périmètre exclut</b> : les imports, les exports et les pertes de transport et de\n            distribution. L'écart avec le facteur annuel ADEME du mix français (≈ 52 gCO₂eq/kWh, frontière\n            <i>consommation</i>) s'explique par cette différence de frontière, pas par un écart de méthode.\n            À noter : le sous-score carbone de `/v1/score`, lui, pondère les imports par l'intensité\n            réelle des pays exportateurs (source Ember) — il sert à décider maintenant, pas à comptabiliser.\n\n<b>Usage en reporting</b> : c'est la granularité horaire qui manque à un facteur annuel, donc\n            une donnée d'entrée pour un scope 2 (CSRD/BEGES/VSME) ou pour un matching 24/7 CFE. Ce n'est\n            pas un facteur « market-based » au sens du GHG Protocol : celui-ci repose sur vos instruments\n            contractuels (garanties d'origine, PPA) ou sur le mix résiduel, que WattMind ne détient pas.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/CarbonIntensityResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CarbonIntensityResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/CarbonIntensityResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "Service Unavailable"
          }
        }
      }
    },
    "/v1/score": {
      "get": {
        "tags": [
          "EnergyData"
        ],
        "summary": "Score composite temps réel national de la \"propreté\" de l'électricité du réseau français.\nCombine intensité carbone, prix spot, tension réseau et adéquation prévisionnelle.\nUtilisé pour décider en temps réel : faut-il lancer ce process maintenant ou reporter ?",
        "description": "Ouvert à tous les plans en pondération nationale `balanced`. Une pondération\npersonnalisée (`profile` ou `w*`) est une capacité de plan : demandée sans y avoir\ndroit, elle est <b>refusée</b> et non servie en balanced (ADR-0001).",
        "parameters": [
          {
            "name": "profile",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "wPrice",
            "in": "query",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "wCarbon",
            "in": "query",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "wGrid",
            "in": "query",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "wAdequacy",
            "in": "query",
            "schema": {
              "type": "number",
              "format": "double"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/EnergyScore"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EnergyScore"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/EnergyScore"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "Service Unavailable"
          }
        }
      }
    },
    "/v1/score/forecast": {
      "get": {
        "tags": [
          "EnergyData"
        ],
        "summary": "Prévision horaire du score composite national.\nIdentifie les créneaux optimaux pour planifier recharge VE, précooling HVAC,\nprocess industriels flexibles, ou pilotage batteries stationnaires.\nL'horizon complet (48h) est réservé aux plans payants ; le plan Decouverte est limité\nà un horizon court (évaluation) — voir WattMind.Domain.Tenancy.ForecastHorizonProvider.",
        "parameters": [
          {
            "name": "hours",
            "in": "query",
            "description": "Nombre d'heures de prévision (1-48). Omis → 24h, borné au plafond du plan.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "profile",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "wPrice",
            "in": "query",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "wCarbon",
            "in": "query",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "wGrid",
            "in": "query",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "wAdequacy",
            "in": "query",
            "schema": {
              "type": "number",
              "format": "double"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ScoreForecast"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScoreForecast"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScoreForecast"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/v1/grid/status": {
      "get": {
        "tags": [
          "EnergyData"
        ],
        "summary": "Signal Ecowatt J à J+3 (tension du réseau électrique français) — données ouvertes RTE\nredistribuées telles quelles via votre clé (accès normalisé, mis en cache, résilient).\nEndpoint utilitaire de commodité, pas un produit à valeur ajoutée propre : pour piloter,\npréférez /v1/score (le signal Ecowatt y est déjà intégré au sous-score Grid).",
        "description": "Source : RTE — Ecowatt (data.rte-france.com), Licence Ouverte / Etalab 2.0.",
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "Service Unavailable"
          }
        }
      }
    },
    "/v1/tariff/today": {
      "get": {
        "tags": [
          "EnergyData"
        ],
        "summary": "Calendrier Tempo du jour (Bleu/Blanc/Rouge) — données EDF redistribuées telles quelles via\nvotre clé. Endpoint utilitaire de commodité (contrats Tempo uniquement, ~5 % du marché),\npas un produit à valeur ajoutée propre : Tempo est déjà intégré au sous-score Price de /v1/score.\nPour l'intelligence carbone générique, préférez /v1/carbon/intensity.",
        "description": "Source : EDF — Tempo (calendrier diffusé via RTE).",
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/v1/score/history": {
      "get": {
        "tags": [
          "ScoreHistory"
        ],
        "summary": "Historique des scores nationaux sur une plage de dates (série nationale, construite en continu).\nLe score étant national, aucun filtre géographique n'est applicable.",
        "description": "Ouvert à tous les plans, y compris `Decouverte` : c'est la <b>profondeur</b> accessible\nqui les distingue, pas l'accès (ADR-0001). Un `from` antérieur au plancher du plan est\nrefusé par un `403 history_depth_exceeded` — et seulement si un plan supérieur\nlivrerait effectivement davantage de données (ADR-0005).\n            \n\n<b>Deux limites distinctes, à ne pas confondre :</b><list type=\"bullet\"><item><b>Profondeur d'historique</b> — jusqu'où l'on peut remonter dans le passé.\n    <b>Dépend du plan.</b> Dépassement → `403 history_depth_exceeded`. Un plan supérieur\n    y change quelque chose.\n  </item><item><b>Fenêtre par requête</b> — combien de jours en un seul appel. <b>366 jours, identique\n    sur tous les plans</b> : c'est une borne technique protégeant la taille de la réponse, pas\n    une différenciation commerciale. Dépassement → `400 window_too_large`. Un plan\n    supérieur n'y change rien ; il faut découper en plusieurs appels.\n  </item></list>\nLa profondeur effectivement servie est le minimum entre celle du plan et celle réellement\ndisponible en base. Quand la seconde est la plus courte, la réponse est simplement\n<b>tronquée</b> (`truncated: true`, `effectiveFrom` renseigné) plutôt que refusée.\nAucun chiffre de profondeur n'est à figer côté client : `GET /api/coverage` (public, sans\nclé) expose la profondeur réellement disponible et s'approfondit d'elle-même.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "description": "Date de début (ISO 8601, ex: 2026-04-01)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Date de fin (ISO 8601, ex: 2026-04-12)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "granularity",
            "in": "query",
            "description": "`raw` (10 min, défaut sur ≤ 31 j — <b>référence auditable CSRD</b>) ou `hourly`\n             (moyenne par heure, vue de confort, défaut au-delà de 31 j). Omis → résolu automatiquement.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "profile",
            "in": "query",
            "description": "Profil de pondération métier (`green-first`, `cost-optimizer`, `ev-charging`…)\npour <b>recalculer</b>`globalScore`/`level` de chaque point selon ses priorités.\nMutuellement exclusif avec `wPrice/wCarbon/wGrid/wAdequacy`. Omis → valeurs nationales\n`balanced` stockées (référence auditable, byte-identiques au live).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "wPrice",
            "in": "query",
            "description": "Pondération prix (custom). Les 4 poids w* sont requis ensemble, somme = 1.0.",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "wCarbon",
            "in": "query",
            "description": "Pondération carbone (custom).",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "wGrid",
            "in": "query",
            "description": "Pondération réseau (custom).",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "wAdequacy",
            "in": "query",
            "description": "Pondération adéquation (custom).",
            "schema": {
              "type": "number",
              "format": "double"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ScoreHistoryResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScoreHistoryResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScoreHistoryResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/v1/score/analytics": {
      "get": {
        "tags": [
          "ScoreHistory"
        ],
        "summary": "Analytics agrégées sur une période : score moyen, meilleures/pires heures.",
        "description": "Réservé aux plans incluant les agrégats de période. Le refus décrit la forme exacte de la\nréponse (liste des champs), pour qu'un développeur puisse évaluer et câbler l'intégration\navant de souscrire plutôt qu'après.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "description": "Date de début (ISO 8601)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Date de fin (ISO 8601)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "granularity",
            "in": "query",
            "description": "Voir M:WattMind.Api.Controllers.B2B.ScoreHistoryController.GetHistory(System.DateTimeOffset,System.DateTimeOffset,System.String,System.String,System.Nullable{System.Double},System.Nullable{System.Double},System.Nullable{System.Double},System.Nullable{System.Double},System.Threading.CancellationToken) : `raw` ou `hourly` (auto si omis).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "profile",
            "in": "query",
            "description": "Profil de pondération métier pour recalculer les agrégats dépendant du `globalScore`\n(`avgGlobalScore`, `bestHours`, `optimalPct`…). Voir M:WattMind.Api.Controllers.B2B.ScoreHistoryController.GetHistory(System.DateTimeOffset,System.DateTimeOffset,System.String,System.String,System.Nullable{System.Double},System.Nullable{System.Double},System.Nullable{System.Double},System.Nullable{System.Double},System.Threading.CancellationToken).\nLes agrégats carbone et prix sont indépendants du profil.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "wPrice",
            "in": "query",
            "description": "Pondération prix (custom). Les 4 poids w* sont requis ensemble, somme = 1.0.",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "wCarbon",
            "in": "query",
            "description": "Pondération carbone (custom).",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "wGrid",
            "in": "query",
            "description": "Pondération réseau (custom).",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "wAdequacy",
            "in": "query",
            "description": "Pondération adéquation (custom).",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "timezone",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ScoreAnalyticsResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScoreAnalyticsResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScoreAnalyticsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "CarbonIntensityResponse": {
        "type": "object",
        "properties": {
          "timestamp": {
            "type": "string",
            "description": "Horodatage UTC de <b>la réponse</b>, pas de la donnée.\n\n⚠ Ce champ dit quand la réponse a été produite. Il ne dit <b>pas</b> quelle heure le mix\ndécrit : RTE construit le réalisé en H+1 (règle AGPPT-RG04) et renvoie l'heure en cours à\nzéro, donc le créneau servi a structurellement 1 à 2 h de retard. Pour attribuer cette\nintensité à une heure — une session de recharge, un créneau de production — utilisez\nWattMind.Domain.Energy.CarbonIntensity.CarbonIntensityResponse.DataSlotStart / WattMind.Domain.Energy.CarbonIntensity.CarbonIntensityResponse.DataSlotEnd, jamais ce champ.\nConservé tel quel pour ne pas casser les intégrations existantes.",
            "format": "date-time"
          },
          "dataSlotStart": {
            "type": "string",
            "description": "Début du créneau RTE <b>effectivement utilisé</b> pour calculer ce mix (UTC).\nC'est la borne à retenir pour rattacher l'intensité à une heure de consommation.\n`null` si aucun créneau réalisé n'a pu être déterminé.",
            "format": "date-time",
            "nullable": true
          },
          "dataSlotEnd": {
            "type": "string",
            "description": "Fin du créneau RTE effectivement utilisé (UTC). Les créneaux `actual_generation` sont\n<b>horaires</b> : la résolution réelle de l'intensité carbone est l'heure, quelle que soit\nla fréquence d'appel.",
            "format": "date-time",
            "nullable": true
          },
          "dataAgeMinutes": {
            "type": "integer",
            "description": "Âge de la donnée en minutes : écart entre WattMind.Domain.Energy.CarbonIntensity.CarbonIntensityResponse.DataSlotEnd et l'instant de la\nréponse. Reflète le délai de publication du réalisé par RTE, pas une latence WattMind.\nPermet de décider en connaissance de cause plutôt que de supposer du temps réel.",
            "format": "int32",
            "nullable": true
          },
          "totalGenerationMW": {
            "type": "number",
            "description": "Production totale en MW.",
            "format": "double"
          },
          "lowCarbonPct": {
            "type": "number",
            "description": "Part bas-carbone (nucléaire + ENR) en %.",
            "format": "double"
          },
          "renewablePct": {
            "type": "number",
            "description": "Part renouvelable (ENR uniquement, hors nucléaire) en %.",
            "format": "double"
          },
          "estimatedGCo2PerKwh": {
            "type": "number",
            "description": "Facteur d'émission estimé en gCO2eq/kWh (pondéré par le mix), en <b>frontière production</b> :\nhors imports, hors exports, hors pertes réseau (ADR-0009).\n\n<b>Cette valeur n'a pas changé</b> et ne changera pas du fait de l'arrivée de\nWattMind.Domain.Energy.CarbonIntensity.CarbonIntensityResponse.EstimatedGCo2PerKwhConsumption : les deux frontières coexistent, la seconde ne\ncorrige pas la première. L'une comptabilise, l'autre décrit ce qui est physiquement consommé.",
            "format": "double"
          },
          "estimatedGCo2PerKwhConsumption": {
            "type": "number",
            "description": "Facteur d'émission en <b>frontière consommation</b> (gCO2eq/kWh) : le mix de production\nnational <b>plus</b> l'électricité importée, pondérée par l'intensité carbone de son pays\nd'origine (source Ember). `null` quand il n'est pas calculable.\n            \n\n<b>Ce que cette valeur inclut et n'inclut pas.</b> Les imports, oui. Les exports, non — à\nconvention d'allocation standard, les retirer ne change pas l'intensité par kWh consommé.\nLes <b>pertes réseau, non</b> : c'est ce qui la distingue encore du facteur annuel ADEME\n(≈ 52 gCO₂eq/kWh), lui-même en frontière consommation mais <i>avec</i> pertes. Deux marches\nnettes valent mieux qu'une marche ambiguë.\n<b>Granularité hétérogène, à savoir avant de citer le chiffre</b> : le terme domestique est\nhoraire, le terme d'import est pondéré par une intensité <b>annuelle</b> par pays. C'est la\nmeilleure donnée publique disponible sur des mix étrangers, pas une intensité horaire de bout\nen bout.\n<b>`null` ne veut jamais dire « égal à la frontière production ».</b> Il signale que les\nflux physiques ou les intensités pays n'étaient pas disponibles sur ce créneau. Un créneau\nréellement sans imports sert au contraire une valeur — égale à\nWattMind.Domain.Energy.CarbonIntensity.CarbonIntensityResponse.EstimatedGCo2PerKwh, avec un WattMind.Domain.Energy.CarbonIntensity.CarbonIntensityResponse.ConsumptionBoundaryDelta à zéro.\nConfondre les deux rendrait l'indisponibilité indiscernable d'un état physique normal.",
            "format": "double",
            "nullable": true
          },
          "consumptionBoundaryDelta": {
            "type": "number",
            "description": "Écart entre les deux frontières, en gCO2eq/kWh :\n`estimatedGCo2PerKwhConsumption − estimatedGCo2PerKwh`. `null` si la valeur\nconsommation l'est.\n\n<b>Servi plutôt que laissé à calculer</b>, et calculé sur les deux valeurs <i>arrondies</i>\ntelles qu'elles sont servies : la soustraction que ferait un client donne donc exactement ce\nnombre. Le laisser dériver inviterait chacun à le recalculer un peu différemment, sur une\ngrandeur dont l'intérêt est justement d'être citable.\nPositif quand les imports sont plus carbonés que le mix français — le cas courant. Négatif\nest possible et n'est pas une anomalie : la France peut importer d'un voisin plus décarboné\nqu'elle à un instant donné.",
            "format": "double",
            "nullable": true
          },
          "unmappedImportCountries": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Pays depuis lesquels la France <b>importe sur ce créneau</b> et dont l'intensité carbone\nn'est pas connue : l'intensité de repli conservatrice leur a été appliquée dans\nWattMind.Domain.Energy.CarbonIntensity.CarbonIntensityResponse.EstimatedGCo2PerKwhConsumption.\n\nMême rôle que `unmappedProductionTypes` côté facteurs d'émission : la substitution reste\nappliquée — cette énergie a bien traversé la frontière, l'ignorer fausserait aussi le\ndénominateur — mais elle cesse d'être silencieuse. Le défaut n'était pas la valeur de repli,\nc'était le silence.\nVide en régime normal : les sept pays directement interconnectés à la France sont tous\ncatalogués. Cette liste se remplira à la première interconnexion nouvelle.",
            "nullable": true
          },
          "mix": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MixEntry"
            },
            "description": "Détail du mix de production trié par valeur décroissante.",
            "nullable": true
          },
          "methodology": {
            "type": "string",
            "description": "Méthodologie de calcul.",
            "nullable": true
          },
          "methodologyVersions": {
            "$ref": "#/components/schemas/MethodologyVersionSet"
          },
          "methodologyVersion": {
            "type": "integer",
            "description": "Version du calcul ayant produit WattMind.Domain.Energy.CarbonIntensity.CarbonIntensityResponse.EstimatedGCo2PerKwh.\n\n<b>Conservé pour ne pas casser les intégrations</b> du lot 3.a, et désormais <b>dérivé</b> de\nWattMind.Domain.Energy.CarbonIntensity.CarbonIntensityResponse.MethodologyVersions plutôt que posé indépendamment — il ne peut donc pas\ndiverger. Même arbitrage que `containsExpiredValidity` au lot 2 : la source de vérité\ndevient la structure riche, l'ancien champ en devient une vue.\nUn second entier frère à la racine pour la frontière consommation aurait été le défaut\nqu'ADR-0010 §3 interdit ; c'est WattMind.Domain.Energy.CarbonIntensity.CarbonIntensityResponse.MethodologyVersions qu'il faut lire.",
            "format": "int32",
            "readOnly": true
          },
          "emissionFactorsSource": {
            "$ref": "#/components/schemas/EmissionFactorsMetadata"
          }
        },
        "additionalProperties": false,
        "description": "Intensité carbone du réseau électrique français à un instant T.\nCalculée à partir du mix de production temps réel (RTE ActualGeneration) et des facteurs\nd'émission ADEME Base Carbone® par filière (synchronisés, licence Etalab).\nDonnées nationales — pas de dimension spatiale."
      },
      "EmissionFactorsMetadata": {
        "type": "object",
        "properties": {
          "source": {
            "type": "string",
            "description": "Source officielle des facteurs.",
            "nullable": true
          },
          "license": {
            "type": "string",
            "description": "Licence de réutilisation de la donnée.",
            "nullable": true
          },
          "lastSyncedAt": {
            "type": "string",
            "description": "Horodatage de la dernière synchronisation réussie depuis l'ADEME.",
            "format": "date-time"
          },
          "mostRecentAdemeModification": {
            "type": "string",
            "description": "Date de modification ADEME la plus récente parmi les facteurs utilisés.",
            "format": "date"
          },
          "expiredValidityFactors": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Codes filière RTE dont la période de validité ADEME est <b>échue</b> (le facteur reste\n\"Valide générique\" côté ADEME ; signalé pour transparence d'audit). Triés, stables.\n\nUn booléen disait « au moins un facteur est périmé » sans dire lequel : un auditeur ne\npouvait ni évaluer l'impact, ni vérifier. La liste le permet.",
            "nullable": true
          },
          "fallbackFactors": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Codes filière RTE servis par une valeur <b>héritée WattMind</b> plutôt que par l'ADEME —\naujourd'hui `BIOMASS` et `WASTE`, absentes de la Base Carbone au format\nkgCO2e/kWh électrique. Triés, stables.",
            "nullable": true
          },
          "unmappedProductionTypes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Filières présentes dans le mix servi mais <b>absentes du jeu de facteurs</b> : elles ont reçu\nle repli conservateur de WattMind.Domain.Energy.CarbonIntensity.EmissionFactorCatalog.UnknownGCo2PerKwh gCO₂eq/kWh.\n\nContrairement aux deux listes ci-dessus, celle-ci est <b>propre à la réponse</b> et non au\njeu de facteurs : elle dépend du mix réellement produit sur le créneau. C'est la substitution\nqui était jusqu'ici totalement silencieuse — ni les drapeaux ni les logs ne la signalaient,\nalors qu'un code filière ajouté par RTE suffit à la déclencher.",
            "nullable": true
          },
          "factorBoundaries": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Frontière ADEME de chaque facteur utilisé (code filière RTE → `Nom_frontière_français`).\n\n<b>Une carte, pas une valeur unique</b> : le jeu servi mélange volontairement plusieurs\nfrontières — le nucléaire est en « production (Parc Français) », le photovoltaïque porte une\nhypothèse de fabrication, les filières héritées n'en ont aucune. Annoncer une frontière\nglobale serait faux pour la moitié des lignes ; les nommer une par une est vérifiable.\nLe périmètre d'ensemble — production, hors imports, exports et pertes réseau — est une\ndécision produit, documentée en ADR-0009, pas une propriété du jeu de facteurs.",
            "nullable": true
          },
          "containsExpiredValidity": {
            "type": "boolean",
            "description": "True si au moins un facteur utilisé a une période de validité ADEME échue.\n<b>Dérivé</b> de WattMind.Domain.Energy.CarbonIntensity.EmissionFactorsMetadata.ExpiredValidityFactors — conservé pour ne pas casser les\nintégrations et l'observabilité qui le lisent (cf. `SourceFreshnessReport`).",
            "readOnly": true
          },
          "containsFallbackValues": {
            "type": "boolean",
            "description": "True si au moins un facteur provient d'une valeur héritée WattMind.\n<b>Dérivé</b> de WattMind.Domain.Energy.CarbonIntensity.EmissionFactorsMetadata.FallbackFactors, même raison que ci-dessus.",
            "readOnly": true
          }
        },
        "additionalProperties": false,
        "description": "Métadonnées de provenance des facteurs d'émission, exposées dans la réponse API\npour rendre `/v1/carbon/intensity` auditable (CSRD scope 2)."
      },
      "EnergyScore": {
        "required": [
          "recommendation",
          "subScores"
        ],
        "type": "object",
        "properties": {
          "computedAt": {
            "type": "string",
            "description": "Horodatage de calcul du score.",
            "format": "date-time"
          },
          "globalScore": {
            "type": "integer",
            "description": "Score global (0-100). Plus c'est haut, plus c'est favorable.",
            "format": "int32"
          },
          "level": {
            "$ref": "#/components/schemas/ScoreLevel"
          },
          "recommendation": {
            "type": "string",
            "description": "Recommandation actionnable en français.",
            "nullable": true
          },
          "subScores": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SubScore"
            },
            "description": "Décomposition par dimension (price, carbon, grid, adequacy).",
            "nullable": true
          },
          "confidence": {
            "type": "integer",
            "description": "Indicateur de confiance (0-100%) basé sur la disponibilité des sources de données.\nEn-dessous de 60%, le score est considéré comme dégradé.",
            "format": "int32"
          },
          "status": {
            "type": "string",
            "description": "Statut de service du score : \"ok\" (>=70), \"degraded\" (50-69), \"insufficient\" (<50).",
            "nullable": true
          },
          "sourceAvailability": {
            "type": "object",
            "additionalProperties": {
              "type": "boolean"
            },
            "description": "Audit de disponibilité par source de données utilisée dans le calcul.\nPermet à un client B2B de comprendre pourquoi un score est dégradé.",
            "nullable": true
          },
          "weightsApplied": {
            "$ref": "#/components/schemas/ScoreWeights"
          },
          "profileApplied": {
            "type": "string",
            "description": "Nom du profil métier appliqué (ex. \"balanced\", \"green-first\") ou \"custom\" si weights explicites.\nNull si calcul par défaut.",
            "nullable": true
          },
          "spotPriceEurPerMWh": {
            "type": "number",
            "description": "Prix spot EPEX en €/MWh ayant servi au sous-score \"price\" (null si indisponible au calcul).\nExposé pour l'affichage (ticker landing) sans avoir à parser le Detail du sous-score.",
            "format": "double",
            "nullable": true
          },
          "ecowattStatus": {
            "type": "string",
            "description": "Statut Ecowatt brut (NetworkStatus.ToString() : \"Optimal\"/\"Favorable\"/\"Tendu\"/\"TresTendu\"),\nou null si le signal Ecowatt était indisponible au calcul.",
            "nullable": true
          },
          "methodologyVersions": {
            "$ref": "#/components/schemas/MethodologyVersionSet"
          }
        },
        "additionalProperties": false,
        "description": "Score composite indiquant si le moment est favorable pour consommer de l'électricité."
      },
      "HourlyScore": {
        "required": [
          "subScores"
        ],
        "type": "object",
        "properties": {
          "time": {
            "type": "string",
            "description": "Début du créneau horaire.",
            "format": "date-time"
          },
          "globalScore": {
            "type": "integer",
            "description": "Score global (0-100).",
            "format": "int32"
          },
          "level": {
            "$ref": "#/components/schemas/ScoreLevel"
          },
          "subScores": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SubScore"
            },
            "description": "Décomposition par dimension.",
            "nullable": true
          },
          "confidence": {
            "type": "integer",
            "description": "Indicateur de confiance (0-100%) basé sur la disponibilité des sources.",
            "format": "int32"
          },
          "spotPriceEurPerMWh": {
            "type": "number",
            "description": "Prix spot EPEX (€/MWh) retenu pour ce créneau, ou `null` si aucun prix ne le couvre.\n\n<b>Le `null` est l'information utile</b> : EPEX est un marché <i>day-ahead</i>, il\nn'existe pas au-delà de la fin de J+1. Passé cette borne, le sous-score prix bascule sur son\nrepli heures creuses / heures pleines et ce champ vaut `null` — l'écart entre une heure\ntarifée et une heure estimée devient donc lisible sans interpréter la confiance.",
            "format": "double",
            "nullable": true
          },
          "carbonIntensityGco2Kwh": {
            "type": "number",
            "description": "Intensité carbone prévisionnelle du créneau en gCO₂eq/kWh, calculée sur le mix <b>projeté</b>\nde l'heure par le même code que `/v1/carbon/intensity` (mêmes facteurs ADEME, même\nfrontière production — cf. ADR-0009). `null` si aucun mix n'est projetable.\n\n<b>Limite à connaître</b> : le mix projeté combine les prévisions RTE pour l'éolien et le\nsolaire, et un <i>report</i> de la dernière production réalisée pour les filières pilotables\n(nucléaire, hydraulique, gaz). L'hypothèse d'inertie est solide à quelques heures et se\ndégrade sur l'horizon : à H+48, cette intensité décrit un réseau où seule la part\nrenouvelable a bougé. Ce n'est pas une valeur auditable — l'auditable est l'historique\nconstaté de `/v1/score/history`.",
            "format": "double",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Score composite pour une heure donnée."
      },
      "MethodologyBreakdownEntry": {
        "type": "object",
        "properties": {
          "methodologyVersions": {
            "$ref": "#/components/schemas/MethodologyVersionSet"
          },
          "sampleCount": {
            "type": "integer",
            "description": "Créneaux de la plage calculés avec ce jeu de versions.",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "Une combinaison de versions de méthodologie observée sur la plage, et le nombre de créneaux\nqu'elle couvre. Trié par nombre de créneaux décroissant : la méthodologie dominante en tête."
      },
      "MethodologyVersionSet": {
        "type": "object",
        "properties": {
          "score": {
            "type": "integer",
            "description": "Version de la formule des sous-scores et de leur agrégation.",
            "format": "int32"
          },
          "carbon": {
            "type": "integer",
            "description": "Version du calcul d'intensité carbone <b>en frontière production</b> — celle qui produit\n`estimatedGCo2PerKwh`.",
            "format": "int32"
          },
          "confidence": {
            "type": "integer",
            "description": "Version du calcul de l'indice de fiabilité.",
            "format": "int32"
          },
          "consumption": {
            "type": "integer",
            "description": "Version du calcul en <b>frontière consommation</b>, ou `null` quand le point ne porte pas de\nvaleur consommation.\n\n<b>`null` signifie « pas de valeur », jamais « version inconnue ».</b> La frontière\nconsommation est apparue en cours de série ; poser `1` sur un point antérieur prétendrait\nqu'une formule a produit une valeur qui n'existe pas — symétrique exact du piège\n`defaultValue: 0` évité au lot 3.a, où c'était la version qui était inventée. Ici c'est la\ngrandeur elle-même qui manque, et la version doit manquer avec elle.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Jeu de versions de méthodologie, sérialisé en <b>objet imbriqué</b> et non en champs frères à la\nracine de la réponse.\n            \n\n<b>Contrat d'extensibilité — à respecter côté client.</b> Cet objet peut gagner des clés le jour\noù une quatrième série devient comparable dans le temps. Un consommateur doit <b>ignorer les\nclés inconnues</b> plutôt que rejeter la réponse : un désérialiseur strict (TypeScript\n`zod.strict()`, schéma fermé) casserait sur un ajout purement additif. L'imbrication est\nprécisément là pour que cet ajout reste circonscrit et lisible, au lieu de polluer la racine."
      },
      "MixEntry": {
        "type": "object",
        "properties": {
          "productionType": {
            "type": "string",
            "nullable": true
          },
          "valueMW": {
            "type": "number",
            "format": "double"
          },
          "sharePct": {
            "type": "number",
            "format": "double"
          },
          "isLowCarbon": {
            "type": "boolean"
          }
        },
        "additionalProperties": false,
        "description": "Entrée du mix de production."
      },
      "ProblemDetails": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "nullable": true
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "detail": {
            "type": "string",
            "nullable": true
          },
          "instance": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": { }
      },
      "ScoreAnalyticsResponse": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "description": "Borne de début <b>demandée</b>, réémise telle quelle. Voir WattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.EffectiveFrom.",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "effectiveFrom": {
            "type": "string",
            "description": "Borne de début réellement agrégée — même sémantique que sur\nWattMind.Api.DTOs.B2B.ScoreHistoryResponse.EffectiveFrom. Sur des agrégats, l'écart compte double :\nune moyenne calculée sur une plage plus courte que celle demandée reste un nombre\nparfaitement plausible.",
            "format": "date-time"
          },
          "truncated": {
            "type": "boolean",
            "description": "Voir WattMind.Api.DTOs.B2B.ScoreHistoryResponse.Truncated."
          },
          "maxHistoryDays": {
            "type": "integer",
            "description": "Voir WattMind.Api.DTOs.B2B.ScoreHistoryResponse.MaxHistoryDays.",
            "format": "int32"
          },
          "granularity": {
            "type": "string",
            "description": "Granularité des points agrégés (`\"raw\"` = 10 min, référence auditable ; `\"hourly\"` =\nvue de confort). Sur une longue fenêtre, l'agrégation bascule en `\"hourly\"` pour borner\nla mémoire ; WattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.SampleCount reflète alors le nombre d'heures, pas de points 10 min.",
            "nullable": true
          },
          "profileApplied": {
            "type": "string",
            "description": "Profil de pondération appliqué pour le recalcul des agrégats dépendant du `globalScore`\n(WattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.AvgGlobalScore, WattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.BestHours, WattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.OptimalPct…) : nom du\npreset, `\"custom\"`, ou `null` si aucun (vue nationale `balanced`). Les agrégats\ncarbone et prix sont indépendants du profil.",
            "nullable": true
          },
          "seriesStart": {
            "type": "string",
            "description": "Début réel de la série nationale (ADR-0005) — même sémantique que sur\nWattMind.Api.DTOs.B2B.ScoreHistoryResponse.SeriesStart. Un `sampleCount` faible sur une longue\nplage s'explique le plus souvent par une plage antérieure à cette date.",
            "format": "date-time",
            "nullable": true
          },
          "carbonSeriesStart": {
            "type": "string",
            "description": "Début réel de la série <b>carbone</b> — plus ancien point portant une intensité carbone.\nToujours postérieur ou égal à WattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.SeriesStart : les colonnes carbone sont apparues\nen cours de série. `GET /api/coverage` expose la même borne, sans clé.",
            "format": "date-time",
            "nullable": true
          },
          "consumptionSeriesStart": {
            "type": "string",
            "description": "Début réel de la série en <b>frontière consommation</b> — plus ancien point portant une\nvaleur consommation. Toujours postérieur ou égal à WattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.CarbonSeriesStart : cette\nfrontière est apparue plus tard encore. `GET /api/coverage` expose la même borne, sans\nclé.",
            "format": "date-time",
            "nullable": true
          },
          "timeZone": {
            "type": "string",
            "description": "Fuseau dans lequel WattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.BestHours et WattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.WorstHours sont exprimées.\n\n<b>`UTC` par défaut</b>, et toujours renseigné — y compris quand il vaut `UTC`.\nPassez `?timezone=Europe/Paris` pour raisonner en heure locale. Le défaut ne changera\npas : le basculer modifierait silencieusement le sens des réponses déjà intégrées par les\nclients existants, qui verraient leurs « meilleures heures » se décaler de deux heures sans\nerreur ni changement de version.",
            "nullable": true
          },
          "sampleCount": {
            "type": "integer",
            "description": "Nombre de créneaux agrégés sur la plage.",
            "format": "int32"
          },
          "carbonSampleCount": {
            "type": "integer",
            "description": "Nombre de créneaux portant effectivement une <b>intensité carbone</b> — c'est-à-dire la\ntaille réelle de l'échantillon derrière WattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.AvgCarbonIntensityGco2Kwh et\nWattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.AvgLowCarbonPct.\n\n<b>À comparer systématiquement à WattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.SampleCount.</b> Sans cette comparaison, une\nmoyenne calculée sur un sous-ensemble se lit comme une moyenne sur la plage demandée —\nexactement l'erreur qu'une plage à cheval sur WattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.CarbonSeriesStart produit.",
            "format": "int32"
          },
          "consumptionSampleCount": {
            "type": "integer",
            "description": "Nombre de créneaux portant effectivement une valeur en <b>frontière consommation</b> —\nc'est-à-dire la taille réelle de l'échantillon derrière\nWattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.AvgCarbonIntensityConsumptionGco2Kwh et\nWattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.AvgConsumptionBoundaryDelta.\n\nÀ comparer à WattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.CarbonSampleCount autant qu'à WattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.SampleCount : sur une\nplage à cheval sur WattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.ConsumptionSeriesStart, ou sur une période où les flux RTE\nont manqué, cet échantillon est plus petit que l'échantillon carbone. Une moyenne calculée\nsur un sous-ensemble se lit sinon comme une moyenne sur la plage demandée.",
            "format": "int32"
          },
          "methodologyBreakdown": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MethodologyBreakdownEntry"
            },
            "description": "Ventilation des créneaux agrégés par jeu de versions de méthodologie.\n\n<b>Une seule entrée = période homogène</b>, les agrégats ci-dessous sont exploitables tels\nquels. <b>Plusieurs entrées = la plage est à cheval sur un changement de méthodologie</b> :\nles moyennes portent alors sur des grandeurs qui n'ont pas été calculées de la même façon.\nLa ventilation dit précisément laquelle a bougé et sur combien de créneaux.\nL'agrégation n'est volontairement <b>pas refusée</b> dans ce cas : découper soi-même reste\nun usage légitime, et un `403` casserait un client dont la plage traverse une évolution\nqui ne concerne pas la grandeur qu'il regarde. Même posture que sur la profondeur — on sert,\non déclare, le client décide.",
            "nullable": true
          },
          "avgGlobalScore": {
            "type": "number",
            "format": "double"
          },
          "avgConfidence": {
            "type": "number",
            "format": "double"
          },
          "avgSpotPriceEurMwh": {
            "type": "number",
            "format": "double"
          },
          "avgCarbonScore": {
            "type": "number",
            "description": "Moyenne du sous-score carbone (0-100) sur la période.",
            "format": "double"
          },
          "avgCarbonIntensityGco2Kwh": {
            "type": "number",
            "description": "Intensité carbone moyenne de la période en <b>gCO₂eq/kWh</b> (moyenne des créneaux où la valeur\nest disponible) — l'agrégat clé, en location-based sur le mix de production.\n`null` si aucun créneau de la période ne porte d'intensité carbone.\n\n⚠ Moyenne calculée sur les seuls créneaux <b>renseignés</b>. Comparez toujours\nWattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.CarbonSampleCount à WattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.SampleCount avant d'exploiter cette valeur :\nles colonnes carbone sont apparues en cours de série, donc une plage à cheval sur cette date\nproduit une moyenne sur un sous-ensemble.",
            "format": "double",
            "nullable": true
          },
          "avgCarbonIntensityConsumptionGco2Kwh": {
            "type": "number",
            "description": "Intensité carbone moyenne de la période en <b>frontière consommation</b> (gCO₂eq/kWh) :\nimports inclus, exports et pertes exclus. `null` si aucun créneau de la période n'en\nporte.\n\n⚠ Moyenne sur les seuls créneaux renseignés. Comparez\nWattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.ConsumptionSampleCount à WattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.SampleCount avant de l'exploiter.",
            "format": "double",
            "nullable": true
          },
          "avgConsumptionBoundaryDelta": {
            "type": "number",
            "description": "Écart moyen entre les deux frontières sur la période, en gCO₂eq/kWh.\n\n<b>Moyenne des écarts par créneau, et non écart des deux moyennes.</b> Les deux ne coïncident\nque si les deux séries portent exactement les mêmes créneaux — ce qui est faux dès qu'un\ncréneau porte l'une sans l'autre. La moyenne des écarts est calculée sur les seuls créneaux\noù les <i>deux</i> valeurs existent, donc chaque terme compare bien deux frontières du même\ninstant.\nC'est la grandeur que peu d'acteurs peuvent publier : l'écart mesuré, horaire, entre deux\nfrontières carbone sur le mix français.",
            "format": "double",
            "nullable": true
          },
          "avgLowCarbonPct": {
            "type": "number",
            "description": "Part bas-carbone (nucléaire + ENR) moyenne en %. `null` si indisponible.",
            "format": "double",
            "nullable": true
          },
          "avgRenewableSharePct": {
            "type": "number",
            "description": "Part renouvelable (ENR hors nucléaire) moyenne en %. `null` si indisponible.",
            "format": "double",
            "nullable": true
          },
          "bestHours": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int32"
            },
            "description": "Les 5 heures de la journée au meilleur score moyen, exprimées dans WattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.TimeZone.",
            "nullable": true
          },
          "worstHours": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int32"
            },
            "description": "Les 5 heures au pire score moyen, exprimées dans WattMind.Api.DTOs.B2B.ScoreAnalyticsResponse.TimeZone.",
            "nullable": true
          },
          "optimalPct": {
            "type": "number",
            "format": "double"
          },
          "unfavorablePct": {
            "type": "number",
            "format": "double"
          }
        },
        "additionalProperties": false
      },
      "ScoreForecast": {
        "required": [
          "bestHour",
          "hours",
          "recommendation",
          "worstHour"
        ],
        "type": "object",
        "properties": {
          "computedAt": {
            "type": "string",
            "description": "Horodatage du calcul de la prévision.",
            "format": "date-time"
          },
          "hours": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HourlyScore"
            },
            "description": "Scores horaires pour les prochaines heures.",
            "nullable": true
          },
          "bestHour": {
            "$ref": "#/components/schemas/HourlyScore"
          },
          "worstHour": {
            "$ref": "#/components/schemas/HourlyScore"
          },
          "recommendation": {
            "type": "string",
            "description": "Recommandation de planification basée sur la prévision.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Prévision du score composite sur les prochaines heures."
      },
      "ScoreHistoryEntry": {
        "type": "object",
        "properties": {
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "globalScore": {
            "type": "integer",
            "format": "int32"
          },
          "level": {
            "type": "string",
            "nullable": true
          },
          "confidence": {
            "type": "integer",
            "format": "int32"
          },
          "scorePrice": {
            "type": "integer",
            "format": "int32"
          },
          "scoreCarbon": {
            "type": "integer",
            "format": "int32"
          },
          "scoreGrid": {
            "type": "integer",
            "format": "int32"
          },
          "scoreAdequacy": {
            "type": "integer",
            "format": "int32"
          },
          "spotPriceEurMwh": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "renewableSharePct": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "carbonIntensityGco2Kwh": {
            "type": "number",
            "description": "Intensité carbone du mix au créneau, en <b>gCO₂eq/kWh</b> (facteurs ADEME Base Carbone®).\nC'est <b>la</b> grandeur physique exploitable de la série — au contraire de\nWattMind.Api.DTOs.B2B.ScoreHistoryEntry.ScoreCarbon (sous-score 0-100 saturé ~90 en France).\n`null` si l'intensité n'a pas pu être calculée pour ce créneau.\n\n<b>Nature de la donnée</b> : intensité <b>location-based à résolution horaire</b>, calculée\nsur le <b>mix de production</b> national (frontière production : hors imports, hors exports,\nhors pertes réseau). C'est la granularité horaire qui manque à un facteur annuel — donc une\ndonnée d'entrée pour un scope 2 ou un matching 24/7 CFE, pas un facteur « market-based » :\ncelui-ci se construit sur vos instruments contractuels (garanties d'origine, PPA) ou sur le\nmix résiduel, que WattMind ne détient pas.",
            "format": "double",
            "nullable": true
          },
          "carbonIntensityConsumptionGco2Kwh": {
            "type": "number",
            "description": "Intensité carbone du créneau en <b>frontière consommation</b> (gCO₂eq/kWh) : le mix de\nproduction <b>plus</b> l'électricité importée, pondérée par l'intensité de son pays d'origine\n(source Ember). Toujours hors pertes réseau.\n\n`null` avant l'apparition du champ — voir `consumptionSeriesStart` sur\n`/api/coverage` — et sur tout créneau où les flux physiques manquaient.\n<b>`null` ne veut jamais dire « égal à la frontière production »</b> : un créneau sans\nimports porte au contraire une valeur, égale à WattMind.Api.DTOs.B2B.ScoreHistoryEntry.CarbonIntensityGco2Kwh.",
            "format": "double",
            "nullable": true
          },
          "consumptionBoundaryDelta": {
            "type": "number",
            "description": "Écart entre les deux frontières sur ce créneau, en gCO₂eq/kWh. `null` si l'une des deux\nvaleurs manque. Servi plutôt que laissé à calculer, pour la même raison que sur\n`/v1/carbon/intensity`.",
            "format": "double",
            "nullable": true
          },
          "lowCarbonPct": {
            "type": "number",
            "description": "Part bas-carbone (nucléaire + ENR) du mix, en %. `null` si indisponible.",
            "format": "double",
            "nullable": true
          },
          "methodologyVersions": {
            "$ref": "#/components/schemas/MethodologyVersionSet"
          }
        },
        "additionalProperties": false
      },
      "ScoreHistoryResponse": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "description": "Borne de début <b>demandée</b>, réémise telle quelle. Voir WattMind.Api.DTOs.B2B.ScoreHistoryResponse.EffectiveFrom.",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "effectiveFrom": {
            "type": "string",
            "description": "Borne de début réellement parcourue. Différente de WattMind.Api.DTOs.B2B.ScoreHistoryResponse.From lorsque la plage\ndemandée précède le début de la série ; WattMind.Api.DTOs.B2B.ScoreHistoryResponse.Truncated le signale alors.",
            "format": "date-time"
          },
          "truncated": {
            "type": "boolean",
            "description": "Vrai si la plage servie est plus courte que la plage demandée, faute de donnée assez\nancienne (ADR-0005). Ce n'est <b>pas</b> une limite de plan : aucun plan n'aurait fait mieux,\nil n'y a rien à changer d'autre que la date demandée. Un refus lié au plan est un\n`403 history_depth_exceeded`, jamais un `200` tronqué."
          },
          "maxHistoryDays": {
            "type": "integer",
            "description": "Profondeur autorisée par le plan, en jours. La profondeur <i>servie</i> est le minimum de\ncette valeur et de la profondeur réellement disponible, que WattMind.Api.DTOs.B2B.ScoreHistoryResponse.SeriesStart permet\nde calculer.",
            "format": "int32"
          },
          "granularity": {
            "type": "string",
            "description": "Granularité des points retournés : `\"raw\"` (tranche de 10 min) ou `\"hourly\"`\n(moyenne agrégée par heure). <b>Seul `\"raw\"` est la donnée de référence auditable</b>\n(CSRD scope 2) : il reflète le score réellement calculé et servi en live. `\"hourly\"` est\nune vue de confort — une moyenne d'arrondis peut s'écarter de ±1 point des points raw.",
            "nullable": true
          },
          "profileApplied": {
            "type": "string",
            "description": "Profil de pondération appliqué pour recalculer `globalScore`/`level` de chaque point :\nnom du preset (`\"green-first\"`, `\"ev-charging\"`…), `\"custom\"`, ou `null` si\naucun (valeurs nationales `balanced` stockées, <b>référence auditable CSRD</b>). Les 4\nsous-scores restent ceux archivés quel que soit le profil — seul leur agrégat change.",
            "nullable": true
          },
          "seriesStart": {
            "type": "string",
            "description": "Début réel de la série nationale (ADR-0005). Permet au client de distinguer « pas de donnée\nsur cette plage » de « plage antérieure au démarrage de la série » sans avoir à deviner :\nun `from` antérieur à cette date ne peut rien renvoyer, pour aucun plan.\n`null` si la série est vide.",
            "format": "date-time",
            "nullable": true
          },
          "count": {
            "type": "integer",
            "format": "int32"
          },
          "snapshots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ScoreHistoryEntry"
            },
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ScoreLevel": {
        "enum": [
          "Optimal",
          "Acceptable",
          "Unfavorable"
        ],
        "type": "string",
        "description": "Niveau global du score énergétique."
      },
      "ScoreWeights": {
        "type": "object",
        "properties": {
          "price": {
            "type": "number",
            "format": "double"
          },
          "carbon": {
            "type": "number",
            "format": "double"
          },
          "grid": {
            "type": "number",
            "format": "double"
          },
          "adequacy": {
            "type": "number",
            "format": "double"
          }
        },
        "additionalProperties": false,
        "description": "Pondérations des 4 sous-scores composant le score global WattMind.\nSomme = 1.0 (±0.01). Chaque valeur ∈ [0, 1].\n            \nDeux modes d'utilisation :\n  - Preset métier (M:WattMind.Domain.Score.ScoreWeights.FromProfile(System.String)) : le client choisit un profil nommé\n    (balanced, green-first, data-center, ev-charging, …) qui encapsule un jeu\n    de poids cohérent avec son cas d'usage.\n  - Custom (M:WattMind.Domain.Score.ScoreWeights.Create(System.Double,System.Double,System.Double,System.Double)) : power users — validation stricte."
      },
      "SubScore": {
        "required": [
          "detail",
          "dimension"
        ],
        "type": "object",
        "properties": {
          "dimension": {
            "type": "string",
            "nullable": true
          },
          "value": {
            "type": "integer",
            "format": "int32"
          },
          "detail": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Sous-score individuel sur une dimension du score composite.\nValeur normalisée entre 0 (pire) et 100 (meilleur)."
      }
    },
    "securitySchemes": {
      "ApiKey": {
        "type": "apiKey",
        "description": "Votre clé API WattMind (format: wm_xxxxxxxx)",
        "name": "X-Api-Key",
        "in": "header"
      }
    }
  },
  "security": [
    { }
  ],
  "tags": [
    {
      "name": "CarbonIntensity"
    },
    {
      "name": "EnergyData"
    },
    {
      "name": "ScoreHistory"
    }
  ]
}