Point and batch weather forecast

GET https://trailsplits.com/api/weather/forecast

Checked by a live probe Served from trailsplits.com

Served from the main site, not the API host. Daily values per point and local date; each day names its model and issued_at is the model run (null with a reason when unknown, never the fetch time). Missing values are null, never 0; fewer days or fields than asked is state partial with reasons. Pass each point height: a model cell height can be a thousand metres off in the mountains. Attribution from provider.attribution (Open-Meteo, CC BY 4.0) is required wherever values are shown.

Example

Point and batch weather forecast (curl)
curl --fail-with-body 'https://trailsplits.com/api/weather/forecast?p=46.0207,7.7491,1608&d=2'

Sends a real request, with your key or the keyless allowance.

Parameters

NameInDescription
prequiredquery1–12 points as lat,lon[,elevation_m] separated by ;. Coordinates are rounded to 2 decimals (about 1 km) and echoed rounded.
dqueryForecast days.
flqueryAdd the hourly freezing level.
hqueryAdd hourly[] per point over the requested days (about 130 bytes per hour and point; ask only for the days you need). Any other value is 400.

Response

FieldTypeDescription
schemastring: weather-forecast/1
providerobject
fetched_atstring
issued_atstring or nullThe model run, or null with issued_at_reason. Never the fetch time.
issued_at_reasonstring or null
fresh_untilstring
requested_daysinteger
pointsarray of WeatherPoint
Full response schema (JSON Schema)
{
  "type": "object",
  "properties": {
    "schema": {
      "type": "string",
      "enum": [
        "weather-forecast/1"
      ]
    },
    "provider": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "model": {
          "type": "string"
        },
        "attribution": {
          "type": "string"
        },
        "licence": {
          "type": "string"
        }
      },
      "required": [
        "attribution",
        "licence"
      ]
    },
    "fetched_at": {
      "type": "string"
    },
    "issued_at": {
      "type": "string",
      "nullable": true,
      "description": "The model run, or null with issued_at_reason. Never the fetch time."
    },
    "issued_at_reason": {
      "type": "string",
      "nullable": true
    },
    "fresh_until": {
      "type": "string"
    },
    "requested_days": {
      "type": "integer",
      "minimum": 1,
      "maximum": 16
    },
    "points": {
      "type": "array",
      "minItems": 1,
      "maxItems": 12,
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lon": {
            "type": "number"
          },
          "elevation_m": {
            "type": "number",
            "nullable": true
          },
          "elevation_source": {
            "type": "string",
            "description": "request (yours), terrain (TrailSplits terrain model), provider_grid (model cell mean) or unknown."
          },
          "timezone": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "ok",
              "partial",
              "missing"
            ]
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "days": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WeatherDay"
            }
          },
          "hourly": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WeatherHour"
            },
            "description": "Only with h=1. Local times; each hour names its model."
          }
        },
        "required": [
          "id",
          "lat",
          "lon",
          "elevation_m",
          "elevation_source",
          "state",
          "reasons",
          "days"
        ]
      }
    }
  },
  "required": [
    "schema",
    "provider",
    "fetched_at",
    "issued_at",
    "fresh_until",
    "requested_days",
    "points"
  ]
}

Errors

400, 503, plus the gateway's 401, 429 and 503 (error conventions). A refused request costs no credits.

Contract

Request

Up to 12 points and 1–16 days. Per point: id, lat, lon and elevation_m.

  • elevation_m given: the height to forecast for (a route's high point, a hut, a summit). Open-Meteo downscales its temperatures to it with 0.0065 °C/m.
  • elevation_m null: the provider uses its grid cell's own height. Do not apply a second lapse correction on top of either.

Answer

FieldMeaning
schema"weather-forecast/1"
providerid (open-meteo-hosted), model (best_match), attribution (shown wherever values are shown), licence (CC-BY-4.0)
fetched_atwhen TrailSplits asked (ISO, UTC)
issued_atthe model run the values come from, or null with issued_at_reason. Hosted Open-Meteo never reports it: provider_does_not_report_model_run. A consumer never labels fetched_at as "issued"
fresh_untilshow as current until then (fetched_at + 3 h, the producer's cache lifetime)
requested_daysthe horizon asked for
units°C, mm, km/h, %, m
points[]as below

Per point:

  • Position and height:
    • provider_lat / provider_lon: where the provider placed it;
    • elevation_m and elevation_source: request (ours), provider_grid (theirs) or unknown;
    • timezone and utc_offset_seconds: dates are local to the point.
  • state: ok, partial or missing, with reasons:
    • horizon_7_of_16_days: fewer days than asked, never silently shortened;
    • field_missing:<name>;
    • some_temperatures_missing;
    • no_daily_values_from_provider.
    • A missing value is null, never 0, and a missing probability is not a 0 % chance.
  • freezing_level_m: optional hourly [local time, metres] pairs.

Limits and failures

  • At most 12 points and 16 days per request; the URL builder refuses more.
  • A provider failure is never a blank: the consumer shows that the forecast is unavailable, and saving, viewing and exporting a route never depend on weather.

Endpoint (2 October 2026)

GET https://trailsplits.com/api/weather/forecast?p=lat,lon[,elevation];...&d=1..16[&fl=1] answers with weather-forecast/1.

Request:

  • Up to 12 points. Coordinates are rounded to 2 decimals (about 1 km), so nearby requests share answers, and the answer echoes the rounded values.
  • Elevation is in metres; leave it empty for the model grid's own height.
  • For mountain points, always pass the height. ECMWF's 0.25° cell is about 25 km wide and its height is the cell's mean. Without a height, Zermatt (1,608 m) was answered at 2,951 m on 2 Oct, correctly labelled provider_grid but useless for the village.
  • d defaults to 7 days. fl=1 adds the hourly freezing level.
  • provider.id is trailsplits-open-meteo and provider.model is ecmwf_ifs025.
  • issued_at is the ECMWF run the copy holds (its initialisation time). When that is unknown it is null, with issued_at_reason: model_run_unknown.
  • Rain probability comes from the ECMWF ensemble, and the freezing level from GFS 0.25.

Limits and failures:

  • A copy whose run is over 30 h old does not answer.
  • When the copy fails, the last good answer is served for up to 12 h. Its fresh_until has passed by then, so consumers see it is not current.
  • After 12 h the answer is 503 {error: {code: "forecast_unavailable"}} with Retry-After: 300. It is never blanks.
  • The hosted Open-Meteo API is not used as a fallback. Its free tier is for TrailSplits' own pages.
  • Successful answers are cacheable: Cache-Control: public, max-age=300, s-maxage=600. CORS is open.

Regional models (2 October 2026, afternoon)

By default (models=seamless), each point's days come from the best model that covers the whole day:

  1. MeteoSwiss ICON-CH2: the Alps, about 2 km, about 5 days.
  2. DWD ICON-D2: Central Europe, 2 km, 2 days.
  3. ECMWF IFS 0.25: global, 15 days, for every other day.

How a day is chosen: from each model's own daily values. A model's last day is often only partly covered, and Open-Meteo leaves such a day empty rather than aggregate a few hours, so a regional day is used only when it is complete. This was checked against the copy: CH2's complete days aggregate exactly as Open-Meteo does, and its sixth, partial day is null.

Additive fields:

  • days[].model: the model the day comes from.
  • days[].probability_model: set when the rain probability comes from another model. ICON-D2 has none, so the ECMWF ensemble fills it.
  • provider.model: trailsplits_seamless.
  • provider.runs: each model's run used.
  • issued_at: the oldest run used.

Regional runs: a regional model is used only while its run is under 12 h old. When the copy's runs cannot be read, only ECMWF is used. models=ecmwf gives ECMWF alone, as before.

Mixed areas (fixed 2 Oct, evening): the copy refuses a whole regional request when one point is outside the model's area. Such a request is now asked again point by point, so Zermatt asked together with Oslo keeps ICON-CH2 (before the fix, every point fell back to ECMWF).

Heights: a point given without a height is now asked at TrailSplits' terrain-model height (elevation_source: terrain), so every model forecasts for the same height. If that lookup fails, it stays provider_grid.

Fresh snow and rain (2 October 2026, evening)

Additive fields: days[].snowfall_cm (Open-Meteo snowfall_sum) and days[].rain_mm (rain_sum).

  • They come from the same model as the day and are null, never zero, when missing.
  • Answers made earlier lack them; an answer without them is not partial.

Caveat: the split between snow and rain follows the model's own terrain height, not the requested one.

  • ICON-CH2's 2 km terrain is close to the real one.
  • ECMWF's 25 km cell can sit far below a summit. On 2 Oct ECMWF gave the Matterhorn summit 1.9 mm of rain and no snow on a day CH2 gave 1.75 cm of snow.
  • On ECMWF days in high mountains, read the freezing level (fl=1) and temperatures alongside the split.
  • Fields: temperature, precipitation, rain probability, wind, gusts, wind direction and weather code. With fl=1 too, freezing_level_m uses the same hours (GFS, lined up by time; an hour GFS lacks is null).
  • Size: about 130 bytes an hour and point: 3 points for 3 days is about 47 kB. Ask only for the days you need.
  • Measured on 2 Oct: Zermatt, Munich and Oslo for 3 days answered in about 1 s from the copy. Zermatt and Munich came from ICON-CH2, Oslo from ECMWF.