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
curl --fail-with-body 'https://trailsplits.com/api/weather/forecast?p=46.0207,7.7491,1608&d=2' Parameters
| Name | In | Description |
|---|---|---|
prequired | query | 1–12 points as lat,lon[,elevation_m] separated by ;. Coordinates are rounded to 2 decimals (about 1 km) and echoed rounded. |
d | query | Forecast days. |
fl | query | Add the hourly freezing level. |
h | query | Add 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
| Field | Type | Description |
|---|---|---|
schema | string: weather-forecast/1 | |
provider | object | |
fetched_at | string | |
issued_at | string or null | The model run, or null with issued_at_reason. Never the fetch time. |
issued_at_reason | string or null | |
fresh_until | string | |
requested_days | integer | |
points | array 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_mgiven: 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_mnull: the provider uses its grid cell's own height. Do not apply a second lapse correction on top of either.
Answer
| Field | Meaning |
|---|---|
schema | "weather-forecast/1" |
provider | id (open-meteo-hosted), model (best_match), attribution (shown wherever values are shown), licence (CC-BY-4.0) |
fetched_at | when TrailSplits asked (ISO, UTC) |
issued_at | the 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_until | show as current until then (fetched_at + 3 h, the producer's cache lifetime) |
requested_days | the 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_mandelevation_source:request(ours),provider_grid(theirs) orunknown; -
timezoneandutc_offset_seconds: dates are local to the point.
-
-
state:ok,partialormissing, withreasons:-
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_gridbut useless for the village. -
ddefaults to 7 days.fl=1adds the hourly freezing level. -
provider.idistrailsplits-open-meteoandprovider.modelisecmwf_ifs025. -
issued_atis the ECMWF run the copy holds (its initialisation time). When that is unknown it is null, withissued_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_untilhas passed by then, so consumers see it is not current. -
After 12 h the answer is
503 {error: {code: "forecast_unavailable"}}withRetry-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:
- MeteoSwiss ICON-CH2: the Alps, about 2 km, about 5 days.
- DWD ICON-D2: Central Europe, 2 km, 2 days.
- 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=1too,freezing_level_muses 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.