{
  "openapi": "3.1.0",
  "info": {
    "title": "madeira.fun Open Data",
    "version": "1.1.0",
    "summary": "Madeira hiking permit availability and live weather, free to reuse under CC BY 4.0.",
    "description": "Open data feeds published by madeira.fun, an independent travel service for Madeira, Portugal. Free, no key, no sign-up, open CORS.\n\nFormats: the permit feeds carry the format id MF-HAF-1.1 (Permit Data Standard); the weather feed carries MF-WX-1.0. Permit fees and rule changes are published as MF-PRC-1.0 records on https://madeira.fun/hiking/permit-data#rule-changes.\n\nGranularity: the open data is per day. No feed lists individual start times; exact start times are shown on each trail's page on madeira.fun.\n\nBooking: link a visitor to https://madeira.fun/hiking/{seo_slug}?date=YYYY-MM-DD. Optional query parameters: adults and children (non-resident visitor counts; children are 12 and under), lang (one of the 18 site languages) and utm_source (the name of your app). madeira.fun is a reservation service: the visitor picks a start time and presses Reserve, madeira.fun fills in and submits the government reservation on Simplifica and emails the government's PayPay payment link, and the visitor pays the permit fee to the government through that link. madeira.fun never collects the permit fee. Visitors can also book on Simplifica themselves.\n\nReuse: CC BY 4.0. Attribution: \"According to Madeira Fun hiking permit availability data (https://madeira.fun/hiking/permit-data)\" for the permit feeds and \"According to Madeira Fun live weather data (https://madeira.fun/weather/data)\" for the weather feed.\n\nCaching and limits: responses are cached for about 5 minutes and carry ETag and Last-Modified; send If-None-Match or If-Modified-Since to get 304 Not Modified. Please cache, poll no faster than the data refreshes, and send a User-Agent that names your app. Request limits exist to protect the service and the government booking system; they are not published.\n\nVersioning: within a format version fields are only added, never renamed, removed or retyped, and CSV columns are only appended. Ignore fields you do not know. A breaking change gets a new format version, and the previous version stays available for at least 6 months after the new one ships.\n\nFreshness: every trail record carries observed_at, every day carries checked_at, and the permit feed carries summary_refreshed_at. There is no separate status endpoint.\n\nA read-only MCP server with the same data is at https://madeira.fun/api/mcp (Streamable HTTP).",
    "termsOfService": "https://madeira.fun/terms#open-data",
    "contact": {
      "name": "madeira.fun data",
      "url": "https://madeira.fun/hiking/permit-data#open-data",
      "email": "support@madeira.fun"
    },
    "license": {
      "name": "CC BY 4.0 (the data)",
      "identifier": "CC-BY-4.0"
    }
  },
  "externalDocs": {
    "description": "Open Data documentation, methodology and rule changes",
    "url": "https://madeira.fun/hiking/permit-data#open-data"
  },
  "servers": [
    { "url": "https://madeira.fun" }
  ],
  "tags": [
    { "name": "hiking-permits", "description": "Madeira hiking permit availability (MF-HAF-1.1). Methodology: https://madeira.fun/hiking/permit-data" },
    { "name": "weather", "description": "Live weather, webcam and crowd observations by place (MF-WX-1.0). Methodology: https://madeira.fun/weather/data" }
  ],
  "x-mcp-server": {
    "url": "https://madeira.fun/api/mcp",
    "transport": "streamable-http",
    "description": "Read-only MCP server with the same per-day data, the permit fee records and a booking link builder."
  },
  "paths": {
    "/api/public/hiking-availability": {
      "get": {
        "operationId": "getHikingAvailability",
        "tags": ["hiking-permits"],
        "summary": "Permit availability for every permit trail (JSON)",
        "description": "One record per permit trail: official status, earliest open date, open days and booking pressure, each with its observation time; for a trail that can be booked, also the next 7 days (days_7d) and the address of its day feed (day_feed_url). Both are null for a trail that cannot be booked. If the 1.1 additions cannot be read for a response, the 1.0 fields are still served and the 1.1 fields that come from that read are null (status_pt, status_since, status_source_url, closures, days_7d, day_feed_url, walked_km, route_type, start, end, normal_time_minutes, sunrise, sunset). available_days_7d / _14d / _30d keep their MF-HAF-1.0 meaning (a day counts when it has at least one observed bookable visitor start time); the day states in days_7d use the stricter rule described under DayStatus.",
        "parameters": [
          { "$ref": "#/components/parameters/IfNoneMatch" },
          { "$ref": "#/components/parameters/IfModifiedSince" }
        ],
        "responses": {
          "200": {
            "description": "The feed.",
            "headers": {
              "ETag": { "$ref": "#/components/headers/ETag" },
              "Last-Modified": { "$ref": "#/components/headers/LastModified" },
              "Cache-Control": { "$ref": "#/components/headers/CacheControl" },
              "X-License": { "$ref": "#/components/headers/XLicense" }
            },
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/HikingAvailabilityFeed" } }
            }
          },
          "304": { "$ref": "#/components/responses/NotModified" },
          "405": { "$ref": "#/components/responses/FeedMethodNotAllowed" },
          "500": { "$ref": "#/components/responses/FeedInternalError" },
          "503": { "$ref": "#/components/responses/FeedUnavailable" }
        }
      },
      "head": {
        "operationId": "headHikingAvailability",
        "tags": ["hiking-permits"],
        "summary": "Headers of the permit feed (JSON)",
        "responses": {
          "200": { "$ref": "#/components/responses/HeadOk" },
          "304": { "$ref": "#/components/responses/NotModified" },
          "503": { "description": "Temporarily unavailable." }
        }
      }
    },
    "/api/public/hiking-availability.csv": {
      "get": {
        "operationId": "getHikingAvailabilityCsv",
        "tags": ["hiking-permits"],
        "summary": "Permit availability for every permit trail (CSV)",
        "description": "The same records as the JSON feed, one row per trail, in JSON trails[] order. A # comment preamble carries the dataset name, license, attribution and the last-checked time (UTC). RFC 4180 quoting. Columns are append-only: the MF-HAF-1.0 columns listed in x-csv-columns come first in that order; MF-HAF-1.1 appends its scalar trail fields after observed_at, with days_7d written as days_7d_states (the seven day states joined by ;). Nested fields (closures, days_7d, start, end) are not repeated as objects in the CSV.",
        "parameters": [
          { "$ref": "#/components/parameters/IfNoneMatch" },
          { "$ref": "#/components/parameters/IfModifiedSince" }
        ],
        "responses": {
          "200": {
            "description": "The feed as CSV.",
            "headers": {
              "ETag": { "$ref": "#/components/headers/ETag" },
              "Last-Modified": { "$ref": "#/components/headers/LastModified" },
              "Cache-Control": { "$ref": "#/components/headers/CacheControl" },
              "X-License": { "$ref": "#/components/headers/XLicense" }
            },
            "content": {
              "text/csv": {
                "schema": { "type": "string" },
                "x-csv-columns": [
                  "code", "name", "seo_slug", "url", "official_status",
                  "earliest_available_date", "earliest_available_time",
                  "available_days_7d", "available_days_14d", "available_days_30d",
                  "morning_available", "afternoon_available", "weekend_available", "same_day_available",
                  "slot_density_14d", "booking_pressure_score", "booking_pressure_label",
                  "observed_at"
                ]
              }
            }
          },
          "304": { "$ref": "#/components/responses/NotModified" },
          "405": { "$ref": "#/components/responses/FeedMethodNotAllowed" },
          "500": { "$ref": "#/components/responses/FeedInternalError" },
          "503": { "$ref": "#/components/responses/FeedUnavailable" }
        }
      },
      "head": {
        "operationId": "headHikingAvailabilityCsv",
        "tags": ["hiking-permits"],
        "summary": "Headers of the permit feed (CSV)",
        "responses": {
          "200": { "$ref": "#/components/responses/HeadOk" },
          "304": { "$ref": "#/components/responses/NotModified" },
          "503": { "description": "Temporarily unavailable." }
        }
      }
    },
    "/api/public/hiking-availability/{seo_slug}": {
      "parameters": [ { "$ref": "#/components/parameters/SeoSlug" } ],
      "get": {
        "operationId": "getTrailDays",
        "tags": ["hiking-permits"],
        "summary": "One trail, every date up to 120 days ahead (JSON)",
        "description": "For each Madeira calendar date from today up to 119 days ahead: the day's state and the visitor permits left that day, with the time the date was last checked. Only bookable permit trails have a day feed. No start times. The same address with .csv appended is the CSV variant of this document (GET /api/public/hiking-availability/{seo_slug}.csv): a seo_slug never contains a dot.",
        "parameters": [
          { "$ref": "#/components/parameters/IfNoneMatch" },
          { "$ref": "#/components/parameters/IfModifiedSince" }
        ],
        "responses": {
          "200": {
            "description": "The trail's days.",
            "headers": {
              "ETag": { "$ref": "#/components/headers/ETag" },
              "Last-Modified": { "$ref": "#/components/headers/LastModified" },
              "Cache-Control": { "$ref": "#/components/headers/CacheControl" },
              "X-License": { "$ref": "#/components/headers/XLicense" }
            },
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/TrailDays" } }
            }
          },
          "304": { "$ref": "#/components/responses/NotModified" },
          "404": { "$ref": "#/components/responses/ProblemNotFound" },
          "405": { "$ref": "#/components/responses/ProblemMethodNotAllowed" },
          "500": { "$ref": "#/components/responses/ProblemInternalError" },
          "503": { "$ref": "#/components/responses/ProblemUnavailable" }
        }
      },
      "head": {
        "operationId": "headTrailDays",
        "tags": ["hiking-permits"],
        "summary": "Headers of one trail's day feed (JSON)",
        "responses": {
          "200": { "$ref": "#/components/responses/HeadOk" },
          "304": { "$ref": "#/components/responses/NotModified" },
          "404": { "description": "Unknown or non-bookable trail." },
          "500": { "description": "Unexpected error." },
          "503": { "description": "Temporarily unavailable." }
        }
      }
    },
    "/api/public/hiking-availability/{seo_slug}.csv": {
      "parameters": [ { "$ref": "#/components/parameters/SeoSlug" } ],
      "get": {
        "operationId": "getTrailDaysCsv",
        "tags": ["hiking-permits"],
        "summary": "One trail, every date up to 120 days ahead (CSV)",
        "description": "The CSV variant of GET /api/public/hiking-availability/{seo_slug}. One row per date, today first. A short # comment preamble carries the format, the trail, generated_at (UTC) and the license line.",
        "parameters": [
          { "$ref": "#/components/parameters/IfNoneMatch" },
          { "$ref": "#/components/parameters/IfModifiedSince" }
        ],
        "responses": {
          "200": {
            "description": "The trail's days as CSV.",
            "headers": {
              "ETag": { "$ref": "#/components/headers/ETag" },
              "Last-Modified": { "$ref": "#/components/headers/LastModified" },
              "Cache-Control": { "$ref": "#/components/headers/CacheControl" },
              "X-License": { "$ref": "#/components/headers/XLicense" }
            },
            "content": {
              "text/csv": {
                "schema": { "type": "string" },
                "x-csv-columns": ["date", "state", "permits_left", "checked_at", "book_url"]
              }
            }
          },
          "304": { "$ref": "#/components/responses/NotModified" },
          "404": { "$ref": "#/components/responses/ProblemNotFound" },
          "405": { "$ref": "#/components/responses/ProblemMethodNotAllowed" },
          "500": { "$ref": "#/components/responses/ProblemInternalError" },
          "503": { "$ref": "#/components/responses/ProblemUnavailable" }
        }
      },
      "head": {
        "operationId": "headTrailDaysCsv",
        "tags": ["hiking-permits"],
        "summary": "Headers of one trail's day feed (CSV)",
        "responses": {
          "200": { "$ref": "#/components/responses/HeadOk" },
          "304": { "$ref": "#/components/responses/NotModified" },
          "404": { "description": "Unknown or non-bookable trail." },
          "500": { "description": "Unexpected error." },
          "503": { "description": "Temporarily unavailable." }
        }
      }
    },
    "/api/public/madeira-weather": {
      "get": {
        "operationId": "getMadeiraWeather",
        "tags": ["weather"],
        "summary": "Live weather, webcam and crowd observations by place (JSON)",
        "description": "Current conditions per place, stitched from the best source per dimension. Every value carries its own observation time. Format MF-WX-1.0; field definitions on https://madeira.fun/weather/data.",
        "parameters": [
          { "$ref": "#/components/parameters/IfNoneMatch" },
          { "$ref": "#/components/parameters/IfModifiedSince" }
        ],
        "responses": {
          "200": {
            "description": "The feed.",
            "headers": {
              "ETag": { "$ref": "#/components/headers/ETag" },
              "Last-Modified": { "$ref": "#/components/headers/LastModified" },
              "Cache-Control": { "$ref": "#/components/headers/CacheControl" },
              "X-License": { "$ref": "#/components/headers/XLicense" }
            },
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/WeatherFeed" } }
            }
          },
          "304": { "$ref": "#/components/responses/NotModified" },
          "405": { "$ref": "#/components/responses/FeedMethodNotAllowed" },
          "500": { "$ref": "#/components/responses/FeedInternalError" },
          "503": { "$ref": "#/components/responses/WeatherUnavailable" }
        }
      },
      "head": {
        "operationId": "headMadeiraWeather",
        "tags": ["weather"],
        "summary": "Headers of the weather feed (JSON)",
        "responses": {
          "200": { "$ref": "#/components/responses/HeadOk" },
          "304": { "$ref": "#/components/responses/NotModified" },
          "503": { "description": "Temporarily unavailable." }
        }
      }
    },
    "/api/public/madeira-weather.csv": {
      "get": {
        "operationId": "getMadeiraWeatherCsv",
        "tags": ["weather"],
        "summary": "Live weather, webcam and crowd observations by place (CSV)",
        "description": "The same records as the JSON feed, one row per place. A # comment preamble carries the format, license, attribution and sources. Columns are append-only.",
        "parameters": [
          { "$ref": "#/components/parameters/IfNoneMatch" },
          { "$ref": "#/components/parameters/IfModifiedSince" }
        ],
        "responses": {
          "200": {
            "description": "The feed as CSV.",
            "headers": {
              "ETag": { "$ref": "#/components/headers/ETag" },
              "Last-Modified": { "$ref": "#/components/headers/LastModified" },
              "Cache-Control": { "$ref": "#/components/headers/CacheControl" },
              "X-License": { "$ref": "#/components/headers/XLicense" }
            },
            "content": {
              "text/csv": {
                "schema": { "type": "string" },
                "x-csv-columns": [
                  "slug", "name", "region", "latitude", "longitude", "url",
                  "temperature_c", "humidity_pct", "wind_kmh", "pressure_hpa",
                  "station", "station_distance_km", "numbers_observed_at",
                  "weather_type_id", "weather_type_en", "weather_type_pt",
                  "sky", "sunshine", "sky_source", "sky_ref", "sky_observed_at",
                  "cloud_state", "cloud_observed_at",
                  "is_raining", "rain_mm", "rain_source",
                  "crowd_level", "sea_state", "crowd_observed_at",
                  "wave_height_m", "wave_observed_at",
                  "webcam_coverage", "observed_at"
                ]
              }
            }
          },
          "304": { "$ref": "#/components/responses/NotModified" },
          "405": { "$ref": "#/components/responses/FeedMethodNotAllowed" },
          "500": { "$ref": "#/components/responses/FeedInternalError" },
          "503": { "$ref": "#/components/responses/WeatherUnavailable" }
        }
      },
      "head": {
        "operationId": "headMadeiraWeatherCsv",
        "tags": ["weather"],
        "summary": "Headers of the weather feed (CSV)",
        "responses": {
          "200": { "$ref": "#/components/responses/HeadOk" },
          "304": { "$ref": "#/components/responses/NotModified" },
          "503": { "description": "Temporarily unavailable." }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "SeoSlug": {
        "name": "seo_slug",
        "in": "path",
        "required": true,
        "description": "The trail's seo_slug as listed in the permit feed (trails[].seo_slug), for example pr18-rei-231. Easiest: follow day_feed_url from the permit feed. Join records on resource_key, not on the slug's text.",
        "schema": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,119}$" },
        "example": "pr18-rei-231"
      },
      "IfNoneMatch": {
        "name": "If-None-Match",
        "in": "header",
        "required": false,
        "description": "An ETag from an earlier response; the answer is 304 when it still matches.",
        "schema": { "type": "string" }
      },
      "IfModifiedSince": {
        "name": "If-Modified-Since",
        "in": "header",
        "required": false,
        "description": "A Last-Modified value from an earlier response; the answer is 304 when the data has not changed since.",
        "schema": { "type": "string" }
      }
    },
    "headers": {
      "ETag": {
        "description": "Weak validator over the response body.",
        "schema": { "type": "string" }
      },
      "LastModified": {
        "description": "When the data in the response last changed (from the data's own time).",
        "schema": { "type": "string" }
      },
      "CacheControl": {
        "description": "Public, cacheable for about 5 minutes.",
        "schema": { "type": "string" }
      },
      "XLicense": {
        "description": "CC-BY-4.0 followed by the license URL.",
        "schema": { "type": "string" }
      },
      "RetryAfter": {
        "description": "Seconds to wait before trying again.",
        "schema": { "type": "string" }
      },
      "Allow": {
        "description": "The methods the address answers: GET, HEAD, OPTIONS.",
        "schema": { "type": "string" }
      }
    },
    "responses": {
      "NotModified": {
        "description": "Not Modified: the validators sent still match.",
        "headers": {
          "ETag": { "$ref": "#/components/headers/ETag" },
          "Last-Modified": { "$ref": "#/components/headers/LastModified" }
        }
      },
      "HeadOk": {
        "description": "The headers a GET would return, without a body.",
        "headers": {
          "ETag": { "$ref": "#/components/headers/ETag" },
          "Last-Modified": { "$ref": "#/components/headers/LastModified" },
          "Cache-Control": { "$ref": "#/components/headers/CacheControl" }
        }
      },
      "FeedUnavailable": {
        "description": "Availability is refreshing or the data source is unreachable. Never cached.",
        "headers": { "Retry-After": { "$ref": "#/components/headers/RetryAfter" } },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/FeedError" },
            "example": { "error": "availability_unavailable", "message": "Availability is refreshing, try again shortly." }
          }
        }
      },
      "WeatherUnavailable": {
        "description": "Observations are refreshing or the data source is unreachable. Never cached.",
        "headers": { "Retry-After": { "$ref": "#/components/headers/RetryAfter" } },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/FeedError" },
            "example": { "error": "observations_unavailable", "message": "Observations are refreshing, try again shortly." }
          }
        }
      },
      "FeedMethodNotAllowed": {
        "description": "Returned to any method other than GET, HEAD and OPTIONS. Never cached.",
        "headers": { "Allow": { "$ref": "#/components/headers/Allow" } },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/FeedError" },
            "example": { "error": "method_not_allowed" }
          }
        }
      },
      "FeedInternalError": {
        "description": "Unexpected error. Never cached.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/FeedError" },
            "example": { "error": "internal_error" }
          }
        }
      },
      "ProblemNotFound": {
        "description": "Unknown trail, or a trail that cannot be booked (code trail_not_found). Never cached.",
        "content": {
          "application/problem+json": {
            "schema": { "$ref": "#/components/schemas/Problem" },
            "example": { "type": "about:blank", "title": "Not Found", "status": 404, "detail": "No bookable permit trail has this address. The trail list (with each trail's day_feed_url) is at https://madeira.fun/api/public/hiking-availability.", "code": "trail_not_found", "docs": "https://madeira.fun/hiking/permit-data#open-data" }
          }
        }
      },
      "ProblemMethodNotAllowed": {
        "description": "Returned to any method other than GET, HEAD and OPTIONS (code method_not_allowed). Never cached.",
        "headers": { "Allow": { "$ref": "#/components/headers/Allow" } },
        "content": {
          "application/problem+json": {
            "schema": { "$ref": "#/components/schemas/Problem" },
            "example": { "type": "about:blank", "title": "Method Not Allowed", "status": 405, "detail": "This feed answers GET and HEAD only.", "code": "method_not_allowed", "docs": "https://madeira.fun/hiking/permit-data#open-data" }
          }
        }
      },
      "ProblemInternalError": {
        "description": "Unexpected error (code internal_error). Never cached.",
        "content": {
          "application/problem+json": {
            "schema": { "$ref": "#/components/schemas/Problem" },
            "example": { "type": "about:blank", "title": "Internal Server Error", "status": 500, "detail": "Something went wrong on our side.", "code": "internal_error", "docs": "https://madeira.fun/hiking/permit-data#open-data" }
          }
        }
      },
      "ProblemUnavailable": {
        "description": "The data source is unreachable or the read failed (code availability_unavailable). Never cached.",
        "headers": { "Retry-After": { "$ref": "#/components/headers/RetryAfter" } },
        "content": {
          "application/problem+json": {
            "schema": { "$ref": "#/components/schemas/Problem" },
            "example": { "type": "about:blank", "title": "Service Unavailable", "status": 503, "detail": "Availability is refreshing, try again shortly.", "code": "availability_unavailable", "docs": "https://madeira.fun/hiking/permit-data#open-data" }
          }
        }
      }
    },
    "schemas": {
      "HikingAvailabilityFeed": {
        "type": "object",
        "description": "MF-HAF-1.1 permit availability feed. Fields are only ever added within a format version.",
        "required": ["format", "jurisdiction", "dataset", "publisher", "license", "attribution", "methodology_url", "timezone", "generated_at", "last_checked", "trails"],
        "properties": {
          "format": { "const": "MF-HAF-1.1", "description": "Format id of this payload." },
          "jurisdiction": { "const": "pt-madeira", "description": "Permit system key (Permit Data Standard section 2.1)." },
          "dataset": { "type": "string" },
          "publisher": { "type": "string" },
          "license": { "type": "string" },
          "attribution": { "type": "string", "description": "The exact attribution sentence to use when you cite the data." },
          "methodology_url": { "type": "string", "format": "uri" },
          "booking_pressure_version": { "type": "string", "description": "Formula version of booking_pressure_score, for example MF-BPS-1.0." },
          "disclaimer": { "type": "string" },
          "timezone": { "const": "Atlantic/Madeira", "description": "Time zone of every local date in the feed." },
          "generated_at": { "type": ["string", "null"], "format": "date-time", "description": "When the summary data was read (ISO 8601, UTC)." },
          "last_checked": { "type": ["string", "null"], "format": "date-time", "description": "The newest observation time across all trails." },
          "summary_refreshed_at": { "type": ["string", "null"], "format": "date-time", "description": "When the trail summary was last rebuilt." },
          "summary": { "$ref": "#/components/schemas/FeedSummary" },
          "trails": { "type": "array", "items": { "$ref": "#/components/schemas/HikingTrailRecord" } }
        }
      },
      "FeedSummary": {
        "type": ["object", "null"],
        "description": "Network-wide aggregate.",
        "properties": {
          "total_trails": { "type": "integer" },
          "trails_with_availability": { "type": "integer", "description": "Trails with at least one available day in the next 7 days." },
          "highest_pressure": {
            "type": ["object", "null"],
            "properties": {
              "trail_name": { "type": "string" },
              "seo_slug": { "type": ["string", "null"] },
              "score": { "type": ["number", "null"] },
              "label": { "type": ["string", "null"] }
            }
          },
          "best_short_notice": {
            "type": ["object", "null"],
            "properties": {
              "trail_name": { "type": "string" },
              "seo_slug": { "type": ["string", "null"] },
              "available_days_7d": { "type": ["integer", "null"] },
              "earliest_available_date": { "type": ["string", "null"], "format": "date" }
            }
          }
        }
      },
      "HikingTrailRecord": {
        "type": "object",
        "description": "One permit trail. MF-HAF-1.0 fields first, then the MF-HAF-1.1 additions. A field with no reliable source for a trail is null, never guessed. A trail that cannot be booked has days_7d and day_feed_url null.",
        "required": ["code", "name", "full_name", "seo_slug", "url", "official_status", "observed_at", "resource_key"],
        "properties": {
          "code": { "type": "string", "description": "The route code as written in the trail's booking name, for example PR1; an empty string when there is none. Display only, format frozen since 1.0, and not unique (two records can share a code): use resource_key as the key." },
          "name": { "type": "string" },
          "full_name": { "type": "string" },
          "seo_slug": { "type": ["string", "null"], "description": "Key of the trail's page and day feed." },
          "url": { "type": "string", "format": "uri", "description": "The trail's booking page on madeira.fun." },
          "official_status": { "type": ["string", "null"], "description": "IFCN status category, verbatim: open, closed, partially_open, restricted or unknown." },
          "earliest_available_date": { "type": ["string", "null"], "format": "date" },
          "earliest_available_time": { "type": ["string", "null"], "description": "First observed bookable visitor start time on the earliest date, HH:MM Madeira time." },
          "available_days_7d": { "type": ["integer", "null"], "description": "Days in the next 7 with at least one observed bookable visitor start time." },
          "available_days_14d": { "type": ["integer", "null"] },
          "available_days_30d": { "type": ["integer", "null"] },
          "morning_available": { "type": ["boolean", "null"] },
          "afternoon_available": { "type": ["boolean", "null"] },
          "weekend_available": { "type": ["boolean", "null"] },
          "same_day_available": { "type": ["boolean", "null"] },
          "slot_density_14d": { "type": ["number", "null"], "minimum": 0, "maximum": 1 },
          "booking_pressure_score": { "type": ["number", "null"], "minimum": 0, "maximum": 10 },
          "booking_pressure_label": { "type": ["string", "null"] },
          "observed_at": { "type": ["string", "null"], "format": "date-time", "description": "When this trail's availability was last observed (ISO 8601, UTC)." },
          "resource_key": { "type": ["string", "null"], "description": "The government reservation system's resource id: the stable key of the trail within pt-madeira. Null only for a summary row that carries no resource id." },
          "name_pt": { "type": ["string", "null"], "description": "Portuguese name." },
          "status_detail": { "type": ["string", "null"], "description": "Detail of the official status, when IFCN gives one." },
          "status_pt": { "type": ["string", "null"], "description": "The official status in Portuguese." },
          "status_since": { "type": ["string", "null"], "format": "date-time", "description": "When madeira.fun's daily check of the IFCN list first saw the current status (ISO 8601, UTC): an observed change time, up to one check after the real change, never the official effective date. Null when no change has been seen since recording began, or when the 1.1 additions could not be read." },
          "status_source_url": { "type": ["string", "null"], "format": "uri", "description": "The official IFCN page the status is read from. Null when the trail was never matched on that page, or when the 1.1 additions could not be read." },
          "closures": { "type": ["array", "null"], "items": { "$ref": "#/components/schemas/Closure" }, "description": "Active and future dated closures; an empty array when there are none. Null when the 1.1 additions could not be read for this response (not known, which is different from none)." },
          "days_7d": { "type": ["array", "null"], "maxItems": 7, "items": { "$ref": "#/components/schemas/DayStatus" }, "description": "Today and the next 6 days, today first: the first 7 days of the trail's day feed. Null for a trail that cannot be booked, and when the 1.1 additions could not be read." },
          "day_feed_url": { "type": ["string", "null"], "format": "uri", "description": "This trail's day feed (up to 120 days). Null for a trail that cannot be booked, and when the 1.1 additions could not be read." },
          "fees_ref": { "type": "string", "format": "uri", "description": "Where the permit fee records (MF-PRC-1.0) are published." },
          "walked_km": { "type": ["number", "null"], "description": "The walked distance, always read together with route_type." },
          "route_type": { "type": ["string", "null"], "description": "Route type of the walked distance (loop, out and back, or point to point)." },
          "start": { "$ref": "#/components/schemas/TrailPoint" },
          "end": { "$ref": "#/components/schemas/TrailPoint" },
          "normal_time_minutes": { "type": ["integer", "null"], "description": "Walking time at a normal pace, in minutes." },
          "sunrise": { "type": ["string", "null"], "description": "Today's astronomical sunrise at the trailhead, HH:MM Madeira time." },
          "sunset": { "type": ["string", "null"], "description": "Today's astronomical sunset at the trailhead, HH:MM Madeira time." },
          "weather_feed_url": { "type": "string", "format": "uri", "description": "The live weather feed." }
        }
      },
      "Closure": {
        "type": "object",
        "description": "A dated closure. from and to are Madeira calendar dates, both inclusive.",
        "properties": {
          "from": { "type": "string", "format": "date" },
          "to": { "type": ["string", "null"], "format": "date" },
          "reason": { "type": ["string", "null"] },
          "source": { "type": ["string", "null"], "description": "URL of the source of the closure, or null." },
          "source_label": { "type": ["string", "null"], "description": "Name of that source." }
        }
      },
      "TrailPoint": {
        "type": ["object", "null"],
        "description": "One end of the walk (WGS84). Any member may be null, for example a named end with no point; the object is null when nothing is known.",
        "properties": {
          "name": { "type": ["string", "null"] },
          "lat": { "type": ["number", "null"] },
          "lng": { "type": ["number", "null"] }
        }
      },
      "DayStatus": {
        "type": "object",
        "description": "One Madeira calendar date for one trail. A start time counts as bookable only when it has not begun (Madeira wall clock) and passes the same booking check the reservation service uses for a party of one.",
        "required": ["date", "state", "permits_left", "checked_at", "book_url"],
        "properties": {
          "date": { "type": "string", "format": "date", "description": "Madeira calendar date." },
          "state": {
            "type": "string",
            "enum": ["open", "full", "started", "closed", "no_times", "unknown"],
            "description": "open: at least one bookable start time. full: start times are listed and none can be booked by a visitor (sold out). started: Madeira today only: start times were listed and every one has already begun, so none can be booked today (this is not sold out). closed: the trail is not open that day (its official status is anything but open, unknown or a missing status included, or a dated closure covers the date). no_times: the date was checked and lists no start times. unknown: the date has not been checked."
          },
          "permits_left": { "type": ["integer", "null"], "minimum": 0, "description": "Visitor (non-resident) permits left across the bookable start times. 0 for full; null for started, closed, no_times and unknown." },
          "checked_at": { "type": ["string", "null"], "format": "date-time", "description": "When this date was last checked (ISO 8601, UTC)." },
          "book_url": { "type": ["string", "null"], "format": "uri", "description": "The booking page for this date; only for open and full days." }
        }
      },
      "TrailDays": {
        "type": "object",
        "description": "MF-HAF-1.1 day feed for one trail.",
        "required": ["format", "jurisdiction", "generated_at", "trail", "days"],
        "properties": {
          "format": { "const": "MF-HAF-1.1" },
          "jurisdiction": { "const": "pt-madeira" },
          "generated_at": { "type": "string", "format": "date-time" },
          "trail": { "$ref": "#/components/schemas/TrailIdentity" },
          "days": { "type": "array", "maxItems": 120, "items": { "$ref": "#/components/schemas/DayStatus" }, "description": "Today first." }
        }
      },
      "TrailIdentity": {
        "type": "object",
        "required": ["seo_slug", "resource_key", "code", "name", "page_url", "status"],
        "properties": {
          "seo_slug": { "type": "string" },
          "resource_key": { "type": "string" },
          "code": { "type": ["string", "null"], "description": "The route code from madeira.fun's route registry, paired by id, for example PR 18 or PS PR 1; null when none is published. Not the same field as the 1.0 code of the permit feed: join on resource_key." },
          "name": { "type": "string" },
          "name_pt": { "type": ["string", "null"] },
          "page_url": { "type": "string", "format": "uri" },
          "status": { "type": "string", "enum": ["open", "closed", "partially_open", "restricted", "unknown"], "description": "IFCN whole-trail status, verbatim. Any status but open (unknown included) makes every day closed." }
        }
      },
      "WeatherFeed": {
        "type": "object",
        "description": "MF-WX-1.0 live weather feed.",
        "required": ["dataset", "format_version", "license", "attribution", "methodology_url", "timezone", "generated_at", "places"],
        "properties": {
          "dataset": { "type": "string" },
          "publisher": { "type": "string" },
          "format_version": { "const": "MF-WX-1.0" },
          "license": { "type": "string" },
          "attribution": { "type": "string" },
          "methodology_url": { "type": "string", "format": "uri" },
          "timezone": { "const": "Atlantic/Madeira" },
          "source_attributions": { "type": "object", "additionalProperties": { "type": "string" } },
          "disclaimer": { "type": "string" },
          "generated_at": { "type": "string", "format": "date-time" },
          "last_observed_at": { "type": ["string", "null"], "format": "date-time" },
          "places": { "type": "array", "items": { "$ref": "#/components/schemas/WeatherPlace" } }
        }
      },
      "WeatherPlace": {
        "type": "object",
        "description": "Current conditions at one place. Definitions: https://madeira.fun/weather/data#field-definitions",
        "properties": {
          "slug": { "type": "string" },
          "name": { "type": "string" },
          "region": { "type": ["string", "null"] },
          "latitude": { "type": ["number", "null"] },
          "longitude": { "type": ["number", "null"] },
          "url": { "type": "string", "format": "uri" },
          "temperature_c": { "type": ["number", "null"] },
          "humidity_pct": { "type": ["number", "null"] },
          "wind_kmh": { "type": ["number", "null"] },
          "pressure_hpa": { "type": ["number", "null"] },
          "station": { "type": ["string", "null"] },
          "station_distance_km": { "type": ["number", "null"] },
          "numbers_observed_at": { "type": ["string", "null"], "format": "date-time" },
          "weather_type_id": { "type": ["integer", "null"], "description": "IPMA weather type id." },
          "weather_type_en": { "type": ["string", "null"] },
          "weather_type_pt": { "type": ["string", "null"] },
          "sky": { "type": ["string", "null"] },
          "sunshine": { "type": ["string", "null"] },
          "sky_source": { "type": ["string", "null"] },
          "sky_ref": { "type": ["string", "null"] },
          "sky_observed_at": { "type": ["string", "null"], "format": "date-time" },
          "cloud_state": { "type": ["string", "null"] },
          "cloud_observed_at": { "type": ["string", "null"], "format": "date-time" },
          "is_raining": { "type": ["boolean", "null"] },
          "rain_mm": { "type": ["number", "null"] },
          "rain_source": { "type": ["string", "null"] },
          "crowd_level": { "type": ["string", "null"] },
          "sea_state": { "type": ["string", "null"] },
          "crowd_observed_at": { "type": ["string", "null"], "format": "date-time" },
          "wave_height_m": { "type": ["number", "null"] },
          "wave_observed_at": { "type": ["string", "null"], "format": "date-time" },
          "webcam_coverage": { "type": ["string", "null"], "enum": ["live", "still", null] },
          "observed_at": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "FeedError": {
        "type": "object",
        "description": "Error body of the MF-HAF and MF-WX summary feeds (kept from version 1.0).",
        "required": ["error"],
        "properties": {
          "error": { "type": "string" },
          "message": { "type": "string" }
        }
      },
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details, used by the per-trail day feed and by unknown /api addresses. Branch on code, a stable machine code: trail_not_found, not_found, method_not_allowed, availability_unavailable or internal_error (new codes may be added).",
        "required": ["type", "title", "status", "detail", "code"],
        "properties": {
          "type": { "type": "string", "format": "uri-reference" },
          "title": { "type": "string" },
          "status": { "type": "integer" },
          "detail": { "type": "string" },
          "instance": { "type": "string" },
          "code": { "type": "string", "description": "Stable machine code of the problem." },
          "docs": { "type": "string", "format": "uri", "description": "The developer documentation." }
        }
      }
    }
  }
}
