DMI API Tutorial#

This tutorial gives an introduction on how to use the Danish Meteorological Institute’s (DMI) API to download meterological observation data (v2).

The tutorial uses the Python programming language and is in the format of a Jupyter Notebook. The notebook can be downloaded and run locally, allowing you to quickly get started downloading data. Part 1 of the tutorial provides some background basic information on how to work with the API, whereas a complete example is provided in Part 2.

If you’re new to the DMI observation data, I recommend that you check out some of the following links:

  1. Meteorological observations data

  2. Meteorological observations API

  3. Station list

  4. Station list explained

  5. FAQ

  6. Terms of use

  7. Operational status

  8. User creation


First, in order to retrieve data, it is necessary to create a user and obtain an API key. This API key grants permission to retrieve data and allows DMI to generate usage statistics.

A guide to creating a user profile and getting an API key can be found here.

api_key = 'xxxxxxxx-yyyy-zzzz-iiii-jjjjjjjjjjjj' # insert your own key between the '' signs
Hide code cell content
# Delete this cell if you run the notebook locally
import os
api_key = os.environ["DMI_API_KEY"]

An easy test to see if your API key works is to paste the following url into your browser, followed by a question mark and your api-key, e.g.: https://dmigw.govcloud.dk/v2/metObs/collections?api-key=xxxxxxxx-yyyy-zzzz-iiii-jjjjjjjjjjjj (the example API key error).

If you have obtained an API key and pasted it correctly, a page with text will be shown. If the API key does not work, the page will say “API key invalid”.


The following code blocks retrieve a list of all the DMI stations (both in Denmark and in Greenland) and plots them on a map using the Python package Folium.

Hide code cell content
import requests
import pandas as pd
r = requests.get('https://dmigw.govcloud.dk/v2/metObs/collections/station/items', params={'api-key': api_key})
stations = pd.json_normalize(r.json()['features'])
stations.columns = [c.replace('properties.', '').replace('geometry.', '') for c in stations.columns]

# Fileter out inactive stations
stations = stations[stations['status'] == 'Active']
# This line removes previous locations of the same station
# thus only the newest/current location is shown
stations = stations[stations['validTo'].isna()]
stations
type id type coordinates owner country anemometerHeight wmoCountryCode operationFrom parameterId ... type stationHeight regionId name wmoStationId operationTo updated stationId validTo status
2 Feature fb2d82b4-a527-6430-d860-7b512397237d Point [-68.7031, 76.5311] Mittafik/Grønlandske lufthavne GRL NaN 6070 1948-12-31T00:00:00Z [cloud_cover, cloud_height, humidity, pressure... ... Synop 77.00 4 Pituffik 04202 None None 04202 None Active
4 Feature b4d0f681-3e73-ca98-817c-620c78f6b9e7 Point [10.2705, 57.4029] DMI DNK NaN None 2011-10-01T00:00:00Z [snow_cover_man, snow_depth_man] ... Manual snow 61.00 None Lendum None None None 20085 None Active
6 Feature a544dec9-e92e-d32f-cf5a-8c0639882ed6 Point [9.9592, 57.2825] DMI DNK NaN None 2024-12-16T00:00:00Z [snow_cover_man, snow_depth_man] ... Manual snow 30.50 None Brønderslev None None None 20119 None Active
7 Feature b3a7c23b-644a-4afe-c1b8-c8368a25d816 Point [-73.1208, 76.7333] DMI GRL NaN 6070 1980-06-15T00:00:00Z [humidity, pressure, pressure_at_sea, temp_dew... ... GIWS 11.00 4 Kitsissut 04203 None None 04203 None Active
10 Feature 487ad7cc-c49a-3581-cb7e-91a7eb8c3c59 Point [-69.3744, 77.4853] Mittafik/Grønlandske lufthavne GRL NaN 6070 1964-01-01T00:00:00Z [cloud_cover, cloud_height, humidity, humidity... ... Synop 16.00 4 Mittarfik Qaanaaq 04205 None None 04205 None Active
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
675 Feature fef7d6f1-ed82-004e-4e6d-bb24d348ae51 Point [14.7718, 55.2979] DMI DNK 10.0 6080 1953-03-01T00:00:00Z [cloud_cover, cloud_height, humidity, humidity... ... Synop 7.81 6 Hammer Odde Fyr 06193 None None 06193 None Active
678 Feature 48556d98-a86d-3a8d-255e-608a26a609df Point [15.0953, 55.0557] DMI DNK 10.0 6080 2002-09-19T00:00:00Z [humidity, humidity_past1h, leav_hum_dur_past1... ... Synop 23.13 6 Nexø Vest 06197 None None 06197 None Active
679 Feature 49d27fda-cc01-48fb-6ffb-2367a549be12 Point [10.6318, 57.7363] DMI DNK NaN None 2001-03-02T00:00:00Z [snow_cover_man, snow_depth_man] ... Manual snow 3.00 None Skagen Fyr None None None 20000 None Active
680 Feature 0cee3919-f8c3-33d7-a10c-52b4fc686855 Point [10.1073, 57.5705] DMI DNK NaN None 2011-10-01T00:00:00Z [snow_cover_man, snow_depth_man] ... Manual snow 8.00 None Uggerby None None None 20030 None Active
681 Feature 3d2aa7f9-b617-5660-d7bf-8207f0287f4b Point [9.7608, 57.4185] DMI DNK NaN None 2011-10-01T00:00:00Z [snow_cover_man, snow_depth_man] ... Manual snow 22.00 None Nørre Lyngby N None None None 20055 None Active

265 rows × 23 columns

Make this Notebook Trusted to load map: File -> Trust Notebook

Part 1: Retrieving data#

Part 1 of this tutorial will show how to request data and convert it to a table format. Part 2 will deal with how to request specific data and more advanced data handling.

First, the necessary libraries have to be imported:

import requests # library for making HTTP requests
import pandas as pd # library for data analysis
import datetime as dt # library for handling date and time objects

In the following code block, data is retrieved using the requests.get function. Further information on REST APIs and HTTP request methods can be found here.

DMI_URL = 'https://dmigw.govcloud.dk/v2/metObs/collections/observation/items'
r = requests.get(DMI_URL, params={'api-key': api_key}) # Issues a HTTP GET request
print(r)
<Response [200]>

The response status code indicates whether the request was successful or not. A 200 code means that the retrieval was successful.

Next, we extract the JSON file containing the data from the returned request object. JSON is a human-readable format for data exchange.

json = r.json()  # Extract JSON data
print(json.keys())  # Print the keys of the JSON dictionary
dict_keys(['type', 'features', 'timeStamp', 'numberReturned', 'links'])

When inspecting the json object, it can be noticed that the measurement data is contained within the features:
json['features'][:2]
[{'type': 'Feature',
  'id': '5f4d5963-845b-c785-20cd-d47b7918780c',
  'geometry': {'type': 'Point', 'coordinates': [11.6035, 55.7358]},
  'properties': {'parameterId': 'leav_hum_dur_past10min',
   'created': '2025-08-22T07:11:30.91972Z',
   'value': 0.0,
   'observed': '2006-12-23T11:20:00Z',
   'stationId': '06156'}},
 {'type': 'Feature',
  'id': '5f860583-7d08-82ae-9c17-92dc7a4b02cd',
  'geometry': {'type': 'Point', 'coordinates': [9.2634, 55.2251]},
  'properties': {'parameterId': 'cloud_cover',
   'created': '2025-08-22T07:11:32.656979Z',
   'value': 90.0,
   'observed': '2006-12-23T11:20:00Z',
   'stationId': '06110'}}]

The JSON object can be converted to a convenient table (pandas DataFrame) using pd.json_normalize:

df = pd.json_normalize(json['features'])  # Convert JSON object to a Pandas DataFrame
df.head()  # Print the first five rows of the DataFrame
type id geometry.type geometry.coordinates properties.parameterId properties.created properties.value properties.observed properties.stationId
0 Feature 5f4d5963-845b-c785-20cd-d47b7918780c Point [11.6035, 55.7358] leav_hum_dur_past10min 2025-08-22T07:11:30.91972Z 0.0 2006-12-23T11:20:00Z 06156
1 Feature 5f860583-7d08-82ae-9c17-92dc7a4b02cd Point [9.2634, 55.2251] cloud_cover 2025-08-22T07:11:32.656979Z 90.0 2006-12-23T11:20:00Z 06110
2 Feature 5fdacaa7-51e2-a6ae-25cd-31abe4cba470 Point [9.9527, 57.1852] precip_dur_past10min 2025-08-22T07:11:30.760214Z 0.0 2006-12-23T11:20:00Z 06031
3 Feature 6018df07-1b4c-2534-b4be-78a5e5261ee8 Point [12.1366, 55.5867] precip_past10min 2025-08-22T07:11:30.548988Z 0.0 2006-12-23T11:20:00Z 06170
4 Feature 603fb8b2-dc96-057d-3238-87028dc88d6d Point [12.1841, 54.879] precip_past10min 2025-08-22T07:11:30.475993Z 0.0 2006-12-23T11:20:00Z 06147

The timestamps strings can be converted to a datetime object using the pandas to_datetime function.

df['time'] = pd.to_datetime(df['properties.observed'])
df['time'].head()  # Print the first five timestamps
0   2006-12-23 11:20:00+00:00
1   2006-12-23 11:20:00+00:00
2   2006-12-23 11:20:00+00:00
3   2006-12-23 11:20:00+00:00
4   2006-12-23 11:20:00+00:00
Name: time, dtype: datetime64[ns, UTC]

Last, we will generate a list of all the available parameters:
parameter_ids = df['properties.parameterId'].unique()  # Generate a list of unique parameter ids
print(parameter_ids)  # Print all unique parameter ids
['leav_hum_dur_past10min' 'cloud_cover' 'precip_dur_past10min'
 'precip_past10min' 'visibility' 'pressure_at_sea' 'humidity' 'wind_speed'
 'temp_grass' 'temp_dry' 'wind_max' 'wind_dir' 'sun_last10min_glob'
 'visib_mean_last10min' 'cloud_height' 'radia_glob' 'temp_soil' 'pressure'
 'weather' 'temp_dew' 'wind_gust_always_past1h' 'temp_min_past1h'
 'temp_max_past1h' 'temp_mean_past1h' 'wind_max_per10min_past1h'
 'precip_dur_past1h' 'wind_speed_past1h' 'temp_grass_max_past1h'
 'wind_dir_past1h' 'humidity_past1h' 'radia_glob_past1h'
 'temp_soil_min_past1h' 'temp_soil_mean_past1h' 'temp_grass_min_past1h'
 'temp_grass_mean_past1h' 'wind_min_past1h' 'temp_soil_max_past1h'
 'sun_last1h_glob' 'leav_hum_dur_past1h' 'precip_past1h' 'wind_min']



Part 2: Requesting specific data#

The above example was a heavily simplied example to illustrate how the API can be accessed. For most applications you probably want to specify query criterias, such as:

  1. Meterological stations (e.g. 04320, 06074, etc.)

  2. Parameters (e.g. wind_speed, humidity, etc.)

  3. Time frame (to and from time)

  4. Limit (maximum number of observations)

Click the “View to show” button below to see a list of a all stations and parameters.

Hide code cell content
all_stations = [
    '04203', '04208', '04214', '04220', '04228', '04242', '04250',
    '04253', '04266', '04271', '04272', '04285', '04301', '04312',
    '04313', '04320', '04330', '04339', '04351', '04360', '04373',
    '04382', '04390', '05005', '05009', '05015', '05031', '05035',
    '05042', '05065', '05070', '05075', '05081', '05085', '05089',
    '05095', '05105', '05109', '05135', '05140', '05150', '05160',
    '05165', '05169', '05185', '05199', '05202', '05205', '05220',
    '05225', '05269', '05272', '05276', '05277', '05290', '05296',
    '05300', '05305', '05320', '05329', '05343', '05345', '05350',
    '05355', '05365', '05375', '05381', '05395', '05400', '05406',
    '05408', '05435', '05440', '05450', '05455', '05469', '05499',
    '05505', '05510', '05529', '05537', '05545', '05575', '05735',
    '05880', '05889', '05935', '05945', '05970', '05986', '05994',
    '06019', '06031', '06032', '06041', '06049', '06051', '06052',
    '06056', '06058', '06065', '06068', '06072', '06073', '06074',
    '06079', '06081', '06082', '06088', '06093', '06096', '06102',
    '06116', '06119', '06123', '06124', '06126', '06132', '06135',
    '06136', '06138', '06141', '06147', '06149', '06151', '06154',
    '06156', '06159', '06168', '06169', '06174', '06181', '06183',
    '06184', '06186', '06187', '06188', '06193', '06197', '20000',
    '20030', '20055', '20085', '20228', '20279', '20315', '20375',
    '20400', '20552', '20561', '20600', '20670', '21020', '21080',
    '21100', '21120', '21160', '21208', '21368', '21430', '22020',
    '22080', '22162', '22189', '22232', '22410', '23100', '23133',
    '23160', '23327', '23360', '24043', '24102', '24142', '24171',
    '24380', '24430', '24490', '25045', '25161', '25270', '25339',
    '26210', '26340', '26358', '26450', '27008', '27082', '28032',
    '28110', '28240', '28280', '28385', '28552', '28590', '29020',
    '29194', '29243', '29330', '29440', '30075', '30187', '30215',
    '30414', '31040', '31185', '31199', '31259', '31350', '31400',
    '31509', '31570', '32110', '32175', '34270', '34320', '34339'
]

all_parameters = [
    # Cloud cover and height
    'cloud_cover', 'cloud_height',
    # Humdity
    'humidity', 'humidity_past1h',
    # Precipitation
    'precip_past10min', 'precip_past1h', 'precip_past24h',
    # Pressure
    'pressure', 'pressure_at_sea',
    # Radiation
    'radia_glob', 'radia_glob_past1h',
    # Temperature
    'temp_dew', 'temp_dry', 'temp_max_past12h', 'temp_max_past1h',
    'temp_mean_past1h', 'temp_min_past12h', 'temp_min_past1h',
    # Visibilty and weather
    'visib_mean_last10min', 'visibility', 'weather',
    # Wind speed and direction
    'wind_dir', 'wind_dir_past1h', 'wind_gust_always_past1h', 'wind_max',
    'wind_max_per10min_past1h', 'wind_min', 'wind_min_past1h',
    'wind_speed', 'wind_speed_past1h',
]

Due to poor design of the API, it is only possible to request one station or all stations, and similarly, it is only possible to request one parameter or all parameters. To be able to select a subset of stations or parameters it is therefore necessary to loop as shown below. This also avoids hitting the rather low maximum amount of data that can be transferred for each request. The implementation below is most suitable for downloading a few stations and a few parameters, and will incur a significant performance penalty if downloading data for all stations.

# Specify the desired start and end time
start_time = pd.Timestamp(2022, 1, 1)
end_time = pd.Timestamp(2022, 1, 15)

# Specify one or more station IDs or all_stations
stationIds = ['04250', '06188']
# Specify one or more parameter IDs or all_parameters
parameterIds = ['radia_glob', 'wind_speed']

# Derive datetime specifier string
datetime_str = start_time.tz_localize('UTC').isoformat() + '/' + end_time.tz_localize('UTC').isoformat()

dfs = []
for station in stationIds:
    for parameter in parameterIds:
        # Specify query parameters
        params = {
            'api-key' : api_key,
            'datetime' : datetime_str,
            'stationId' : station,
            'parameterId' : parameter,
            'limit' : '300000',  # max limit
        }

        # Submit GET request with url and parameters
        r = requests.get(DMI_URL, params=params)
        # Extract JSON object
        json = r.json() # Extract JSON object
        # Convert JSON object to a MultiIndex DataFrame and add to list
        dfi = pd.json_normalize(json['features'])
        if dfi.empty is False:
            dfi['time'] = pd.to_datetime(dfi['properties.observed'])
            # Drop other columns
            dfi = dfi[['time', 'properties.value', 'properties.stationId', 'properties.parameterId']]
            # Rename columns, e.g., 'properties.stationId' becomes 'stationId'
            dfi.columns = [c.replace('properties.', '') for c in dfi.columns]
            # Drop identical rows (considers both value and time stamp)
            dfi = dfi[~dfi.duplicated()]
            dfi = dfi.set_index(['parameterId', 'stationId', 'time'])
            dfi = dfi['value'].unstack(['stationId','parameterId'])
            dfs.append(dfi)

df = pd.concat(dfs, axis='columns').sort_index()
df.head()
stationId 04250 06188
parameterId radia_glob wind_speed radia_glob wind_speed
time
2022-01-01 00:00:00+00:00 0.0 3.6 0.0 4.9
2022-01-01 00:10:00+00:00 0.0 4.0 0.0 5.5
2022-01-01 00:20:00+00:00 0.0 3.8 0.0 4.8
2022-01-01 00:30:00+00:00 0.0 3.8 0.0 5.3
2022-01-01 00:40:00+00:00 0.0 3.8 0.0 5.9

If the request was succesfull, the dataframe df now contains the requested data. The dataframe is a MultiIndex dataframe and has two column levels (station and parameter). The index is the observation time.

MultiIndex dataframes are extremely convenient and versatile, though they do take some time getting used to. As an example, the below command demonstrates how to get the wind speed from the station 04250 for four days in December:

df.loc['2022-01-05':, ('04250', 'wind_speed')]
time
2022-01-05 00:00:00+00:00    9.2
2022-01-05 00:10:00+00:00    7.5
2022-01-05 00:20:00+00:00    6.1
2022-01-05 00:30:00+00:00    4.4
2022-01-05 00:40:00+00:00    4.4
                            ... 
2022-01-14 23:20:00+00:00    5.5
2022-01-14 23:30:00+00:00    4.7
2022-01-14 23:40:00+00:00    4.8
2022-01-14 23:50:00+00:00    5.0
2022-01-15 00:00:00+00:00    4.4
Freq: 10min, Name: (04250, wind_speed), Length: 1441, dtype: float64

The last step is to visualize the data. As an example, we’ll visualize the wind speed and global horizontal irradiance (GHI) for the station 04250.

station = '04250'
params = ['wind_speed', 'radia_glob']  # parameters to plot

# Generate plot of data
ax = df[station][params].plot(figsize=(8,5), legend=False, fontsize=12, rot=0, subplots=True)
ax[0].set_ylabel('Air temperature [$^\circ$C]', size=12)
ax[1].set_ylabel('Global horizontal\nirradiance [W/m$^2$]', size=12)
ax[1].set_xlabel('', size=12)
Text(0.5, 0, '')
../../_images/05687d30c18e9eb31fdd60fa3a8c06d808e407eb8629399d00c4893b064ad030.png