How to Use the History API for Ground-Truth Validation: Catching When Your Forecast Was Just Wrong

Most developers use the history endpoint to pull past weather for backtesting or to fill gaps in their records. That’s fine. But there’s a less obvious use for it: validating whether the forecast you actually served to your users was any good.

Not in a general “how accurate is weather forecasting” sense. In a specific, per-location, per-timestamp sense — did the temperature we predicted for 14:00 local on Tuesday match what was observed? By how much? Did that gap get worse at day 3 versus day 1?

This matters more than it sounds if your application makes decisions based on forecast data. Construction scheduling, outdoor event planning, agricultural spray windows — if your app told someone it would be 4°C and it hit -2°C, that’s worth knowing. Systematically.

The Basic Comparison Setup

The mechanics aren’t complicated. When you serve a forecast, log the key fields and the target timestamp alongside when you fetched them. Something like this in whatever datastore you’re using:

{
  "location": "51.5085,-0.1257",
  "fetched_at": "2025-07-01T08:00:00Z",
  "target_dt": "2025-07-03T14:00:00",
  "forecast_temp_c": 21.4,
  "forecast_precip_mm": 0.0,
  "forecast_condition_code": 1000,
  "hours_ahead": 54
}

Then, once the target timestamp has passed — give it at least a couple of hours for observations to flush through — query the history endpoint for the same location and timestamp:

GET https://api.weatherapi.com/v1/history.json
  ?key=YOUR_KEY
  &q=51.5085,-0.1257
  &dt=2025-07-03
  &hour=14

Pull temp_c, precip_mm, and condition.code from the matching hour block in the response. Diff them against your stored forecast values. That’s the error for that forecast, at that lead time, for that location.

Do that across hundreds or thousands of stored forecasts and you have an actual accuracy distribution, not a theoretical one.

What Fields Are Worth Comparing

Temperature (temp_c) is the obvious one and usually well-behaved. The history endpoint returns observed temperatures derived from METAR stations and model blend — the same general pipeline the forecast pulls from, which means the comparison isn’t perfectly apples-to-apples, but it’s the closest approximation available without running your own ground station.

Precipitation is harder. precip_mm in hourly history is an estimated accumulation, not a tipping-bucket gauge reading. It tends to be reliable on direction (rain / no rain) but noisy on exact amounts, especially for convective events where a cell hits one neighborhood and skips the next. Build your validation logic to flag large absolute errors on precip separately rather than blending them into a single RMSE figure — a 2mm miss on a convective afternoon is a different kind of failure than a 2mm miss on a steady frontal rain event.

Condition codes are categorical, so you can’t diff them numerically. What you can do is bucket them: did the forecast and observed codes both fall in the “precipitation” group? Both in “clear/partly cloudy”? A false negative (forecast said dry, observation shows rain) is operationally more significant than a false positive for most use cases. Track those separately.

Lead Time Is the Actual Variable

The single most useful thing this analysis surfaces is how accuracy degrades with lead time for your specific locations. HRRR is updated hourly and runs at a 3km grid — reasonably sharp out to about 18 hours. GFS runs at roughly 13km and extends to 240 hours. Those are different beasts, and the degradation curve isn’t the same across all variables or all geographies.

If you stored hours_ahead at fetch time, you can bin errors by lead time and plot the distribution. Errors are typically tight at 6–12 hours, start widening at 24–36, and get notably noisier past 72. But the shape of that degradation varies by location. Coastal sites near large water bodies tend to go bad faster at medium range because sea-breeze dynamics are poorly resolved at GFS resolution. Inland plains sites often hold up better at day 3 than you’d expect.

This is genuinely useful if you’re deciding how many forecast days to expose to users or how to communicate uncertainty. A 5-day forecast isn’t equally uncertain across all 5 days, and displaying it as if it is — a single temperature number with no context — is an interface choice worth questioning once you have your own validation data behind it. Weather forecast confidence degrades over time in ways that vary by location and variable, and your own data will show you exactly how.

A Practical Caveat About History Endpoint Observations

The history endpoint isn’t a verified observation archive in the same sense as a NOAA climate dataset. It’s model-assisted data blended with station observations, and the station behind any given location reading might be several kilometers away, at a different elevation, and subject to the same representativeness issues that affect real-time data. Near a coast or in terrain with real elevation variation, your “observed” history value carries its own uncertainty — it’s not ground truth in the strict meteorological sense.

We’ve dealt with this directly in our METAR station selection logic: composite scoring that accounts for elevation difference and station consistency, not just proximity. But clever station selection doesn’t solve sparse networks. In parts of rural Scotland or the western US interior, the nearest reporting station can be far enough away that the “observed” temperature has a meaningful representativeness error of its own.

The practical implication: use validation data directionally. A consistent 3°C cold bias at day 2 for a specific city is real and worth knowing. Scatter of less than 1°C where you can’t tell whether you’re measuring the forecast’s error or the history endpoint’s — you probably can’t resolve that without a ground sensor of your own.

Making This Automatable

The cleanup problem is that you have to wait for the target time to pass before you can validate, so your validation job runs on a delay. A background worker that runs daily, looks for logged forecasts with target_dt < NOW() - 2 hours and validated = false, pulls history for each, writes the diff, and marks them done — that’s about as minimal as it gets.

Two things to watch for. First, the history endpoint has a minimum date limit depending on your plan tier, so if you’re on a free plan and your validation backlog grows, you may hit a wall. Second, each history call counts against your request quota the same as any other API call. If you’re storing forecasts at hourly granularity for dozens of locations, the validation job can burn through quota fast. Batch by date and pull the full day response (dt=YYYY-MM-DD without the hour filter), then extract the hour you want locally — one request per location per day instead of one per hour.

Where This Pays Off

Most consumer-facing weather apps don’t need this. Showing a 7-day forecast to someone planning a picnic — systematic validation is overkill.

If your application is making or informing decisions with real costs attached — route planning, field operations, energy load scheduling, anything where a bad forecast has a measurable downstream consequence — understanding your actual error distribution at the lead times you rely on is worth building. You stop guessing whether day 3 forecasts are trustworthy for your use case and start knowing.

Start with one location, one variable (temperature), a 90-day window, and plot errors binned by lead time. What you find in that first pass should tell you whether it’s worth wiring up the rest.

Scroll to Top