{
  "openapi": "3.1.0",
  "info": {
    "title": "Le Vent API",
    "version": "1.0.0",
    "summary": "Vent, houle et marées en temps réel — 13 000+ stations dans le monde",
    "description": "API REST en lecture seule de Le Vent (levent.live). Documentation : https://levent.live/developers\n\nUnités : vent en nœuds, directions en degrés (provenance, 0 = nord), températures en °C, pression en hPa, hauteurs en m, heures ISO 8601 UTC. Identifiant à utiliser : stableId (<source>_<id>).\n\nLicences : afficher le champ `attribution` quand il est présent et citer la source ; les réseaux privés et communautaires ne sont pas sous licence ouverte (voir la documentation).",
    "contact": {
      "name": "Le Vent",
      "email": "contact@levent.live",
      "url": "https://levent.live/developers"
    }
  },
  "servers": [
    {
      "url": "https://api.levent.live/api"
    }
  ],
  "security": [
    {
      "ApiKeyHeader": []
    },
    {
      "ApiKeyQuery": []
    }
  ],
  "tags": [
    {
      "name": "Stations",
      "description": "Stations vent temps réel et historique"
    },
    {
      "name": "Bouées",
      "description": "Bouées houlographes"
    },
    {
      "name": "Marées",
      "description": "Ports SHOM et ~2 000 ports dans le monde, coefficients"
    },
    {
      "name": "Prévisions",
      "description": "Prévisions Open-Meteo (AROME, ECMWF, GFS, ICON, vagues)"
    },
    {
      "name": "Vent animé",
      "description": "Grilles de vent mondiales (public, sans clé)"
    },
    {
      "name": "Webcams",
      "description": "Webcams de spots (métadonnées)"
    }
  ],
  "externalDocs": {
    "description": "Documentation",
    "url": "https://levent.live/developers"
  },
  "paths": {
    "/all-stations": {
      "get": {
        "tags": [
          "Stations"
        ],
        "operationId": "getAllStations",
        "summary": "Toutes les stations vent (ou historique d’une station avec ?history=)",
        "description": "Sans filtre : ~13 000 stations, ~3,8 Mo de JSON transmis compressé (br ~0,46 Mo / gzip ~0,54 Mo), ETag stable jusqu’à la prochaine reconstruction de la liste (toutes les 2 min). Avec `history` : mesures récentes d’une station (réponse StationHistory).",
        "parameters": [
          {
            "name": "source",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Source(s), séparées par des virgules (ex. meteofrance,pioupiou)"
          },
          {
            "name": "bbox",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "latMin,lonMin,latMax,lonMax",
            "example": "47.2,-5,48.9,-1"
          },
          {
            "name": "online",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            },
            "description": "Stations en ligne uniquement"
          },
          {
            "name": "history",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "stableId de la station : renvoie son historique",
            "example": "meteofrance_29075001"
          },
          {
            "name": "hours",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 168,
              "default": 6
            },
            "description": "Profondeur de l’historique (avec history)"
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "schema": {
              "type": "string"
            },
            "description": "ETag d’une réponse précédente : 304 si rien n’a changé"
          }
        ],
        "responses": {
          "200": {
            "description": "Liste des stations, ou historique (avec history)",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/StationList"
                    },
                    {
                      "$ref": "#/components/schemas/StationHistory"
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "description": "Inchangé depuis l’ETag envoyé dans If-None-Match (corps vide)"
          },
          "401": {
            "description": "Clé API absente",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Clé invalide, ou origine navigateur non autorisée pour cette clé",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Station inconnue (history)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Limite par minute de la clé dépassée",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Source amont injoignable (history)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/stations-lite": {
      "get": {
        "tags": [
          "Stations"
        ],
        "operationId": "getStationsLite",
        "summary": "Sélection légère de stations (ids, zone, rayon, source, nouveautés, champs)",
        "description": "Mêmes stations que /all-stations. Au moins un filtre parmi ids, bbox, near, source, since est requis. L’ETag dépend de la version de la liste et de la requête.",
        "parameters": [
          {
            "name": "ids",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Jusqu’à 200 stableId séparés par des virgules ; réponse dans l’ordre demandé, introuvables dans `missing`",
            "example": "meteofrance_29075001,holfuy_1062"
          },
          {
            "name": "bbox",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "latMin,lonMin,latMax,lonMax (lonMin > lonMax : traverse l’antiméridien)"
          },
          {
            "name": "near",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "lat,lon : stations les plus proches d’abord, avec distanceKm",
            "example": "48.38,-4.49"
          },
          {
            "name": "radius",
            "in": "query",
            "schema": {
              "type": "number",
              "exclusiveMinimum": 0,
              "maximum": 300,
              "default": 25
            },
            "description": "Rayon autour de near (km)"
          },
          {
            "name": "source",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Source(s), séparées par des virgules"
          },
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Stations mesurées à cette heure ou après (ts ≥ since)"
          },
          {
            "name": "online",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true",
                "0",
                "false"
              ]
            },
            "description": "Stations en ligne uniquement (1 / true)"
          },
          {
            "name": "fields",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Champs à renvoyer, séparés par des virgules (stableId et attribution toujours inclus)",
            "example": "name,wind,gust,direction,ts"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5000
            },
            "description": "Nombre maximal de stations"
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "schema": {
              "type": "string"
            },
            "description": "ETag d’une réponse précédente : 304 si rien n’a changé"
          }
        ],
        "responses": {
          "200": {
            "description": "Stations sélectionnées",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StationLiteList"
                }
              }
            }
          },
          "304": {
            "description": "Inchangé depuis l’ETag envoyé dans If-None-Match (corps vide)"
          },
          "400": {
            "description": "Paramètre invalide ou aucun filtre",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Clé invalide, ou origine navigateur non autorisée pour cette clé",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Limite par minute de la clé dépassée",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Liste pas encore disponible (redémarrage)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/all-buoys": {
      "get": {
        "tags": [
          "Bouées"
        ],
        "operationId": "getAllBuoys",
        "summary": "Toutes les bouées houlographes (ou historique avec ?history=)",
        "parameters": [
          {
            "name": "source",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Réseau(x) : candhis, ndbc, cefas, rws, irishlights, imi, ispra, arpae, smhi, fmi, vegagerdin, metno, aodn, queensland, siw"
          },
          {
            "name": "region",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Région (insensible à la casse), ex. mediterranee, bretagne, Pays-Bas"
          },
          {
            "name": "bbox",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "latMin,lonMin,latMax,lonMax"
          },
          {
            "name": "history",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Identifiant de la bouée : renvoie ~7 jours de mesures",
            "example": "rws_a12"
          }
        ],
        "responses": {
          "200": {
            "description": "Liste des bouées, ou historique (avec history)",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/BuoyList"
                    },
                    {
                      "$ref": "#/components/schemas/BuoyHistory"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Clé invalide, ou origine navigateur non autorisée pour cette clé",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Limite par minute de la clé dépassée",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Réseau amont injoignable (history)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/tide": {
      "get": {
        "tags": [
          "Marées"
        ],
        "operationId": "getTide",
        "summary": "Marées d’un port (par position ou par code) ou liste des ports",
        "description": "Ports SHOM (France) et ~2 000 ports dans le monde calculés par Le Vent. Prédictions indicatives, ne pas utiliser pour la navigation. Une indisponibilité de la source peut être renvoyée en 200 avec un champ `error`.",
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "schema": {
              "type": "number"
            },
            "description": "Latitude : port le plus proche"
          },
          {
            "name": "lon",
            "in": "query",
            "schema": {
              "type": "number"
            },
            "description": "Longitude : port le plus proche"
          },
          {
            "name": "maxKm",
            "in": "query",
            "schema": {
              "type": "number",
              "default": 100,
              "maximum": 1000
            },
            "description": "Distance maximale au port (avec lat/lon)"
          },
          {
            "name": "scope",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "world",
                "fr",
                "all"
              ]
            },
            "description": "world : ports calculés par Le Vent ; fr : ports SHOM ; all : les deux (défaut avec lat/lon ; défaut fr pour list)"
          },
          {
            "name": "port",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Code (cst) ou nom du port"
          },
          {
            "name": "list",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            },
            "description": "Liste des ports"
          },
          {
            "name": "date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Premier jour (heure locale du port), défaut : aujourd’hui"
          },
          {
            "name": "duration",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 35,
              "default": 7
            },
            "description": "Nombre de jours"
          }
        ],
        "responses": {
          "200": {
            "description": "Marées du port, ou liste des ports (list=true)",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/TideResponse"
                    },
                    {
                      "$ref": "#/components/schemas/TidePortList"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "lat/lon invalides",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Clé invalide, ou origine navigateur non autorisée pour cette clé",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Aucun port à moins de maxKm, ou port inconnu",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Limite par minute de la clé dépassée",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/forecast": {
      "get": {
        "tags": [
          "Prévisions"
        ],
        "operationId": "getForecast",
        "summary": "Prévisions au format Open-Meteo (modèle à indiquer)",
        "description": "Réponse identique à Open-Meteo (latitude, longitude, utc_offset_seconds, timezone, hourly_units, hourly, daily_units, daily). Coordonnées arrondies à 0,01°. Vent en km/h par défaut. Crédit : Open-Meteo.com (CC BY 4.0) et le modèle.",
        "parameters": [
          {
            "name": "model",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "arome",
                "ecmwf",
                "gfs",
                "icon",
                "marine"
              ],
              "default": "arome"
            },
            "description": "Modèle (arome : 4 j, ecmwf : 10 j, gfs : 16 j, icon : 7 j, marine : vagues 7 j)"
          },
          {
            "name": "latitude",
            "in": "query",
            "schema": {
              "type": "number",
              "minimum": -90,
              "maximum": 90
            },
            "description": "Latitude (alias lat)",
            "required": true
          },
          {
            "name": "longitude",
            "in": "query",
            "schema": {
              "type": "number",
              "minimum": -180,
              "maximum": 180
            },
            "description": "Longitude (alias lon)",
            "required": true
          },
          {
            "name": "hourly",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Variables horaires, séparées par des virgules (voir la documentation)",
            "example": "wind_speed_10m,wind_gusts_10m,wind_direction_10m"
          },
          {
            "name": "daily",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Variables quotidiennes"
          },
          {
            "name": "wind_speed_unit",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "kmh",
                "kn",
                "ms",
                "mph"
              ],
              "default": "kmh"
            },
            "description": "Unité du vent"
          },
          {
            "name": "timezone",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "GMT",
                "UTC",
                "Europe/Paris",
                "auto"
              ],
              "default": "GMT"
            },
            "description": "Fuseau des heures renvoyées"
          },
          {
            "name": "forecast_days",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 16
            },
            "description": "Jours de prévision (borné par la portée du modèle)"
          },
          {
            "name": "past_days",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 1,
              "default": 0
            },
            "description": "Jours passés"
          }
        ],
        "responses": {
          "200": {
            "description": "Prévision (format Open-Meteo)",
            "headers": {
              "X-Cache": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "HIT",
                    "MISS",
                    "STALE",
                    "FREE"
                  ]
                }
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "description": "Paramètre ou variable non pris en charge",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Clé invalide, ou origine navigateur non autorisée pour cette clé",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Limite par minute de la clé dépassée",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Prévision indisponible",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Quota journalier atteint (Retry-After)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/webcams": {
      "get": {
        "tags": [
          "Webcams"
        ],
        "operationId": "getWebcams",
        "summary": "Webcams en ligne (métadonnées et URL des images)",
        "description": "Les images appartiennent à leurs exploitants : leur affichage dans un autre service nécessite l’accord de l’exploitant.",
        "responses": {
          "200": {
            "description": "Tableau de webcams",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Webcam"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Clé invalide, ou origine navigateur non autorisée pour cette clé",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Limite par minute de la clé dépassée",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/wind-world": {
      "get": {
        "tags": [
          "Vent animé"
        ],
        "operationId": "getWindWorld",
        "summary": "Vent à 10 m prévu sur grille mondiale : index, tuiles binaires, format web",
        "description": "Public (sans clé). Sans run : index JSON du run courant. Avec run + step + tile=X_Y : tuile binaire WWND (gzip, immuable). Avec overview=1 : grille grossière. format=web&t=H : JSON WindGL de l’échéance la plus proche de « heure courante + H ». Crédits : ECMWF (CC BY 4.0), NOAA GFS, DWD ICON-EU.",
        "security": [],
        "parameters": [
          {
            "name": "model",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "ecmwf",
                "gfs",
                "icon_eu"
              ],
              "default": "ecmwf"
            },
            "description": "Modèle"
          },
          {
            "name": "run",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Identifiant du run (index.run), ex. 2026101500"
          },
          {
            "name": "step",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 384
            },
            "description": "Échéance en heures après le run (multiple de 3)"
          },
          {
            "name": "tile",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Tuile X_Y (0 ≤ X < tilesX, 0 ≤ Y < tilesY)"
          },
          {
            "name": "overview",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            },
            "description": "Grille globale grossière"
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "web"
              ]
            },
            "description": "JSON pour WindGL"
          },
          {
            "name": "t",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": -72,
              "maximum": 240
            },
            "description": "Décalage en heures par rapport à l’heure courante (format=web)"
          }
        ],
        "responses": {
          "200": {
            "description": "Index JSON, JSON web ou binaire WWND",
            "headers": {
              "X-Wind-Model": {
                "schema": {
                  "type": "string"
                },
                "description": "Modèle ayant répondu"
              },
              "X-Wind-Fallback": {
                "schema": {
                  "type": "string"
                },
                "description": "Présent si un modèle de repli a répondu (format=web)"
              },
              "X-Wind-Run": {
                "schema": {
                  "type": "string"
                }
              },
              "X-Wind-Valid-Time": {
                "schema": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WindWorldIndex"
                }
              },
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "application/octet-stream",
                  "description": "WWND v1 : en-tête 40 octets puis Int16 u[nx·ny], Int16 v[nx·ny]"
                }
              }
            }
          },
          "400": {
            "description": "Modèle, run, step ou tuile invalide",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Run remplacé, fichier ou données absents",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key"
      },
      "ApiKeyQuery": {
        "type": "apiKey",
        "in": "query",
        "name": "apikey"
      }
    },
    "headers": {
      "X-RateLimit-Limit": {
        "schema": {
          "type": "integer"
        },
        "description": "Requêtes autorisées par minute pour la clé"
      },
      "X-RateLimit-Remaining": {
        "schema": {
          "type": "integer"
        },
        "description": "Requêtes restantes dans la minute en cours"
      },
      "X-RateLimit-Reset": {
        "schema": {
          "type": "integer"
        },
        "description": "Secondes avant la remise à zéro du compteur"
      },
      "Retry-After": {
        "schema": {
          "type": "integer"
        },
        "description": "Secondes à attendre"
      },
      "ETag": {
        "schema": {
          "type": "string"
        },
        "description": "À renvoyer dans If-None-Match"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "hint": {
            "type": "string"
          },
          "retryAfter": {
            "type": "integer"
          }
        }
      },
      "Station": {
        "type": "object",
        "required": [
          "id",
          "stableId",
          "name",
          "lat",
          "lon",
          "wind",
          "gust",
          "direction",
          "source",
          "ts",
          "isOnline"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Identifiant brut chez la source"
          },
          "stableId": {
            "type": "string",
            "description": "<source>_<id>, unique",
            "examples": [
              "meteofrance_29075001"
            ]
          },
          "name": {
            "type": "string"
          },
          "lat": {
            "type": "number",
            "description": "Latitude WGS84"
          },
          "lon": {
            "type": "number",
            "description": "Longitude WGS84"
          },
          "wind": {
            "type": "number",
            "description": "Vent moyen, nœuds"
          },
          "gust": {
            "type": "number",
            "description": "Rafale max, nœuds (= wind ou 0 si non mesurée)"
          },
          "direction": {
            "type": "integer",
            "minimum": 0,
            "maximum": 359,
            "description": "Direction d’où vient le vent, degrés"
          },
          "source": {
            "type": "string",
            "description": "Source (nouvelles valeurs possibles)",
            "examples": [
              "meteofrance",
              "metnorway",
              "holfuy"
            ]
          },
          "ts": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Heure de la mesure, UTC"
          },
          "isOnline": {
            "type": "boolean"
          },
          "temperature": {
            "type": [
              "number",
              "null"
            ],
            "description": "°C"
          },
          "humidity": {
            "type": [
              "number",
              "null"
            ],
            "description": "%"
          },
          "pressure": {
            "type": [
              "number",
              "null"
            ],
            "description": "hPa"
          },
          "altitude": {
            "type": [
              "number",
              "null"
            ],
            "description": "m"
          },
          "attribution": {
            "type": "string",
            "description": "Mention exigée par la licence de la source, à afficher (facultatif)"
          }
        }
      },
      "StationLite": {
        "description": "Station (tous les champs, ou ceux de `fields` + stableId + attribution), avec distanceKm pour near",
        "type": "object",
        "required": [
          "stableId"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "stableId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lon": {
            "type": "number"
          },
          "wind": {
            "type": "number"
          },
          "gust": {
            "type": "number"
          },
          "direction": {
            "type": "integer"
          },
          "source": {
            "type": "string"
          },
          "ts": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "isOnline": {
            "type": "boolean"
          },
          "temperature": {
            "type": [
              "number",
              "null"
            ]
          },
          "humidity": {
            "type": [
              "number",
              "null"
            ]
          },
          "pressure": {
            "type": [
              "number",
              "null"
            ]
          },
          "altitude": {
            "type": [
              "number",
              "null"
            ]
          },
          "attribution": {
            "type": "string"
          },
          "distanceKm": {
            "type": "number",
            "description": "Distance au point near (km)"
          }
        }
      },
      "StationList": {
        "type": "object",
        "required": [
          "stations",
          "count",
          "cachedAt",
          "refreshInterval"
        ],
        "properties": {
          "stations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Station"
            }
          },
          "count": {
            "type": "integer"
          },
          "sources": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Stations par flux amont (kv-stations = services nationaux)"
          },
          "cachedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Reconstruction de la liste"
          },
          "refreshInterval": {
            "type": "integer",
            "description": "Secondes entre deux reconstructions (120)"
          }
        }
      },
      "StationLiteList": {
        "type": "object",
        "required": [
          "stations",
          "count",
          "cachedAt",
          "refreshInterval"
        ],
        "properties": {
          "stations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StationLite"
            }
          },
          "count": {
            "type": "integer"
          },
          "cachedAt": {
            "type": "string",
            "format": "date-time"
          },
          "refreshInterval": {
            "type": "integer"
          },
          "missing": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "ids demandés introuvables (avec ids)"
          },
          "truncated": {
            "type": "boolean",
            "description": "Réponse coupée par limit"
          }
        }
      },
      "Observation": {
        "type": "object",
        "properties": {
          "ts": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "wind": {
            "type": "number"
          },
          "gust": {
            "type": "number"
          },
          "direction": {
            "type": "integer"
          },
          "temperature": {
            "type": [
              "number",
              "null"
            ]
          }
        }
      },
      "StationHistory": {
        "type": "object",
        "required": [
          "stationId",
          "name",
          "source",
          "hours",
          "count",
          "observations"
        ],
        "properties": {
          "stationId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "hours": {
            "type": "integer"
          },
          "count": {
            "type": "integer"
          },
          "observations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Observation"
            }
          },
          "attribution": {
            "type": "string"
          }
        }
      },
      "Buoy": {
        "type": "object",
        "required": [
          "id",
          "name",
          "lat",
          "lon",
          "source"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "CANDHIS : numérique (02911) ; autres : <réseau>_<id>"
          },
          "name": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lon": {
            "type": "number"
          },
          "region": {
            "type": [
              "string",
              "null"
            ]
          },
          "depth": {
            "type": [
              "number",
              "null"
            ],
            "description": "m"
          },
          "hm0": {
            "type": [
              "number",
              "null"
            ],
            "description": "Hauteur significative, m"
          },
          "hmax": {
            "type": [
              "number",
              "null"
            ],
            "description": "Hauteur maximale, m"
          },
          "tp": {
            "type": [
              "number",
              "null"
            ],
            "description": "Période pic, s"
          },
          "direction": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Direction d’où vient la houle, degrés"
          },
          "spread": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Étalement directionnel, degrés"
          },
          "seaTemp": {
            "type": [
              "number",
              "null"
            ],
            "description": "°C"
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "description": "online / offline (CANDHIS : état de diffusion)"
          },
          "lastUpdate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "source": {
            "type": "string"
          },
          "attribution": {
            "type": [
              "string",
              "null"
            ],
            "description": "Mention exigée par la licence du réseau"
          }
        }
      },
      "BuoyList": {
        "type": "object",
        "properties": {
          "buoys": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Buoy"
            }
          },
          "count": {
            "type": "integer"
          },
          "sources": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "cachedAt": {
            "type": "string",
            "format": "date-time"
          },
          "refreshInterval": {
            "type": "integer"
          }
        }
      },
      "BuoyHistory": {
        "type": "object",
        "properties": {
          "buoyId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "region": {
            "type": [
              "string",
              "null"
            ]
          },
          "count": {
            "type": "integer"
          },
          "observations": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "timestamp": {
                  "type": "string",
                  "format": "date-time"
                },
                "hm0": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "hmax": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "tp": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "direction": {
                  "type": [
                    "integer",
                    "null"
                  ]
                },
                "seaTemp": {
                  "type": [
                    "number",
                    "null"
                  ]
                }
              }
            }
          },
          "attribution": {
            "type": "string"
          }
        }
      },
      "TidePort": {
        "type": "object",
        "properties": {
          "cst": {
            "type": "string",
            "description": "Code du port"
          },
          "name": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lon": {
            "type": "number"
          },
          "region": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "countryName": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "model": {
            "type": "boolean"
          }
        }
      },
      "TideEvent": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "high",
              "low"
            ]
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "Jour local du port"
          },
          "time": {
            "type": "string",
            "description": "HH:MM local"
          },
          "datetime": {
            "type": "string",
            "format": "date-time",
            "description": "Instant UTC"
          },
          "height": {
            "type": "number",
            "description": "m au-dessus du zéro des cartes"
          },
          "coefficient": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Pleines mers uniquement (20–120)"
          }
        }
      },
      "TideResponse": {
        "type": "object",
        "properties": {
          "port": {
            "$ref": "#/components/schemas/TidePort"
          },
          "distanceKm": {
            "type": "number",
            "description": "Avec lat/lon"
          },
          "tides": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TideEvent"
            }
          },
          "nextHighTide": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "time": {
                "type": "string",
                "format": "date-time"
              },
              "height": {
                "type": "number"
              },
              "coefficient": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          },
          "nextLowTide": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "time": {
                "type": "string",
                "format": "date-time"
              },
              "height": {
                "type": "number"
              }
            }
          },
          "todayCoefficient": {
            "type": [
              "integer",
              "null"
            ]
          },
          "coefficientSource": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "shom",
              "computed",
              null
            ],
            "description": "shom : officiel ; computed : calculé par Le Vent (équivalent SHOM, non officiel)"
          },
          "coefficientReason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "diurnal",
              "small-tide",
              null
            ]
          },
          "coefficientNote": {
            "type": [
              "string",
              "null"
            ],
            "description": "Phrase prête à afficher"
          },
          "source": {
            "type": "string"
          },
          "sourceLabel": {
            "type": "string"
          },
          "model": {
            "type": "boolean"
          },
          "fallback": {
            "type": "boolean"
          },
          "datum": {
            "type": "string",
            "description": "Zéro des cartes (LAT, MLLW…)"
          },
          "timezone": {
            "type": "string"
          },
          "attribution": {
            "type": [
              "string",
              "null"
            ],
            "description": "Crédit de licence à afficher"
          },
          "fetchedAt": {
            "type": "string",
            "format": "date-time"
          },
          "error": {
            "type": "string",
            "description": "Présent si la source est indisponible"
          }
        }
      },
      "TidePortList": {
        "type": "object",
        "properties": {
          "ports": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TidePort"
            }
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "Webcam": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "location": {
            "type": "string"
          },
          "region": {
            "type": "string"
          },
          "latitude": {
            "type": "number"
          },
          "longitude": {
            "type": "number"
          },
          "imageUrl": {
            "type": "string"
          },
          "streamUrl": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "type": "string",
            "description": "Exploitant"
          },
          "refreshInterval": {
            "type": "integer",
            "description": "s"
          },
          "lastCapture": {
            "type": [
              "string",
              "null"
            ]
          },
          "provider": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "linkUrl": {
            "type": "string"
          },
          "attribution": {}
        }
      },
      "WindWorldIndex": {
        "type": "object",
        "properties": {
          "model": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "attribution": {
            "type": "string"
          },
          "run": {
            "type": "string"
          },
          "runTime": {
            "type": "integer",
            "description": "Unix s"
          },
          "generated": {
            "type": "string",
            "format": "date-time"
          },
          "grid": {
            "$ref": "#/components/schemas/WindGrid"
          },
          "overview": {
            "$ref": "#/components/schemas/WindGrid"
          },
          "tileCells": {
            "type": "integer"
          },
          "tilesX": {
            "type": "integer"
          },
          "tilesY": {
            "type": "integer"
          },
          "frames": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "step": {
                  "type": "integer"
                },
                "validTime": {
                  "type": "integer",
                  "description": "Unix s"
                }
              }
            }
          }
        }
      },
      "WindGrid": {
        "type": "object",
        "properties": {
          "lon0": {
            "type": "number"
          },
          "lat0": {
            "type": "number"
          },
          "dLon": {
            "type": "number"
          },
          "dLat": {
            "type": "number"
          },
          "nx": {
            "type": "integer"
          },
          "ny": {
            "type": "integer"
          },
          "wrapsX": {
            "type": "boolean"
          }
        }
      }
    }
  }
}
