Currently, open data are available from the following locations:
- ECMWF Data Store offers data sets from current date and up to four days before today.

- Amazon’s AWS hosts data from 2023 to 2025.
File-naming convention
An example (for HH=00z
and stream=oper
) shows how a filename has changed since year 2023
- 20230118/00z/0p4-beta/oper/20230118000000-0h-oper-fc.grib2,
in Februar 2024 one can choose between the resolution0p4-beta
and0p25
- 20240201/00z//oper/20240201000000-0h-oper-fc.grib2
- 20240201/00z//oper/20240201000000-0h-oper-fc.grib2,
in March 2024 one can choose betweenaifs
andifs
(onlyifs
is available in0p4-beta
or0p25
) - 20240301/00z//0p25/oper/20240301000000-0h-oper-fc.grib2
- 20240301/00z//0p4-beta/oper/20240301000000-0h-oper-fc.grib2,
in February 2025 one can choose betweenaifs-single
andaifs
(ifs
does not contain any changes) - 20250210/00z//0p25//oper/20250210000000-0h-oper-fc.grib2
- 20250210/00z//0p25/oper/20250210000000-0h-oper-fc.grib2,
in March 2025 the file-naming convention is the same as we know it today - 20250301/00z/aifs-single/0p25/oper/20250301000000-0h-oper-fc.grib2
- 20250301/00z/ifs/0p25/oper/20250301000000-0h-oper-fc.grib2


- Microsoft’s Azure makes the ECMWF products available for the previous 30 days.

The example below shows how to download yesterday’s data.
!pip3 install pystac_client planetary_computer
!pip3 install xarray cfgrib
import pystac_client
import planetary_computer
import requests
import xarray as xr
catalog = pystac_client.Client.open(
"https://planetarycomputer.microsoft.com/api/stac/v1",
modifier=planetary_computer.sign_inplace,
)
search = catalog.search(
collections=["ecmwf-forecast"],
query={
"ecmwf:stream": {"eq": "oper"},
"ecmwf:type": {"eq": "fc"},
"ecmwf:step": {"eq": "0h"},
},
)
items = search.item_collection()
# select the most recent item
item = max(items, key=lambda item: item.datetime)
url = item.assets["data"].href
url_ecmwf = url.rpartition('?')[0]
filename = str(url_ecmwf.rpartition('oper/')[-1])
r = requests.get(url, stream=True)
with open(filename, mode="wb") as file:
for chunk in r.iter_content(chunk_size=10 * 1024):
file.write(chunk)
ds = xr.open_dataset(f'./{filename}', engine='cfgrib',
decode_timedelta=True,
backend_kwargs={'filter_by_keys': {'typeOfLevel': 'soilLayer'}})
ds
Retrieve data for the date range of your interest
import pystac_client
import planetary_computer
import requests
import earthkit.data as ekd
catalog = pystac_client.Client.open(
"https://planetarycomputer.microsoft.com/api/stac/v1",
modifier=planetary_computer.sign_inplace,
)
# specify a date range
time_range = "2025-06-01/2025-06-01"
search = catalog.search(
collections=["ecmwf-forecast"],
query={
"ecmwf:stream": {"eq": "oper"},
"ecmwf:type": {"eq": "fc"},
"ecmwf:step": {"eq": "0h"},
},
datetime=time_range)
items = search.item_collection()
list_of_files = []
for item in items:
url = item.assets["data"].href
url_ecmwf = url.rpartition('?')[0]
filename = str(url_ecmwf.rpartition('oper/')[-1])
r = requests.get(url, stream=True)
with open(filename, mode="wb") as file:
for chunk in r.iter_content(chunk_size=10 * 1024):
file.write(chunk)
list_of_files.append(filename)
for file_ in list_of_files:
print(f'File: {file_}')
ds = ekd.from_source("file", f'./{file_}')
ds.ls()
- Open-Meteo offers a diverse range of APIs that will suit your weather-related needs:

!pip3 install openmeteo-requests
!pip3 install requests-cache retry-requests numpy pandas
import openmeteo_requests
import pandas as pd
import requests_cache
from retry_requests import retry
# Setup the Open-Meteo API client with cache and retry on error
cache_session = requests_cache.CachedSession('.cache', expire_after = 3600)
retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2)
openmeteo = openmeteo_requests.Client(session = retry_session)
# Make sure all required weather variables are listed here
# The order of variables in hourly or daily is important to assign them correctly below
url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": 52.52,
"longitude": 13.41,
"hourly": "temperature_2m",
"models": "ecmwf_ifs025"
}
responses = openmeteo.weather_api(url, params=params)
# Process first location. Add a for-loop for multiple locations or weather models
response = responses[0]
model = "IFS" if response.Model() == 60 else "AIFS"
print(f"ECMWF {model} model")
print(f"Coordinates {response.Latitude()}°N {response.Longitude()}°E")
print(f"Elevation {response.Elevation()} m asl")
print(f"Timezone {response.Timezone()}{response.TimezoneAbbreviation()}")
print(f"Timezone difference to GMT+0 {response.UtcOffsetSeconds()} s")
# Process hourly data. The order of variables needs to be the same as requested.
hourly = response.Hourly()
hourly_temperature_2m = hourly.Variables(0).ValuesAsNumpy()
hourly_data = {"date": pd.date_range(
start = pd.to_datetime(hourly.Time(), unit = "s", utc = True),
end = pd.to_datetime(hourly.TimeEnd(), unit = "s", utc = True),
freq = pd.Timedelta(seconds = hourly.Interval()),
inclusive = "left"
)}
hourly_data["temperature_2m"] = hourly_temperature_2m
hourly_dataframe = pd.DataFrame(data = hourly_data)
hourly_dataframe.to_csv(f'{model}_lat{response.Latitude()}-lon{response.Longitude()}_hourly.csv', index=False)
print(hourly_dataframe)
ECMWF IFS model
Coordinates 52.5°N 13.5°E
Elevation 38.0 m asl
Timezone NoneNone
Timezone difference to GMT+0 0 s
date temperature_2m
0 2025-06-20 00:00:00+00:00 13.850000
1 2025-06-20 01:00:00+00:00 12.800000
2 2025-06-20 02:00:00+00:00 11.800000
3 2025-06-20 03:00:00+00:00 11.450000
4 2025-06-20 04:00:00+00:00 12.000000
.. ... ...
163 2025-06-26 19:00:00+00:00 26.250000
164 2025-06-26 20:00:00+00:00 25.450001
165 2025-06-26 21:00:00+00:00 24.700001
166 2025-06-26 22:00:00+00:00 23.950001
167 2025-06-26 23:00:00+00:00 23.250000
[168 rows x 2 columns]
- Forecast API allows you to access accurate weather forecasts for up to 16 days.
Example of three weather variables and two models for different locations
import openmeteo_requests
import pandas as pd
import requests_cache
from retry_requests import retry
# Setup the Open-Meteo API client with cache and retry on error
cache_session = requests_cache.CachedSession('.cache', expire_after = 3600)
retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2)
openmeteo = openmeteo_requests.Client(session = retry_session)
# Make sure all required weather variables are listed here
# The order of variables in hourly or daily is important to assign them correctly below
url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": [51.51, 55.95, 53.35],
"longitude": [0.13, 3.19, 6.26],
"daily": ["rain_sum", "precipitation_sum", "cloud_cover_mean"],
"models": ["ecmwf_ifs025", "ecmwf_aifs025_single"],
"timezone": "auto",
"start_date": "2025-06-10",
"end_date": "2025-06-17"
}
responses = openmeteo.weather_api(url, params=params)
# Process first location. Add a for-loop for multiple locations or weather models
for r in range(len(responses)):
response = responses[r]
model = "IFS" if response.Model() == 60 else "AIFS"
print(f"ECMWF {model} model")
print(f"Coordinates {response.Latitude()}°N {response.Longitude()}°E")
print(f"Elevation {response.Elevation()} m asl")
print(f"Timezone {response.Timezone()}{response.TimezoneAbbreviation()}")
print(f"Timezone difference to GMT+0 {response.UtcOffsetSeconds()} s")
# Process daily data. The order of variables needs to be the same as requested.
daily = response.Daily()
daily_rain_sum = daily.Variables(0).ValuesAsNumpy()
daily_precipitation_sum = daily.Variables(1).ValuesAsNumpy()
daily_cloud_cover_mean = daily.Variables(2).ValuesAsNumpy()
daily_data = {"date": pd.date_range(
start = pd.to_datetime(daily.Time(), unit = "s", utc = True),
end = pd.to_datetime(daily.TimeEnd(), unit = "s", utc = True),
freq = pd.Timedelta(seconds = daily.Interval()),
inclusive = "left"
)}
daily_data["rain_sum"] = daily_rain_sum
daily_data["precipitation_sum"] = daily_precipitation_sum
daily_data["cloud_cover_mean"] = daily_cloud_cover_mean
daily_dataframe = pd.DataFrame(data = daily_data)
daily_dataframe.to_csv(f'{model}_lat{response.Latitude()}-lon{response.Longitude()}_daily.csv', index=False)
print(daily_dataframe)
Table 1:The Forecast API provides you with data of ECMWF IFS and AIFS models.
Forecast Data Provider | Model | Resolution | Forecast Length | Frequency |
---|---|---|---|---|
ECMWF | IFS & AIFS | 25 km | 15 days | Every 6 hours |
- Historical Weather API provides access to archived ECMWF IFS model data from 2017 onwards or ECMWF IFS Assimilation Long-Window model data that are available since 2024.
Historical Data - IFS model
An example of retrieving open data from ECMWF IFS model for two sites: Port Elizabeth (33.91˚S, 25.58 °E) and East London (33.02˚S, 27.91°E), for year 2017. The selected daily weather variables are Maximum Wind Speed (10 m), Maximum Wind Gust (10 m), Mean Wind Gusts (10 m), and Mean Wind Speed (10 m). For hourly weather variables, we chose Wind Speed (10 m), Wind Speed (100 m), and Wind Gusts (10 m).
import openmeteo_requests
import pandas as pd
import requests_cache
from retry_requests import retry
# Setup the Open-Meteo API client with cache and retry on error
cache_session = requests_cache.CachedSession('.cache', expire_after = -1)
retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2)
openmeteo = openmeteo_requests.Client(session = retry_session)
# Make sure all required weather variables are listed here
# The order of variables in hourly or daily is important to assign them correctly below
url = "https://archive-api.open-meteo.com/v1/archive"
params = {
"latitude": [33.91, 33.02],
"longitude": [25.58, 27.91],
"start_date": "2017-01-01",
"end_date": "2017-12-31",
"daily": ["wind_speed_10m_max", "wind_gusts_10m_max", "wind_gusts_10m_mean", "wind_speed_10m_mean"],
"hourly": ["wind_speed_10m", "wind_gusts_10m", "wind_speed_100m"],
"models": "ecmwf_ifs",
"timezone": "auto"
}
responses = openmeteo.weather_api(url, params=params)
# Process first location. Add a for-loop for multiple locations or weather models
for r in range(len(responses)):
response = responses[r]
model = "IFS" if response.Model() == 60 else "AIFS"
print(f"Coordinates {response.Latitude()}°N {response.Longitude()}°E")
print(f"Elevation {response.Elevation()} m asl")
print(f"Timezone {response.Timezone()}{response.TimezoneAbbreviation()}")
print(f"Timezone difference to GMT+0 {response.UtcOffsetSeconds()} s")
# Process hourly data. The order of variables needs to be the same as requested.
hourly = response.Hourly()
hourly_wind_speed_10m = hourly.Variables(0).ValuesAsNumpy()
hourly_wind_gusts_10m = hourly.Variables(1).ValuesAsNumpy()
hourly_wind_speed_100m = hourly.Variables(2).ValuesAsNumpy()
hourly_data = {"date": pd.date_range(
start = pd.to_datetime(hourly.Time(), unit = "s", utc = True),
end = pd.to_datetime(hourly.TimeEnd(), unit = "s", utc = True),
freq = pd.Timedelta(seconds = hourly.Interval()),
inclusive = "left"
)}
hourly_data["wind_speed_10m"] = hourly_wind_speed_10m
hourly_data["wind_gusts_10m"] = hourly_wind_gusts_10m
hourly_data["wind_speed_100m"] = hourly_wind_speed_100m
hourly_dataframe = pd.DataFrame(data = hourly_data)
hourly_dataframe.to_csv(f'{model}_lat{response.Latitude()}-lon{response.Longitude()}_hourly.csv', index=False)
print(hourly_dataframe)
# Process daily data. The order of variables needs to be the same as requested.
daily = response.Daily()
daily_wind_speed_10m_max = daily.Variables(0).ValuesAsNumpy()
daily_wind_gusts_10m_max = daily.Variables(1).ValuesAsNumpy()
daily_wind_gusts_10m_mean = daily.Variables(2).ValuesAsNumpy()
daily_wind_speed_10m_mean = daily.Variables(3).ValuesAsNumpy()
daily_data = {"date": pd.date_range(
start = pd.to_datetime(daily.Time(), unit = "s", utc = True),
end = pd.to_datetime(daily.TimeEnd(), unit = "s", utc = True),
freq = pd.Timedelta(seconds = daily.Interval()),
inclusive = "left"
)}
daily_data["wind_speed_10m_max"] = daily_wind_speed_10m_max
daily_data["wind_gusts_10m_max"] = daily_wind_gusts_10m_max
daily_data["wind_gusts_10m_mean"] = daily_wind_gusts_10m_mean
daily_data["wind_speed_10m_mean"] = daily_wind_speed_10m_mean
daily_dataframe = pd.DataFrame(data = daily_data)
daily_dataframe.to_csv(f'{model}_lat{response.Latitude()}-lon{response.Longitude()}_daily.csv', index=False)
print(daily_dataframe)
Table 2:The Historical Weather API allows users to retrieve historical weather data of ECMWF IFS (using simulation runs at 00z and 12z,) and IFS Assimilation Long-Window models for a specific location and time period.
Forecast Data Provider | Model | Spatial Resolution | Temporal Resolution | Frequency | Available Since |
---|---|---|---|---|---|
ECMWF | IFS | 9 km | hourly | daily with 2 days delay | 2017 to present |
ECMWF | IFS Assimilation Long-Window | 9 km | 6-hourly | daily with 2 days delay | 2024 to present |
Historical Forecast API provides access to archived model data from the ECMWF Weather Forecast API.
Historical Data - IFS & AIFS
An example of retrieving open data from ECMWF IFS and AIFS models for Bern (46.95° N, 7.45° E) from 2024-02-03
to 2025-06-15
. The selected daily weather variables are Maximum Temperature (2 m), Minimum Temperature (2 m), and Mean Temperature (2 m). For hourly weather variables, we chose Temperature (2 m), Rain, and Snowfall.
import openmeteo_requests
import pandas as pd
import requests_cache
from retry_requests import retry
# Setup the Open-Meteo API client with cache and retry on error
cache_session = requests_cache.CachedSession('.cache', expire_after = 3600)
retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2)
openmeteo = openmeteo_requests.Client(session = retry_session)
# Make sure all required weather variables are listed here
# The order of variables in hourly or daily is important to assign them correctly below
url = "https://historical-forecast-api.open-meteo.com/v1/forecast"
params = {
"latitude": 46.95,
"longitude": 7.45,
"start_date": "2024-02-03",
"end_date": "2025-06-15",
"daily": ["temperature_2m_max", "temperature_2m_mean", "temperature_2m_min"],
"hourly": ["rain", "snowfall", "temperature_2m"],
"models": ["ecmwf_ifs025", "ecmwf_aifs025_single"],
"timezone": "auto"
}
responses = openmeteo.weather_api(url, params=params)
# Process first location. Add a for-loop for multiple locations or weather models
for r in range(len(responses)):
response = responses[r]
model = "IFS" if response.Model() == 60 else "AIFS"
print(f"ECMWF {model} model")
print(f"Coordinates {response.Latitude()}°N {response.Longitude()}°E")
print(f"Elevation {response.Elevation()} m asl")
print(f"Timezone {response.Timezone()}{response.TimezoneAbbreviation()}")
print(f"Timezone difference to GMT+0 {response.UtcOffsetSeconds()} s")
# Process hourly data. The order of variables needs to be the same as requested.
hourly = response.Hourly()
hourly_rain = hourly.Variables(0).ValuesAsNumpy()
hourly_snowfall = hourly.Variables(1).ValuesAsNumpy()
hourly_temperature_2m = hourly.Variables(2).ValuesAsNumpy()
hourly_data = {"date": pd.date_range(
start = pd.to_datetime(hourly.Time(), unit = "s", utc = True),
end = pd.to_datetime(hourly.TimeEnd(), unit = "s", utc = True),
freq = pd.Timedelta(seconds = hourly.Interval()),
inclusive = "left"
)}
hourly_data["rain"] = hourly_rain
hourly_data["snowfall"] = hourly_snowfall
hourly_data["temperature_2m"] = hourly_temperature_2m
hourly_dataframe = pd.DataFrame(data = hourly_data)
hourly_dataframe.to_csv(f'{model}_lat{response.Latitude()}-lon{response.Longitude()}_hourly.csv', index=False)
print(hourly_dataframe)
# Process daily data. The order of variables needs to be the same as requested.
daily = response.Daily()
daily_temperature_2m_max = daily.Variables(0).ValuesAsNumpy()
daily_temperature_2m_mean = daily.Variables(1).ValuesAsNumpy()
daily_temperature_2m_min = daily.Variables(2).ValuesAsNumpy()
daily_data = {"date": pd.date_range(
start = pd.to_datetime(daily.Time(), unit = "s", utc = True),
end = pd.to_datetime(daily.TimeEnd(), unit = "s", utc = True),
freq = pd.Timedelta(seconds = daily.Interval()),
inclusive = "left"
)}
daily_data["temperature_2m_max"] = daily_temperature_2m_max
daily_data["temperature_2m_mean"] = daily_temperature_2m_mean
daily_data["temperature_2m_min"] = daily_temperature_2m_min
daily_dataframe = pd.DataFrame(data = daily_data)
daily_dataframe.to_csv(f'{model}_lat{response.Latitude()}-lon{response.Longitude()}_daily.csv', index=False)
print(daily_dataframe)
Table 3:The Historical Forecast API archives comprehensive data of ECMWF IFS and AIFS models, data is available since 2022.
Forecast Data Provider | Model | Spatial Resolution | Temporal Resolution | Frequency | Available Since |
---|---|---|---|---|---|
IFS 0.4° | 0.4° (~44 km) | 3-hourly | 2022-11-07 | ||
ECMWF | IFS 0.25° | 0.25° (~25 km) | 3-hourly | every 6 hours | 2024-02-03 |
AIFS 0.25° Single | 0.25° (~25 km) | 6-hourly | 2025-02-25 |
ECMWF IFS 0.25° Ensemble
An example shows how to retrieve weather data of daily mean temperature (2 m) and daily mean wind speed (10 m) in Berlin in the past 3 months.
https://ensemble-api.open-meteo.com/v1/ensemble?latitude=52.52&longitude=13.41&daily=temperature_2m_mean,wind_speed_10m_mean&models=ecmwf_ifs025&timezone=Europe%2FBerlin&past_days=92
import openmeteo_requests
from openmeteo_sdk.Variable import Variable
from openmeteo_sdk.Aggregation import Aggregation
import pandas as pd
import requests_cache
from retry_requests import retry
# Setup the Open-Meteo API client with cache and retry on error
cache_session = requests_cache.CachedSession('.cache', expire_after = 3600)
retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2)
openmeteo = openmeteo_requests.Client(session = retry_session)
# Make sure all required weather variables are listed here
# The order of variables in hourly or daily is important to assign them correctly below
url = "https://ensemble-api.open-meteo.com/v1/ensemble"
params = {
"latitude": 52.52,
"longitude": 13.41,
"daily": ["temperature_2m_mean", "wind_speed_10m_mean"],
"models": "ecmwf_ifs025",
"timezone": "Europe/Berlin",
"past_days": 92
}
responses = openmeteo.weather_api(url, params=params)
# Process first location. Add a for-loop for multiple locations or weather models
response = responses[0]
model = "IFS" if response.Model() == 60 else "AIFS"
print(f"ECMWF {model} model")
print(f"Coordinates {response.Latitude()}°N {response.Longitude()}°E")
print(f"Elevation {response.Elevation()} m asl")
print(f"Timezone {response.Timezone()}{response.TimezoneAbbreviation()}")
print(f"Timezone difference to GMT+0 {response.UtcOffsetSeconds()} s")
# Process daily data
daily = response.Daily()
daily_variables = list(map(lambda i: daily.Variables(i), range(0, daily.VariablesLength())))
daily_temperature_2m_mean = filter(lambda x: x.Variable() == Variable.temperature and x.Altitude() == 2 and x.Aggregation() == Aggregation.mean, daily_variables)
daily_wind_speed_10m_mean = filter(lambda x: x.Variable() == Variable.wind_speed and x.Altitude() == 10 and x.Aggregation() == Aggregation.mean, daily_variables)
daily_data = {"date": pd.date_range(
start = pd.to_datetime(daily.Time(), unit = "s", utc = True),
end = pd.to_datetime(daily.TimeEnd(), unit = "s", utc = True),
freq = pd.Timedelta(seconds = daily.Interval()),
inclusive = "left"
)}
# Process all members
for variable in daily_temperature_2m_mean:
member = variable.EnsembleMember()
daily_data[f"temperature_2m_mean_member{member}"] = variable.ValuesAsNumpy()
for variable in daily_wind_speed_10m_mean:
member = variable.EnsembleMember()
daily_data[f"wind_speed_10m_mean_member{member}"] = variable.ValuesAsNumpy()
daily_dataframe = pd.DataFrame(data = daily_data)
daily_dataframe.to_csv(f'{model}_lat{response.Latitude()}-lon{response.Longitude()}_daily.csv', index=False)
print(daily_dataframe)
Copyright Statement: Copyright “© 2025 European Centre for Medium-Range Weather Forecasts (ECMWF)”.
Source: www.ecmwf.int
Licence Statement: This data is published under a Creative Commons Attribution 4.0 International (CC BY 4.0). https://
creativecommons .org /licenses /by /4 .0/ Disclaimer: ECMWF does not accept any liability whatsoever for any error or omission in the data, their availability, or for any loss or damage arising from their use.