The Weather API That Forgot to Ask for a Key — Part I
- Mic

- Jul 7
- 10 min read
Updated: Jul 12
Weather APIs normally begin with an account, an API key, a pricing table, and a quiet suspicion that checking tomorrow’s temperature may somehow require a credit card.
In this post we are looking at Open-Meteo. Meant for non-commercial use, its public endpoints can be queried without an API key. The project is open source, can be self-hosted, and exposes forecasts, historical reanalysis, archived model runs, marine conditions, air quality, flood discharge, elevation, and climate projections through a mostly consistent coordinate-and-variable interface.
That makes it useful for a weather application. It becomes more interesting when treated as a data-science source.
This series develops a number of little standalone projects. Each part looks a question to see what can be done with Open-Meteo. For this first part we look at
Can weather and marine variables be combined into a practical beach score?
The complete source code can be found as the first project in this GitHub repo. In the next part we will look at historical data.

What Is Open-Meteo?
The main Open-Meteo repository contains the API server rather than a small Python wrapper. The service ingests numerical weather-model output from national and international providers, converts it into a purpose-built time-series format, and serves location-based responses through HTTP endpoints.
The public API currently offers no-key access for non-commercial use, while attribution is required under the data licence. Multiple coordinates can be passed as comma-separated latitude and longitude lists, and the response changes from one JSON object to a list of response objects. The API also supports JSON, CSV, and XLSX output.
The central forecast endpoint combines suitable weather models by default. A particular model can also be selected when consistency matters more than automatic best-match selection. The Open-Meteo documentation currently lists global and regional model families from providers including ECMWF, NOAA, DWD, Météo-France, the UK Met Office, JMA, KMA, GEM, and the Australian Bureau of Meteorology.
The repository is also self-hostable. The official getting-started guide demonstrates running the API with Docker, downloading selected model variables, and querying the resulting local instance. That becomes relevant for high-volume analysis, but the public endpoints are more than enough for the projects in this post.
The APIs Used Here
The projects use four endpoints:
Endpoint | Purpose in this post |
Forecast API | Current weather forecasts and multi-location NSW requests |
Marine API | Waves, wave period, and sea-surface temperature |
Historical Weather API | ERA5-Land reanalysis for long-run pattern analysis |
Previous Model Runs API | Forecast values aligned to fixed one-to-seven-day lead times |
Other available services include ensembles, seasonal forecasts, air quality, satellite radiation, flood discharge, geocoding, elevation, and climate projections. Those could comfortably support another series of projects.
Choosing the Right Kind of Weather Data
Open-Meteo’s datasets look similar at the HTTP level: provide coordinates, select variables, specify a time range, and receive arrays. They do not, however, represent the same thing.
A live forecast describes what a numerical model currently expects. Reanalysis combines observations and modelling into a consistent reconstruction of the past. Previous model runs preserve what a forecasting system predicted at different lead times.
A consistent API reduces plumbing. It does not remove the need to choose the correct dataset.
Project Layout
The scripts in this series all assume the same folder hierarchy for data, tabular outputs, GIS products, and figures:
./
├── project.py
├── data/
├── outputs/
└── figures/In all projects, these will be created at the start via the function
Depending on the project the respective function might contain a few more folders that are relevant.
Project 1: Is It Actually a Beach Day?
Australia has more than enough beaches. The difficult part is deciding when to visit one.
A weather forecast can tell us the temperature. A marine forecast can tell us the wave height. Neither will directly answer the more practical question:
At which beach, and at what time, do the conditions look most inviting?
In this project, we will combine hourly weather and marine forecasts for a few beaches along Australia's eastern coast and one in Melbourne:
Bondi, Sydney
Manly, Sydney
St Kilda, Melbourne
Greenfield Beach, Jervis Bay
Surfers Paradise, Gold Coast
Nudgee Beach, Brisbane
We will then calculate a transparent beach-comfort score from zero to 100, export the underlying data, and create a heatmap showing the most promising daylight hours.
The score is deliberately simple. It is not a machine-learning model, an official beach rating, or a substitute for checking warnings. It is just a documented preference model that turns several forecast variables into one convenient number.
What the Project Produces
When the script runs, it creates two useful outputs:
outputs/project_01_beach_conditions.csv
figures/01_beach_score_heatmap.pngThe CSV contains the complete hourly forecast table, including the individual weather and marine measurements. The image summarises the calculated scores and marks the highest-scoring daylight hour for each beach.
For the project we will need the following packages
matplotlib
numpy
pandas
requests
In addition we will need Path from the pathlib package and Any from typing.
Keeping Project Choices in One Place
Rather than scattering API addresses, directory names, hours, and plotting settings throughout the script, we collect them as global configuration constants.
The project uses two Open-Meteo endpoints.
FORECAST_URL provides atmospheric weather data such as air temperature, rain probability, wind speed, and UV index.
MARINE_URL provides ocean-related measurements such as wave height, wave period, and sea-surface temperature.
The two services are queried separately because the weather and marine models do not necessarily use the same point on the map.
We also set the timezone explicitly. Without that, timestamps may arrive in UTC, leaving us to mentally convert a 9:00 pm forecast into tomorrow morning.
The daylight constants limit the visualisation to hours between 6:00 am and 8:00 pm. The API still downloads the complete hourly forecast, but the heatmap excludes the hours when a beach visit would mainly involve darkness and drunk backpackers.
Output Folders and Filenames
All file locations are defined globally.
This makes the storage layout visible near the top of the script.
The data directory is included for completeness, even though this script does not currently cache the raw API responses. The processed table goes into outputs, while the chart is stored separately in figures.
Choosing Land and Sea Coordinates
Each beach needs two coordinate pairs.
The land coordinates are used for the ordinary weather forecast. They sit close to the beach itself and represent the conditions a visitor is likely to experience on shore.
The sea coordinates sit slightly offshore and are used for the marine forecast.
This distinction matters. Asking a marine model about a point on dry land may return missing values or measurements from a nearby model cell that is not representative of the water immediately offshore.
The values are stored in a nested dictionary:
BEACHES["Bondi"]["land"]returns:
(-33.8915, 151.2767)The script contains the coordinates for the other beaches as well.
Selecting Forecast Variables
Open-Meteo offers many possible measurements. We only request the ones needed for the analysis.
The weather variables describe conditions above or near the surface:
temperature_2m is the forecast air temperature two metres above the ground.
precipitation_probability estimates the chance of precipitation.
wind_speed_10m is the forecast wind speed ten metres above the ground.
uv_index represents expected ultraviolet exposure.
The marine variables describe the nearby sea:
wave_height gives the modelled significant wave height.
wave_period describes the average interval between waves.
sea_surface_temperature gives the temperature of the upper ocean surface.
The wave period does not currently affect the score. We still download it because it adds useful context to the exported table and the summary of the best forecast periods.
That is an important distinction: not every variable in an analysis must be compressed into the final metric.
A Reusable API Request Function
Both Open-Meteo endpoints return JSON, so we place the common request logic in one function.
The function accepts an endpoint and a dictionary of query parameters.
Passing the parameters through params lets requests construct and encode the URL:
requests.get(url, params=params)The timeout prevents the script from waiting indefinitely if the server or network stops responding.
response.raise_for_status()turns unsuccessful HTTP responses into exceptions. A missing endpoint or server error should produce a visible failure rather than quietly passing an unusable response into Pandas.
We also inspect the decoded JSON. The script expects one dictionary-like response object, and Open-Meteo may return an error description inside that object.
Centralising these checks means that the weather and marine requests behave consistently.
Converting an API Section into a DataFrame
An Open-Meteo response contains several sections. The measurements requested by this project live under the hourly key.
The following helper converts such a section into a Pandas DataFrame.
The response section usually resembles a dictionary of parallel lists:
{
"time": ["2026-07-12T00:00", "2026-07-12T01:00"],
"temperature_2m": [13.2, 12.8],
"wind_speed_10m": [9.1, 8.7],
}Pandas converts each key into a column and aligns values by their list position.
The timestamp begins as text, so we explicitly convert it:
frame["time"] = pd.to_datetime(frame["time"])Once the column contains proper datetime values, we can extract hours, sort chronologically, format axis labels, and merge forecasts by time.
Downloading Conditions for One Beach
We can now fetch the weather and marine forecasts for one beach. First, we extract all of our coordinates.
Next, we request the land-based weather variables.
The API expects the hourly variables as one comma-separated string. Rather than maintaining that string manually, we create it from the configuration list:
",".join(WEATHER_VARIABLES)This produces:
temperature_2m,precipitation_probability,wind_speed_10m,uv_indexThe marine request follows the same structure but uses the offshore coordinates.
Both hourly sections are converted to DataFrames and merged on the timestamp.
An inner join retains timestamps that occur in both responses.
The validate argument is particularly useful:
validate="one_to_one"It tells Pandas that every timestamp should occur no more than once in each table. If duplicate timestamps appear unexpectedly, the merge fails instead of silently multiplying rows. Finally, the beach name is added as a column with the name "beach".
Inventing a Beach Score
Now we reach the completely scientific process of deciding what “nice beach weather” means. Or in other words, I make up what I think is good beach weather...your opinion may vary wildly here.
The score begins at 100 and applies a series of penalties.
Creating the score as a Pandas Series preserves the original DataFrame index. This makes it safe to assign the result back as a new column later.
Air-Temperature Penalty
score -= (frame["temperature_2m"] - 25).abs() * 3The model treats 25°C as the preferred air temperature. We look at the difference and multiply that by 3....I really like it being 25°C.
Rain Penalty
score -= frame["precipitation_probability"].fillna(0) * 0.7Every percentage point of rain probability removes 0.7 points. In other words, a 50% rain probability therefore costs 35 points.
Missing precipitation probabilities are treated as zero here. That is a modelling choice rather than a universal rule. Another project might treat a missing rain forecast as uncertainty and invalidate the whole score.
Strong-Wind Penalty
score -= (frame["wind_speed_10m"] - 18).clip(lower=0) * 2Wind speeds up to 18 km/h receive no penalty. Every kilometre per hour above the threshold removes two points.
Wave-Height Penalty
score -= (frame["wave_height"] - 1.0).abs() * 10The comfort model prefers a wave height near one metre.
Wave heights much smaller or larger than that target lose points. This is a general-purpose preference rather than a surfing score. A surfer, swimmer, parent with small children, and person attempting to keep a sandwich dry would probably choose different targets.
UV Penalty
score -= (frame["uv_index"] - 7).clip(lower=0) * 4UV values up to seven receive no penalty. Each point above seven removes four score points. Considering that we are in Australia and the sun can be really bad, this might even be a bit too low of a penalty.
Cold-Water Penalty
score -= (19 - frame["sea_surface_temperature"]).clip(lower=0) * 3Water temperatures of 19°C or warmer receive no penalty. For colder water, every degree below 19 removes three points.
Handling Missing Inputs
A score should not look authoritative when one of its essential ingredients is absent.
This selects the essential scoring columns and checks for missing values in the row, returning a Boolean value. We then restrict valid scores to the intended range and invalidate incomplete rows.
Restricting the Analysis to Daylight Hours
The downloaded table includes every hour, including the middle of the night. For the heatmap and best-period summary, we create a daylight-only subset.
We simply look at the rows of the dataframe where the time entry is between 6:00 am and 8:00 pm.
Turning the Scores into a Heatmap
The next function builds the main visualisation.
We restrict our data to daylight hours. We use pivot_table to reshape the data:
index="beach" creates one row per beach.
columns="time" creates one column per forecast hour.
values="beach_score" fills the grid with scores.
The mean aggregation is mainly defensive. There should only be one score for each beach-time combination, but pivot_table requires an aggregation rule in case duplicates exist and if there is a duplicate for some reason, the mean is a good aggregation method.
We sort the beaches alphabetically and the timestamps chronologically.
Before plotting, one should also raise an error if for some reason nothing survived the daylight filter.
The heatmap itself uses imshow.
We use the obvious options here, making sure that it stretches to fite the wide chart, and using "nearest" for interpolation to keep cell boundaries sharp and not blend into each other.
To be able to compare on differrent dates, we use fixed limits
vmin=0
vmax=100Of course we should also add nice labels, we omit the exact code for that here.
Marking the Best Hour for Each Beach
The heatmap shows all daylight periods, but we also want to identify the best-scoring hour for each beach.
The loop processes one beach row at a time and skips beaches where all values are missing.
We locate the column containing the highest valid score, using np.nanargmax to ignore NaN values and returns the position of the maximum remaining value.
We then draw a white star over that cell.
Making sure with zorder=3 that the marker is placed above the heatmap. We also add a numerical score just below the star. We also omit the code for title, colour bar, and export.
Running Everything
Now we put everything together. After ensuring that our folders all exists we fetch our data.
We then calculate out beach scores and add them to our existig dataframe. We can then plot the heatmap and do other things, like saving all the data in a CSV file and printing a list of the conditions for each beach at their best beach score time. This produces then something that looks as follows.

From API Response to Data-Science Project
This project as well as the next ones share a simple workflow.
First, each request selects only variables that serve the question. Weather services expose many tempting columns, but downloading everything usually creates a wider table rather than a clearer analysis.
Second, the response arrays become tidy tables with explicit timestamps. Timezones, units, missing values, and hourly-versus-daily resolution are handled before modelling begins.
Third, the assumptions remain visible. The beach score exposes its penalties.
Finally, each figure answers one question. In the project here: when do beach conditions align.
Practical Caveats
We should mention a number of caveats when using this data.
A coordinate is not a station
The API returns model-grid values. The model coordinate used may differ from the requested coordinate, and elevation downscaling can alter the local value unless explicitly disabled.
Forecast models change
Operational systems are upgraded. This is why Open-Meteo separates long-run reanalysis, historical forecasts, fixed-lead previous runs, and archived individual runs.
Cache repeated requests
Development code should avoid downloading identical data whenever a label, title, or plotting choice changes.
Do not turn exploratory maps into warnings
None of the projects should replace official weather warnings, emergency information, marine navigation products, beach safety advice, or local observations.


Comments