A gentle introduction to SDMX for reproducible data extraction from international organizations

Dec 7, 2025·
Roger Mario López Justiniano
Roger Mario López Justiniano
· 49 min read

This is a post I had been wanting to write for some time, and one I had promised in another similar post a few weeks ago.

When you start doing macroeconomic analysis —whether for research or for presentations— the usual workflow involves going to the data source (usually some international organization that aggregates information), downloading the dataset, and then importing it into your tool of choice to proceed with the analysis.

This workflow, however, has several drawbacks. First, it is not replicable: there are several manual steps that are not strictly documented. Moreover, it is not scalable, because if at some point a new variable becomes necessary for the analysis and was not included in the first data extraction, you need to repeat the whole process from the beginning.

Therefore, for the applied analyst, it becomes essential to automate the data extraction process, so that most of the work is focused on analysis and less on the plumbing required to gather the necessary information.

In this sense, the optimal solution is to rely on an API (application programming interface) that allows you to find, understand, and extract information quickly, so that the analysis is fully reproducible and scalable. For example, in previous posts I showed how, in a simplified way, to access the API of ECLAC and the World Economic Outlook published by the International Monetary Fund (IMF) to automate this workflow.

In this post, however, I want to broaden the data sources and present, at least conceptually, a general method that allows systematic access to data from international organizations. This is possible thanks to a standardized information model or protocol called SDMX, which allows us to access data in a more or less homogeneous way.

In what follows, I will review the SDMX model conceptually, show a relatively simplified workflow to extract information, and present some applied examples. Finally, for the IMF case, a library called imfapi has been created, which facilitates data extraction using the IMF’s SDMX API, and which I explore below.

What is SDMX?

As stated on its official website:

SDMX, which stands for Statistical Data and Metadata eXchange, is an ISO standard designed to describe statistical data and metadata, standardize their exchange, and improve their efficient distribution among statistical and similar organizations.

SDMX is sponsored by eight international organizations:

  1. Bank for International Settlements (BIS),
  2. European Central Bank (ECB),
  3. Statistical Office of the European Union (Eurostat),
  4. International Labour Organization (ILO),
  5. International Monetary Fund (IMF),
  6. Organisation for Economic Co-operation and Development (OECD),
  7. United Nations Statistics Division (UNSD), and
  8. World Bank (WB)

Each of these organizations has implemented SDMX in their statistical information systems and provides APIs following this structure. This can be seen in the table below:

ORGSDMX APIDocumentation
BIShttps://stats.bis.org/api/v1BIS SDMX Tech Spec
ECBhttps://data-api.ecb.europa.eu/serviceECB API Overview
Eurostathttps://ec.europa.eu/eurostat/api/dissemination/Eurostat API
ILOhttps://sdmx.ilo.org/restILOSTAT SDMX User Guide
IMFhttps://sdmxcentral.imf.org/sdmx/v2/IMF Data APIs
OECDhttps://sdmx.oecd.org/public/rest/OECD Data API Explainer
UNSDhttp://data.un.org/WS/rest/UN SDMX / SDG API Manual
WBhttps://api.worldbank.org/v2/sdmx/rest/World Bank SDMX API Queries

Thus, analysts can access a wide range of macroeconomic and social data from different international sources using a standardized approach.

General workflow to extract SDMX data

Although each API may have its particularities and the SDMX model is quite broad, the general workflow for extracting data using SDMX APIs can be summarized in four steps. Each one corresponds to a key concept within the SDMX architecture and helps us understand how statistical agencies organize and expose their data.

  1. Identifying the dataflow. The first step consists in identifying the dataflow. Conceptually, a dataflow is the container for a thematic dataset. It works like the “database” or the statistical domain published by an institution (for example, monetary statistics, bank balance sheets, exchange rates, or macroeconomic indicators). Identifying the appropriate dataflow involves reviewing the list of dataflows available in the API and selecting the one that contains the information relevant for the desired analysis. Without this step, it is impossible to proceed to a valid query, because every SDMX dataset starts from a specific dataflow.

  2. Retrieving the data structure. Once the dataflow has been selected, the next step is to retrieve the associated data structure, known as the Data Structure Definition (DSD). Conceptually, the DSD is the logical schema of the dataset: it describes how observations are organized and which elements are involved in their identification. The DSD specifies the dimensions (such as country, indicator, frequency, or currency), which are key variables that classify the data and determine how they are combined to form series or observations. It also defines the main measure, which is the numerical value of the data (for example, a price index or a monetary balance), as well as additional attributes that provide contextual information (such as unit, seasonal adjustment method, or observation status). Understanding this structure is essential to know what filters can be applied and how to build the query.

  3. Consulting the codelists. The third step consists in consulting the codelists. Each dimension defined in the DSD has an associated codelist, which is the authorized set of possible values for that dimension. Codelists act like a standard dictionary of codes that ensures interoperability between institutions and systems. For example, a codelist may contain all country codes, the types of time frequency (annual, quarterly, monthly), or the different indicators within a statistical domain. Consulting them is fundamental to know which values can be used when filtering a query and how they should be correctly encoded in the URL or request body. As we will see later, many times codelists show what is possible but not necessarily what is valid for a particular dataflow. Therefore, it is often necessary to investigate whether the DSD contains constraints on values and, if not, to resort to brute force by downloading a sample of the data and extracting unique values. In any case, the important thing is that the user becomes familiar with the data.

  4. Extracting the data. Finally, the fourth step is to extract the data. Once we know the dataflow, its structure, and the codes allowed for each dimension, we can build a valid query to the SDMX API. This involves selecting the relevant combinations of codes (for example, country, indicator, and frequency) and applying time or detail filters as permitted by the API. The response is usually received in a standardized format, such as SDMX-JSON or SDMX-XML, which can later be processed in tools like R or Python for analysis, visualization, or integration into a reproducible pipeline. This step is where all the previous work materializes, as it turns the conceptual understanding of the SDMX structure into a concrete data request.

Some examples

In this section we will work through an example using the IMF APIs and, later, the European Central Bank (ECB) APIs. Although we go step by step, the key idea is that what we are going to do is:

  1. Identify the available dataflows (“databases”)
  2. Retrieve the data structure of the dataflow of interest (“the variables”)
  3. Consult the codelists associated with the dimensions of interest (“the valid codes”)
  4. Extract the data (“the final query”)

SDMX at the IMF

The IMF provides a fairly complete collection of SDMX 3.0 APIs that give access to a wide range of macroeconomic data. These can be found, after registering, here:

IMF Data API

The IMF’s SDMX APIs—and in fact any SDMX 3.0 API—are designed following a hierarchical structure, where the URL acts like a path that moves from the most general to the most specific. This allows the user to progressively explore the statistical system, starting from complete catalogs and drilling down to a specific dataset or even a single time series.

The core idea is that each segment of the path adds a restriction, a filter, or a level of precision. Thus, a request can mean “show everything” or “give me exactly this data structure”, depending on how many parameters are included. Let’s go through this step by step.

Available data: the dataflow

The starting point is the base URL, which in this case is https://api.imf.org/external/sdmx/3.0. From this point we can say that the IMF API is divided into two big families: a structure family and a data family. The former allows you to explore catalogs and structures, while the latter allows you to extract specific data.

For example, if we append /structure/dataflow to the base URL, we get the complete catalog of dataflows available in the API—that is, we are asking “give me all the dataflows the IMF has”. If, on the other hand, we append /structure/datastructure, we are essentially asking “give me all the DSDs the IMF has”. This level is quite general: it does not apply any filters to the extraction, which is useful when we are not yet sure what we need.

To illustrate this, we will carry out the first step of the workflow: obtaining all the dataflows available in the IMF API and locating the dataflow corresponding to the World Economic Outlook (WEO). This can be done with the following R code:

# 1. Base URL SDMX 3.0 of the IMF
base_url <- "https://api.imf.org/external/sdmx/3.0"

# 2. Get all dataflows
flows_resp <- httr2::request(paste0(base_url, "/structure/dataflow")) |>
  httr2::req_perform()

flows_json <- flows_resp |>
  httr2::resp_body_string() |>
  jsonlite::fromJSON(flatten = TRUE)

flows <- flows_json$data$dataflows

# Dataflows table as a tibble
df_flows <- tibble::tibble(
  id      = flows$id,
  name    = flows$names.en,
  version = flows$version,
  agency  = flows$agencyID,
  dsd_urn = flows$structure
)

df_flows |> 
  dplyr::select(id, name, version, agency) |>
  dplyr::arrange(id) |> 
  kableExtra::kable() 
idnameversionagency
AFRREOSub-Saharan Africa Regional Economic Outlook (AFRREO)6.0.1IMF.AFR
ANEANational Economic Accounts (NEA), Annual Data6.0.1IMF.STA
APDREOAsia and Pacific Regional Economic Outlook (APDREO)6.0.0IMF.APD
BOPBalance of Payments (BOP)21.0.0IMF.STA
BOP_AGGBalance of Payments and International Investment Position Statistics (BOP/IIP), World and Country Group Aggregates9.0.1IMF.STA
COFERCurrency Composition of Official Foreign Exchange Reserves (COFER)7.0.0IMF.STA
CPIConsumer Price Index (CPI)5.0.0IMF.STA
CPI_WCAConsumer Price Index (CPI), World and Country Aggregates (CPI_WCA)2.0.2IMF.STA
CTOTCommodity Terms of Trade (CTOT)5.0.1IMF.RES
DIPDirect Investment Positions by Counterpart Economy (formerly CDIS)12.0.0IMF.STA
EDExport Diversification (ED)1.0.0IMF.RES
EEREffective Exchange Rate (EER)6.0.0IMF.STA
EQExport Quality (EQ)2.0.0IMF.RES
ERExchange Rates (ER)4.0.1IMF.STA
FAFund Accounts (FA)8.0.0IMF.STA
FASFinancial Access Survey (FAS)4.0.0IMF.STA
FDFiscal Decentralization (FD)6.0.0IMF.STA
FDIFinancial Development Index (FDI)1.0.0IMF.MCM
FMFiscal Monitor (FM)5.0.0IMF.FAD
FSIBSISFinancial Soundness Indicators (FSI), Balance Sheet, Income Statement and Memorandum Series18.0.0IMF.STA
FSICFinancial Soundness Indicators (FSI), Core and Additional Indicators13.0.1IMF.STA
FSICDMFinancial Soundness Indicators (FSI), Concentration and Distribution Measures7.0.0IMF.STA
GDDGlobal Debt Database (GDD)2.0.0IMF.FAD
GFS_BSGFS Balance Sheet12.0.0IMF.STA
GFS_COFOGGFS Government Expenditures by Function11.0.0IMF.STA
GFS_SFCPGFS Stocks and Flows by Counterparty10.0.0IMF.STA
GFS_SOEFGFS Statement of Other Economic Flows11.0.0IMF.STA
GFS_SOOGFS Statement of Operations12.0.0IMF.STA
GFS_SSUCGFS Statement of Sources and Uses of Cash10.0.0IMF.STA
HPDHistorical Public Debt (HPD)1.0.0IMF.FAD
ICSDInvestment and Capital Stock Dataset (ICSD)1.0.0IMF.FAD
IIPInternational Investment Position (IIP)13.0.0IMF.STA
IIPCCCurrency Composition of the International Investment Position (IIPCC)13.0.0IMF.STA
ILInternational Liquidity (IL)13.0.1IMF.STA
IMTSInternational Trade in Goods (by partner country) (IMTS)1.0.0IMF.STA
IRFCLInternational Reserves and Foreign Currency Liquidity (IRFCL)11.0.0IMF.STA
ISORA_2016_DATA_PUBISORA 2016 Data2.0.0ISORA
ISORA_2018_DATA_PUBISORA 2018 Data2.0.0ISORA
ISORA_LATEST_DATA_PUBISORA Latest Data4.0.0ISORA
ITGInternational Trade in Goods (ITG)4.0.0IMF.STA
ITG_WCAInternational Trade in Goods, World and Country Aggregates2.0.4IMF.STA
ITSInternational Trade in Services (ITS)3.0.1IMF.RES
LSLabor Statistics (LS)9.0.0IMF.STA
MCDREOMiddle East and Central Asia Regional Economic Outlook (MCDREO) 8.0.0IMF.MCD
MFS_CBSMonetary and Financial Statistics (MFS), Central Bank Data24.0.0IMF.STA
MFS_DCMonetary and Financial Statistics (MFS), Depository Corporations8.0.0IMF.STA
MFS_FCMonetary and Financial Statistics (MFS), Financial Corporations9.0.0IMF.STA
MFS_FMPMonetary and Financial Statistics (MFS): Financial Market Prices3.0.0IMF.STA
MFS_IRMonetary and Financial Statistics (MFS), Interest Rate8.0.1IMF.STA
MFS_MAMonetary and Financial Statistics (MFS), Monetary Aggregates10.0.1IMF.STA
MFS_NSRFMonetary and Financial Statistics (MFS), Non-Standard Data1.0.3IMF.STA
MFS_ODCMonetary and Financial Statistics (MFS), Other Depository Corporations10.0.0IMF.STA
MFS_OFCMonetary and Financial Statistics (MFS), Other Financial Corporations7.0.0IMF.STA
MPFTMonetary Policy Frameworks Toolkit (MPFT)7.0.1IMF.RES
NSDPNational Summary Data Page (NSDP)7.0.0IMF.STA
PCPSPrimary Commodity Price System (PCPS)9.0.0IMF.RES
PIProduction Indexes (PI)2.0.0IMF.STA
PIPPortfolio Investment Positions by Counterpart Economy (formerly CPIS)4.0.0IMF.STA
PI_WCAProduction Indexes, World and Country Group Aggregates1.0.0IMF.STA
PPIProducer Price Index (PPI)3.0.0IMF.STA
PSBSPublic Sector Balance Sheet (PSBS)2.0.0IMF.FAD
QGDP_WCAQuarterly Gross Domestic Product (GDP), World and Country Aggregates3.0.0IMF.STA
QGFSQuarterly Government Finance Statistics (QGFS)12.0.0IMF.STA
QNEANational Economic Accounts (NEA), Quarterly Data7.0.0IMF.STA
SDGIMF Reported SDG Data2.0.1IMF.STA
SPESpecial Purpose Entities (SPEs)13.0.0IMF.STA
SRDStructural Reform Database (SRD)1.0.0IMF.RES
TAXFITTax and Benefits Analysis Tool (TAXFIT)2.0.3IMF.RES
TEGTrade in Low Carbon Technology Goods (TEG)3.0.2IMF.STA
WEOWorld Economic Outlook (WEO)9.0.0IMF.RES
WHDREOWestern Hemisphere Regional Economic Outlook (WHDREO)5.0.0IMF.WHD
WPCPERCrypto-based Parallel Exchange Rates (Working Paper dataset WP-CPER)6.0.0IMF.STA

In the table above we can see all the dataflows available in the IMF API. Each dataflow has a unique identifier (id), a descriptive name (name), a version (version), a responsible agency (agency), and a URN (Uniform Resource Name) not shown in the previous table but pointing to its associated data structure (dsd_urn).

In our case we are interested in the WEO dataflow. We can filter the dataflow of interest as follows:

# Keep only the WEO dataflow
weo <- df_flows |>
  dplyr::filter(id == "WEO")

weo |> 
  kableExtra::kable()
idnameversionagencydsd_urn
WEOWorld Economic Outlook (WEO)9.0.0IMF.RESurn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.RES:DSD_WEO(9.0+.0)

Data structure: the dimensions

Now that we have the dataflow of interest, the next step is to understand which variables (dimensions) are available in its data structure. To do this, the API we need to query has the following form: /structure/datastructure/{agency}/{dsd_id}/{version}, where {agency}, {dsd_id}, and {version} are the components we must extract from the WEO dataflow URN. Note that if we do not specify these three fields, we run the risk of getting outdated information.

How do we obtain this information? The good thing is that, when we query the dataflow, this information is contained in the associated URN:

# Parse the URN of WEO's datastructure
urn <- weo$dsd_urn
urn
## [1] "urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.RES:DSD_WEO(9.0+.0)"

Note that here we have the three essential pieces:

  1. The agency responsible for the DSD: IMF.RES
  2. The DSD identifier: DSD_WEO
  3. The DSD version: 9.0+.0

To systematically extract these pieces we can use regular expressions in R as follows:

m     <- base::regexec("DataStructure=([^:]+):([^()]+)\\(([^)]+)\\)", urn)
parts <- base::regmatches(urn, m)[[1]]

agency_dsd  <- parts[2]   # e.g. "IMF.RES"
dsd_id      <- parts[3]   # e.g. "DSD_WEO"
version_dsd <- parts[4]   # e.g. "9.0+.0"

# Download WEO's datastructure and inspect dimensions
dsd_url <- paste0(
  base_url,
  "/structure/datastructure/",
  agency_dsd, "/", dsd_id, "/", version_dsd
)
dsd_url
## [1] "https://api.imf.org/external/sdmx/3.0/structure/datastructure/IMF.RES/DSD_WEO/9.0+.0"

Now that we have the URL for WEO’s datastructure, we can download it and inspect the available dimensions:

dsd_resp <- httr2::request(dsd_url) |>
  httr2::req_perform()

dsd_json <- dsd_resp |>
  httr2::resp_body_string() |>
  jsonlite::fromJSON(flatten = TRUE)

# "Regular" dimensions: COUNTRY, INDICATOR, FREQUENCY
dims_main <- dsd_json$data$dataStructures$dataStructureComponents.dimensionList.dimensions[[1]] |>
  tibble::as_tibble() |>
  dplyr::select(position, id, type)

# Time dimension: TIME_PERIOD
time_dim <- dsd_json$data$dataStructures$dataStructureComponents.dimensionList.timeDimensions[[1]] |>
  tibble::as_tibble() |>
  dplyr::select(position, id, type)

# Full dimension order
dims_clean <- dplyr::bind_rows(dims_main, time_dim) |>
  dplyr::arrange(position)

dims_clean |> 
  kableExtra::kable()
positionidtype
0COUNTRYDimension
1INDICATORDimension
2FREQUENCYDimension
3TIME_PERIODTimeDimension

In the table above we can see the available dimensions in the WEO dataflow (COUNTRY, INDICATOR, FREQUENCY, and TIME_PERIOD). These dimensions are key for understanding how the data are organized and what filters we can apply when extracting specific information.

Note also that, although the query ran without errors, we first explored dsd_json to understand where and how the information was stored.

Codelists: the valid codes

As a third step, and before extracting data, it is important to know the valid codes for each dimension of the dataset. This is essential because, when building the final query, we must make sure to use the exact values that the API recognizes. Otherwise, it will return an error. For example, if we want to obtain real GDP growth for a specific country, we need to know how the IMF codes the country, the indicator, and the frequency. It is not enough to write “Bolivia” or “Real GDP growth”: we must use the official codes defined by the dataset’s structure.

Each of the WEO dimensions—COUNTRY, INDICATOR, FREQUENCY, and TIME_PERIOD—has an authorized set of possible values: the codelist. For example, country codes might be BOL, ARG, or USA; indicators could be NGDP_RPCH, LUR, and so on; and valid frequencies are typically A (annual), Q (quarterly), etc. An important detail is that each dataset can have different codelists, even for conceptually similar items. The IMF might use one list of countries for WEO, another for BOP (Balance of Payments), and another for monetary statistics. This means that codes that work for one dataflow will not necessarily work for another.

Therefore, if we want to query WEO data, we cannot make up codes or rely on old lists: we need to query the official codelists from the IMF. The only safe way to do this is to start from the dataflow, retrieve its structure (the DSD), and from there obtain the correct lists of codes for each dimension. This procedure ensures that the query is reproducible, accurate, and fully compatible with the IMF’s internal definitions.

To obtain the codelists associated with the WEO dataflow, we use a specific URL from the IMF’s SDMX API. The general pattern is: /structure/dataflow/{agency}/{dataflow_id}/+, where {agency} and {dataflow_id} come from the dataflow itself (IMF.RES and WEO, respectively, in our case). The + symbol indicates that we want the latest available version. But the most important part is the parameters we add at the end: ?detail=full&references=descendants1.

For our case, the full URL to obtain WEO’s codelists is:

weo_struct_url <- paste0(
  base_url,
  "/structure/dataflow/",
  weo$agency, "/", weo$id, "/+",
  "?detail=full&references=descendants"
)
weo_struct_url
## [1] "https://api.imf.org/external/sdmx/3.0/structure/dataflow/IMF.RES/WEO/+?detail=full&references=descendants"

When we send a request to this URL, we get a response that includes all the codelists associated with the WEO dataflow. We can process this response to extract the specific codelists we need for each dimension:

weo_struct_resp <- httr2::request(weo_struct_url) |>
  httr2::req_perform()

weo_struct_json <- weo_struct_resp |>
  httr2::resp_body_string() |>
  jsonlite::fromJSON(flatten = TRUE)

# Codelists table
codelists_tbl <- weo_struct_json$data$codelists

codelists_tbl |> 
  dplyr::select(id, name, version, agencyID) |> 
  head() |> 
  kableExtra::kable()
idnameversionagencyID
CL_SEC_CLASSIFICATIONSecurity Classification1.0.1IMF
CL_METHODOLOGYMethodology2.6.0IMF
CL_DEPARTMENTDepartment1.0.2IMF
CL_COUNTRYCountry1.5.1IMF
CL_TOPICTopic2.2.0IMF
CL_UNIT_MULTUnit Multiplier1.0.2IMF

Note that in the table above we have extracted quite a bit of information about the codelists available for the WEO dataflow. In the following code, we summarize what we obtained:

# Summary view of the codelists
weo_codelists_overview <- tibble::tibble(
  cl_id   = codelists_tbl$id,
  cl_name = ifelse(is.na(codelists_tbl$names.en), 
                   yes = codelists_tbl$name,
                   no = codelists_tbl$names.en
                   ),
  n_codes = sapply(codelists_tbl$codes, nrow)
)

weo_codelists_overview |> 
  dplyr::arrange(desc(cl_id)) |> 
  kableExtra::kable()
cl_idcl_namen_codes
CL_WEO_INDICATORWorld Economic Outlook (WEO) Indicator145
CL_WEO_COUNTRYWorld Economic Outlook (WEO) Country338
CL_VALUATIONValuation26
CL_UNIT_MULTUnit Multiplier31
CL_UNITUnit of Measure270
CL_TRANSFORMATIONTransformation487
CL_TRADE_FLOWTrade flow50
CL_TOPICTopic118
CL_S_ADJUSTMENTSeasonal Adjustment17
CL_STATISTICAL_MEASURESStatistical Measures23
CL_SOC_CONCEPTSSocial Concepts31
CL_SEXSex9
CL_SEC_CLASSIFICATIONSecurity Classification6
CL_SECTORInstitutional Sector288
CL_REPORTING_PERIOD_TYPEREPORTING_PERIOD_TYPE6
CL_PRICESPrices15
CL_OVERLAPIMF Data Overlap1
CL_ORGANIZATIONOrganization1588
CL_OBS_STATUSObservation Status22
CL_NA_STONA Stocks, Transactions, Other Flows636
CL_MFS_INSTRMonetary and Financial Instruments18
CL_METHODOLOGYMethodology69
CL_LANGUAGELanguage93
CL_INT_TTCInterest Rate types, terms and conditions101
CL_INT_ACC_ITEMInternational accounts item507
CL_INSTR_ASSETInstrument and assets classification342
CL_INDEX_TYPEIndex type49
CL_GFS_STOGFS Stocks, Transactions, Other Flows389
CL_GENDERGender15
CL_FUNCTIONAL_CATFunctional category72
CL_FSENTRYFlow or stock entry43
CL_FREQFrequency6
CL_FI_MATURITYMaturity of Financial Instrument32
CL_EXRATEExchange Rate37
CL_DERIVATION_TYPEDerivation Type12
CL_DEPARTMENTDepartment34
CL_DECIMALSDecimals16
CL_CURRENCYCurrency177
CL_COUNTRYCountry337
CL_CONF_STATUSConfidentiality Status12
CL_COMMODITYCommodity135
CL_COICOP_2018COICOP 201816
CL_COICOP_1999COICOP 199915
CL_COFOGCOFOG190
CL_CIVIL_STATUSCivil (or Marital) Status8
CL_ACCOUNTSMacroeconomic and financial accounts40
CL_ACCOUNTING_ENTRYAccounting Entry82
CL_ACCESS_SHARING_LEVELAccess and Sharing Level8

In the table above we can see a summary of the codelists available for the WEO dataflow. Each codelist has an identifier (cl_id), a descriptive name (cl_name), and the number of codes it contains (n_codes). In our case, we are particularly interested in the codelists associated with the COUNTRY, INDICATOR, and FREQUENCY dimensions: CL_WEO_COUNTRY, CL_WEO_INDICATOR, and CL_FREQUENCY.

For example, for CL_WEO_COUNTRY:

weo_country_codes <- codelists_tbl$codes[[which(codelists_tbl$id == "CL_WEO_COUNTRY")]] |>
  tibble::as_tibble() |>
  dplyr::transmute(
    code    = id,
    name_en = names.en  # in CL_WEO_COUNTRY it appears as names.en1
  )
weo_country_codes |>
  head(20) |> 
  kableExtra::kable()
codename_en
GX123Other Advanced Economies (Advanced Economies excluding G7 and Euro Area countries)
AFGAfghanistan, Islamic Republic of
ALBAlbania
DZAAlgeria
ASMAmerican Samoa
ANDAndorra, Principality of
AGOAngola
AIAAnguilla, United Kingdom-British Overseas Territory
ATGAntigua and Barbuda
ARGArgentina
ARMArmenia, Republic of
ABWAruba, Kingdom of the Netherlands
AUSAustralia
AUTAustria
AZEAzerbaijan, Republic of
BHSBahamas, The
BHRBahrain, Kingdom of
BGDBangladesh
BRBBarbados
BLRBelarus, Republic of

In the table above we can see the valid country codes for the WEO dataflow. Each country has a code (code) and an English name (name_en). These codes are essential for building precise queries to the IMF API.

Next, we look for the available INDICATOR codes in WEO:

weo_indicator_codes <- codelists_tbl$codes[[which(codelists_tbl$id == "CL_WEO_INDICATOR")]] |>
  tibble::as_tibble() |>
  dplyr::transmute(
    code    = id,
    name_en = names.en
  )

weo_indicator_codes |>
  kableExtra::kable()
codename_en
LURUnemployment rate
PCOALSACoal, South Africa, Export price, US dollars per metric tonne
DSPExternal debt: total debt service, amortization, US dollar
DSIExternal debt: total debt service, interest, US dollar
DSP_NGDPDExternal debt: total debt service, amortization, Percent of GDP
BXExports of goods and services, US dollar
BMImports of goods and services, US dollar
DSExternal debt: total debt service, US dollar
TTPCHTerms of trade of goods and services, percent change
DSI_NGDPDExternal debt: total debt service, interest, Percent of GDP
NGDP_RPCHMKGross domestic product (GDP), Constant prices, Percent
NGDPDGross domestic product (GDP), Current prices, US dollar
PTEATea, Kenyan, Unit prices, US cents per kilogram
DS_NGDPDExternal debt: total debt service, Percent of GDP
DExternal debt, US dollar
NGDP_DGross domestic product (GDP), Price deflator, Index
NGDP_RPCHGross domestic product (GDP), Constant prices, Percent change
TTTPCHTerms of trade of goods, percent change
NGDP_RGross domestic product (GDP), Constant prices, Domestic currency
NGDPGross domestic product (GDP), Current prices, Domestic currency
NGSD_NGDPGross national savings, Percent of GDP
PSUNOSunflower oil, Export price, US dollars per metric tonne
PNGASEUNatural gas, EU, Unit prices, US dollars per million metric British thermal units of gas
PZINCZinc, Unit prices, US dollars per metric tonne
PPPSHGross domestic product (GDP), Purchasing power parity (PPP) international dollar, percent of world GDP, Percent, ICP benchmarks 2017-2021
D_NGDPDExternal debt, Percent of GDP
PPPPCGross domestic product (GDP), Per capita, purchasing power parity (PPP) international dollar, ICP benchmarks 2017-2021
PPPGDPGross domestic product (GDP), Current prices, Purchasing power parity (PPP) international dollar, ICP benchmarks 2017-2021
DSP_BXExternal debt: total debt service, amortization, Percent of exports of goods and services
PPORKSwine, Unit prices, US cents per pound
PSAWMALHard sawnwood, Dark Red Meranti, Unit prices, US dollars per cubic meter
PORANGOrange, Import price, US dollars per metric tonne
PTINTin, Unit prices, US dollars per metric tonne
PPOULTPoultry, Unit prices, US cents per pound
DSI_BXExternal debt: total debt service, interest, Percent of exports of goods and services
NGDPDPCGross domestic product (GDP), Current prices, Per capita, US dollar
PSAWORESoft sawnwood, Export price, US dollars per cubic meter
PROILRapeseed oil, Unit prices, US dollars per metric tonne
PLAMBLamb, Unit prices, US cents per pound
PBARLBarley, Unit prices, US dollars per metric tonne
PLOGORESoft logs, Export price, US dollars per cubic meter
GGR_NGDPRevenue, General government, Percent of GDP
TRADEPCHTrade of goods and services, Volume, Percent change
PALUMAluminum, Unit prices, US dollars per metric tonne
PLOGSKHard logs, import price Japan, Import price, US dollars per cubic meter
PWOOLCWool, coarse, Unit prices, US cents per kilogram
GGXWDN_NGDPNet debt, General government, Percent of GDP
PFISHFishmeal, Unit prices, US dollars per metric tonne
NGDPRPCGross domestic product (GDP), Constant prices, Per capita, Domestic currency
PNGASJPLNG, Asia, Unit prices, US dollars per million metric British thermal units of gas
LEEmployed persons, Persons for countries / Index for country groups
NGDPPCGross domestic product (GDP), Current prices, Per capita, Domestic currency
GGRRevenue, General government, Domestic currency
PWOOLFWool, fine, Unit prices, US cents per kilogram
PCOFFOTMCoffee, other mild Arabica, Unit prices, US cents per pound
PNICKNickel, Unit prices, US dollars per metric tonne
BCACurrent account balance (credit less debit), US dollar
PBANSOPBananas, Unit prices, US dollars per metric tonne
PRICENPQRice, Thailand, Unit prices, US dollars per metric tonne
DS_BXExternal debt: total debt service, Percent of exports of goods and services
PCOFFROBCoffee, Robustas, Unit prices, US cents per pound
PSALMFish, Export price, US dollars per kilogram
PSUGAUSASugar, No. 16, US, Import price, US cents per pound
GGXWDG_NGDPGross debt, General government, Percent of GDP
PCOPPCopper, Unit prices, US dollars per metric tonne
PLEADLead, Unit prices, US dollars per metric tonne
PBEEFBeef, Import price, US cents per pound
BFFinancial account balance (assets less liabilities), US dollar
PURANUranium, Unit prices, US dollars per pound
BCA_NGDPDCurrent account balance (credit less debit), Percent of GDP
PHIDEHides, Unit prices, US cents per pound
PGNUTSGroundnuts, Unit prices, US dollars per metric tonne
GGSBStructural balance, General government, Domestic currency
BFPPortfolio investment, Net (assets minus liabilities), US dollar
GGX_NGDPExpenditure, General government, Percent of GDP
GGXWDGGross debt, General government, Domestic currency
GGXWDNNet debt, General government, Domestic currency
NGDP_FYGross domestic product (GDP), Current prices, Fiscal year, Domestic currency
GGXExpenditure, General government, Domestic currency
PRUBBRubber, Unit prices, US cents per pound
PWHEAMTWheat, Unit prices, US dollars per metric tonne
PMAIZMTCorn, Unit prices, US dollars per metric tonne
GGSB_NPGDPStructural balance, General government, Percent
POLVOILOlive oil, Unit prices, US dollars per metric tonne
PIORECRIron ore, Unit prices, US dollars per metric tonne
D_BXExternal debt, Percent of exports of goods and services
BFDDirect investment, Net (assets minus liabilities), US dollar
TX_RPCHExports of goods and services, Volume, Free on board (FOB), Percent change
PCOCOCocoa, Unit prices, US dollars per metric tonne
BFOOther investment, Net (assets minus liabilities), US dollar
PPOILPalm oil, Unit prices, US dollars per metric tonne
NGDPRPPPPCGross domestic product (GDP), Constant prices, Per capita, purchasing power parity (PPP) international dollar, ICP benchmark 2021
PRAWMWAgricultural raw materials, Commodity price index
PSMEASoybean meal, Unit prices, US dollars per metric tonne
PCOTTINDCotton, Unit prices, US cents per pound
PCOALAUCoal, Australia, Unit prices, US dollars per metric tonne
PNGASUSNatural Gas, US Henry Hub Gas, Unit prices, US dollars per metric tonne
TXGM_DExports of manufactures, Price deflator, Free on board (FOB), Index
TM_RPCHImports of goods and services, Volume, Cost insurance freight (CIF), Percent change
PSOILSoybeans oil, Unit prices, US dollars per metric tonne
PSUGAISASugar, No. 11, World, Unit prices, US cents per pound
BFFFinancial derivatives and employee stock options, Net (assets minus liabilities), US dollar
NGAP_NPGDPOutput gap, Percent of potential GDP
PSOYBSoybeans, Unit prices, US dollars per metric tonne
PINDUWIndustrial materials, Commodity price index
GGXCNLNet lending (+) / net borrowing (-), General government, Domestic currency
BFRAChange in reserve assets, Net (assets minus liabilities), US dollar
TXGM_DPCHExports of manufactures, Price deflator, Free on board (FOB), Percent change
POILBREBrent crude, Unit prices, US dollars per barrel
PSHRIShrimp, Unit prices, US dollars per kilogram
GGXCNL_NGDPNet lending (+) / net borrowing (-), General government, Percent of GDP
PCOALWCoal, Commodity price index
PBEVEWBeverages, Commodity price index
PWOOLWWool, Commodity price index
POILDUBDubai crude, Unit prices, US dollars per barrel
POILWTIWTI crude, Unit prices, US dollars per barrel
PCOFFWCoffee, Commodity price index
PCPIAll Items, Consumer price index (CPI), Period average
PSEAFWSeafood, Commodity price index
PTIMBWTimber, Commodity price index
GGXONLB_NGDPPrimary net lending (+) / net borrowing (-), General government, Percent of GDP
PSUGAWSugar, Commodity price index
POILAPSPAPSP crude oil, Unit prices, US dollars per barrel
PCEREWCereal, Commodity price index
PALLFNFWAll commodities, Commodity price index
PCPIEAll Items, Consumer price index (CPI), End-of-period (EoP)
GGXONLBPrimary net lending (+) / net borrowing (-), General government, Domestic currency
TXG_RPCHExports of goods, Volume, Free on board (FOB), Percent change
TMG_RPCHImports of goods, Volume, Cost insurance freight (CIF), Percent change
PSOFTWSoftwood, Commodity price index
PHARDWHardwood, Commodity price index
PCPIPCHAll Items, Consumer price index (CPI), Period average, percent change
PMEATWMeat, Commodity price index
PFANDBWFood and beverage, Commodity price index
PCPIEPCHAll Items, Consumer price index (CPI), End-of-period (EoP), percent change
PNFUELWNon-fuel, Commodity price index
PNRGWEnergy, Commodity price index
LPPopulation, Persons for countries / Index for country groups
PNGASWNatural gas, Commodity price index
PPPEXRate, Domestic currency per international dollar in PPP terms, ICP benchmarks 2017-2021
PFOODWFood, Commodity price index
PMETAWMetal, Commodity price index
POILAPSPWAPSP crude oil, Commodity price index
NID_NGDPGross capital formation, Percent of GDP
PVOILWVegetable oil, Commodity price index

Finally, for the frequency:

weo_frequency_codes <- codelists_tbl$codes[[which(codelists_tbl$id =="CL_FREQ")]] |>
  tibble::as_tibble() |>
  dplyr::transmute(
    code    = id,
    name_en = names.en
  )
weo_frequency_codes |>
  kableExtra::kable()
codename_en
AAnnual
DDaily
MMonthly
QQuarterly
SHalf-yearly, semester
WWeekly

Once we have the valid codes for the dimensions of interest, we are ready to proceed to the extraction of specific data from the WEO dataflow. This is the final step of the SDMX workflow and will allow us to obtain the time series needed for our macroeconomic analysis.

Extracting data: the final query

With valid codes in hand for the COUNTRY, INDICATOR, and FREQUENCY dimensions, we are now in a position to build the final query to extract specific data from the WEO dataflow. From the IMF API’s perspective, the general data endpoint has the form:

/data/{context}/{agencyID}/{resourceID}/{version}/{key}[?c]

In our case, {context} will be dataflow, {agencyID} will be IMF.RES, {resourceID} will be WEO, and {version} will be the latest stable version (+). That is, everything before {key} is already known from the previous steps. What we still need to define is the key and, optionally, the filter parameter c.

The key parameter is the central piece of the query: it represents the combination of dimension values that identifies a series or a subset of the “cube” of data. The API defines it as “the combination of dimension values identifying series or slices of the cube”. In practice, this translates into a string where dimension values are concatenated following the order defined in the DSD.

In the case of WEO, we saw that the order is COUNTRY.INDICATOR.FREQUENCY, so a key like BOL.NGDP_RPCH.A means: country Bolivia (BOL), indicator “Real GDP, percent change” (NGDP_RPCH), and annual frequency (A). The API also allows the use of wildcards (*), for example BOL.*.A (all annual series for Bolivia) or *.NGDP_RPCH.A (annual real GDP growth for all countries). This makes the key a very compact way of specifying which dimension combinations we want.

The c parameter, in turn, allows additional filtering using a syntax like c[DIM]=value. The documentation describes it as “Filter data by component value (e.g. c[FREQ]=A)”, and it also allows logical operators. In our simple example, we do not use c because we already restrict country, indicator, and frequency directly in the key, and we let the API return the full history of the series. However, c is very useful when you want to apply additional filters (for example, narrowing the time range, selecting only certain currencies, methods, or observation statuses) without complicating the key, or when you are working with richer dimensional structures.

Suppose we want to obtain real GDP growth (percent change) for Bolivia at annual frequency.

# Code for Bolivia (according to the WEO codelist)
bol_code <- "BOL"

# Code for the "Real GDP, percent change" indicator
rgdp_code <- "NGDP_RPCH"

# Annual frequency (A) according to the FREQUENCY dimension
freq_code <- "A"

# SDMX key for WEO: COUNTRY.INDICATOR.FREQUENCY
weo_key <- paste(bol_code, rgdp_code, freq_code, sep = ".")
weo_key
## [1] "BOL.NGDP_RPCH.A"

Now that we have the key, we proceed to make the query:

# SDMX 3.0 data extraction URL (correct data query)
weo_data_url <- paste0(
  base_url,
  "/data/dataflow/",
  weo$agency, "/", weo$id, "/+/",
  weo_key
)

weo_data_resp <- httr2::request(weo_data_url) |>
  httr2::req_url_query(
    dimensionAtObservation = "TIME_PERIOD",
    attributes             = "dsd",
    measures               = "all",
    includeHistory         = "false"
  ) |>
  httr2::req_perform()

weo_data_json <- weo_data_resp |>
  httr2::resp_body_string() |>
  jsonlite::fromJSON(flatten = FALSE)

In weo_data_json we have the full API response containing the requested data. The structure of this object is quite complex, as it is a list of lists with several different levels of depth.

str(weo_data_json, max.level = 4)
## List of 2
##  $ meta: Named list()
##  $ data:List of 2
##   ..$ dataSets  :'data.frame':	1 obs. of  4 variables:
##   .. ..$ structure               : int 0
##   .. ..$ action                  : chr "Replace"
##   .. ..$ series                  :'data.frame':	1 obs. of  1 variable:
##   .. .. ..$ 0:0:0:'data.frame':	1 obs. of  2 variables:
##   .. ..$ dimensionGroupAttributes:'data.frame':	1 obs. of  1 variable:
##   .. .. ..$ :0:::List of 1
##   ..$ structures:'data.frame':	1 obs. of  6 variables:
##   .. ..$ dataSets   :List of 1
##   .. .. ..$ : int 0
##   .. ..$ links      :List of 1
##   .. .. ..$ :'data.frame':	2 obs. of  2 variables:
##   .. ..$ dimensions :'data.frame':	1 obs. of  2 variables:
##   .. .. ..$ series     :List of 1
##   .. .. ..$ observation:List of 1
##   .. ..$ measures   :'data.frame':	1 obs. of  1 variable:
##   .. .. ..$ observation:List of 1
##   .. ..$ attributes :'data.frame':	1 obs. of  3 variables:
##   .. .. ..$ dimensionGroup:List of 1
##   .. .. ..$ series        :List of 1
##   .. .. ..$ observation   :List of 1
##   .. ..$ annotations:List of 1
##   .. .. ..$ : list()

The next step, therefore, is to transform this response into a more manageable format, such as a tibble in R that contains time observations and their corresponding values. First, we extract the dataSets object from the previous list and everything it contains:

# Series 0:0:0 (the only one we requested in WEO)
obs_df <- weo_data_json$data$dataSets$series$`0:0:0`$observations

obs_values <- obs_df[1, ] |>
  unlist(use.names = FALSE) |>
  as.numeric()

# TIME_PERIOD from the structure
time_dim    <- weo_data_json$data$structures$dimensions$observation[[1]]$values |> 
  unlist(use.names = FALSE)

# Final tibble (year, value)
weo_bol_rgdp <- tibble::tibble(
  time  = as.integer(time_dim),
  value = obs_values
) |>
  dplyr::arrange(time)

weo_bol_rgdp |> 
  kableExtra::kable()
timevalue
19800.610
19810.300
1982-3.939
1983-4.042
1984-0.201
1985-1.676
1986-2.574
19872.463
19882.910
19893.790
19904.636
19915.267
19921.646
19934.269
19944.667
19954.678
19964.361
19974.954
19985.029
19990.427
20002.508
20011.684
20022.486
20032.711
20044.173
20054.421
20064.797
20074.564
20086.148
20093.357
20104.127
20115.204
20125.122
20136.796
20145.461
20154.857
20164.264
20174.195
20184.224
20192.217
2020-8.738
20216.111
20223.606
20233.082
20240.729
20250.600

Finally, we can also make a richer query. For example, we can extract both real GDP growth and the primary deficit as a percentage of GDP for Bolivia, Spain, and the United States between 2015 and 2025:

# 1. Query parameters

countries  <- c("BOL", "ESP", "USA")
indicators <- c("NGDP_RPCH", "GGXONLB_NGDP")  # Real GDP % and deficit % of GDP
freq_code  <- "A"                             # Annual frequency

country_key   <- paste(countries,  collapse = "+")
indicator_key <- paste(indicators, collapse = "+")
weo_key_multi <- paste(country_key, indicator_key, freq_code, sep = ".")

# SDMX 3.0 data URL (IMF WEO)
weo_data_url_multi <- paste0(
  base_url,
  "/data/dataflow/",
  weo$agency, "/", weo$id, "/+/",
  weo_key_multi
)


# 2. API call (1980–2030)

weo_data_resp_multi <- httr2::request(weo_data_url_multi) |>
  httr2::req_url_query(
    dimensionAtObservation = "TIME_PERIOD",
    attributes             = "dsd",
    measures               = "all",
    includeHistory         = "false",
    startPeriod            = "1980",
    endPeriod              = "2030"
  ) |>
  httr2::req_perform()

weo_data_json_multi <- weo_data_resp_multi |>
  httr2::resp_body_string() |>
  jsonlite::fromJSON(flatten = FALSE)


# 3. Dimension structures

series_dims_resp <- weo_data_json_multi$data$structures$dimensions$series[[1]]
obs_dim_resp     <- weo_data_json_multi$data$structures$dimensions$observation[[1]]

time_values <- obs_dim_resp$values |>
  unlist(use.names = FALSE)


# 4. Extract the series

series_df <- weo_data_json_multi$data$dataSets$series

# Each element is a data.frame with $attributes and $observations
series_list <- lapply(series_df, function(col) col$observations)
names(series_list) <- names(series_df)

# Function to unnest a series
extract_series_tbl <- function(obs_df, series_name) {
  # Dimension indices from "0:0:0"
  idx <- as.integer(strsplit(series_name, ":", fixed = TRUE)[[1]]) + 1L
  
  # Map indices to COUNTRY / INDICATOR / FREQUENCY codes
  dim_values <- purrr::map2_chr(
    seq_along(series_dims_resp$id),
    idx,
    ~ series_dims_resp$values[[.x]]$id[.y]
  )
  names(dim_values) <- series_dims_resp$id
  
  # Number of observations
  n_obs <- ncol(obs_df)
  if (is.null(n_obs) || n_obs == 0) {
    return(tibble::tibble(
      COUNTRY     = character(0),
      INDICATOR   = character(0),
      FREQ        = character(0),
      TIME_PERIOD = integer(0),
      value       = numeric(0)
    ))
  }
  
  # Map column names "0,1,2,...,41,..." to time_values indices
  pos_idx     <- as.integer(colnames(obs_df)) + 1L
  these_times <- as.integer(time_values[pos_idx])
  
  values_num <- obs_df[1, ] |>
    unlist(use.names = FALSE) |>
    as.numeric()
  
  tibble::tibble(
    COUNTRY     = dim_values[["COUNTRY"]],
    INDICATOR   = dim_values[["INDICATOR"]],
    FREQ        = dim_values[["FREQUENCY"]],
    TIME_PERIOD = these_times,
    value       = values_num
  )
}

# Apply to all series
weo_multi_tidy_raw <- purrr::imap_dfr(
  series_list,
  extract_series_tbl
)


# 5. Filter 2015–2025

weo_multi_tidy <- weo_multi_tidy_raw |>
  dplyr::filter(TIME_PERIOD >= 2015, TIME_PERIOD <= 2025) |>
  dplyr::arrange(COUNTRY, INDICATOR, TIME_PERIOD)

# Wide version (one column per indicator)
weo_multi_wide <- weo_multi_tidy |>
  tidyr::pivot_wider(
    id_cols    = c(COUNTRY, TIME_PERIOD),
    names_from  = INDICATOR,
    values_from = value
  )

weo_multi_wide |> 
  kableExtra::kable()
COUNTRYTIME_PERIODGGXONLB_NGDPNGDP_RPCH
BOL2015-5.9344.857
BOL2016-6.2534.264
BOL2017-6.7434.195
BOL2018-6.9794.224
BOL2019-5.8752.217
BOL2020-11.228-8.738
BOL2021-7.9796.111
BOL2022-5.4993.606
BOL2023-8.6713.082
BOL2024-7.5060.729
BOL2025-9.9340.600
ESP2015-2.6754.061
ESP2016-1.8852.915
ESP2017-0.8662.896
ESP2018-0.3992.395
ESP2019-1.0011.961
ESP2020-7.999-10.940
ESP2021-4.7236.683
ESP2022-2.5366.370
ESP2023-1.7312.461
ESP2024-1.3353.455
ESP2025-0.5782.908
USA2015-1.6922.946
USA2016-2.3741.820
USA2017-2.7812.458
USA2018-3.1012.967
USA2019-3.5282.584
USA2020-12.072-2.081
USA2021-9.1646.152
USA2022-0.9872.524
USA2023-4.6692.935
USA2024-4.6142.793
USA2025-3.8002.017

SDMX at the ECB

The European Central Bank (ECB) also offers a fairly complete SDMX 3.0 API to access its statistical data. In addition, the ECB has information on its website explaining each of its dataflows, which makes exploration easier2.

The workflow for extracting ECB data is similar to that of the IMF, but with some differences in the URL structure and specific parameters. In addition, the responses from the APIs come in .xml format and the data can be downloaded as .csv files, which must be taken into account when processing the information.

Available data: the dataflow

First, we look up the dataflows available in the ECB API.

# 1. Base URL
base_url_ecb <- "https://data-api.ecb.europa.eu/service/"

# 2. Request dataflows (XML only)
flows_resp_ecb <- httr2::request(
  paste0(base_url_ecb, "dataflow")
) |>
  httr2::req_perform()

xml <- xml2::read_xml(
  httr2::resp_body_string(flows_resp_ecb)
)

# 3. Extract Dataflow nodes
df_nodes <- xml2::xml_find_all(
  xml,
  ".//*[local-name()='Dataflow']"
)

# 4. Extract basic fields + the DSD reference
flows_tbl <- tibble::tibble(
  id      = purrr::map_chr(df_nodes, ~ xml2::xml_attr(.x, "id")),
  agency  = purrr::map_chr(df_nodes, ~ xml2::xml_attr(.x, "agencyID")),
  version = purrr::map_chr(df_nodes, ~ xml2::xml_attr(.x, "version")),
  
  # extract the DSD reference
  dsd_id = purrr::map_chr(
    df_nodes,
    ~ xml2::xml_attr(
        xml2::xml_find_first(.x, ".//*[local-name()='Structure']/*[local-name()='Ref']"),
        "id"
      )
  ),
  
  dsd_agency = purrr::map_chr(
    df_nodes,
    ~ xml2::xml_attr(
        xml2::xml_find_first(.x, ".//*[local-name()='Structure']/*[local-name()='Ref']"),
        "agencyID"
      )
  ),
  
  dsd_version = purrr::map_chr(
    df_nodes,
    ~ xml2::xml_attr(
        xml2::xml_find_first(.x, ".//*[local-name()='Structure']/*[local-name()='Ref']"),
        "version"
      )
  ),
  
  # Extract Name with namespace-independent XPath
  name    = purrr::map_chr(
    df_nodes,
    ~ xml2::xml_text(
        xml2::xml_find_first(.x, ".//*[local-name()='Name']")
      )
  )
)

# 5. Show sorted table
flows_tbl |>
  dplyr::arrange(id) |>
  dplyr::select(agency, version, dsd_id, name) |>
  kableExtra::kable()
agencyversiondsd_idname
ECB1.0ECB_BCS1AGR
ECB1.0ECB_AME1AMECO
ECB1.0ECB_BKN1Banknotes statistics
ECB.DISS1.0ECB_BKN1Banknotes statistics - Published series
ECB1.0ECB_BLS1Bank Lending Survey Statistics
ECB1.0ECB_BOP_BNTShipments of Euro Banknotes Statistics (ESCB)
ECB1.0ECB_BOP1Euro Area Balance of Payments and International Investment Position Statistics
IMF1.0BOP1_15Balance of Payments and International Investment Position (BPM6)
ECB.DISS1.0BOP1_15Balance of Payments and International Investment Position (BPM6) - Published series
IMF1.0BOPBalance of Payments and International Investment Position
ECB.DISS1.0BOPBalance of Payments and International Investment Position - Published series
ECB1.0ECB_BSI1Balance Sheet Items
ECB.DISS1.0ECB_BSI1Balance Sheet Items - Published series
ECB1.0ECB_BSI1Balance Sheet Items Statistics (tables 2 to 5 of the Blue Book)
ECB1.0ECB_CAR1CAR
ECB1.0ECB_CBD1Statistics on Consolidated Banking Data
ECB1.0ECB_CBD2Consolidated Banking data
ECB1.0ECB_CCP1Central Counterparty Clearing Statistics
ECB1.0ECB_CES1Consumer Expectations Survey
ECB1.0ECB_FMD2Composite Indicator of Systemic Stress
ECB1.0ECB_FMD2Country-Level Index of Financial Stress (CLIFS)
ECB1.0ECB_CPP3Commercial Property Price Statistics
ECB.DISS1.0ECB_CPP3Commercial Property Price Statistics - Published series
ECB1.0NA_SECCSEC
ECB.DISS1.0NA_SECCSEC - Published series
ECB1.0ECB_DCM1Dealogic DCM analytics data
ECB1.0ECB_DD1Derived Data
ECB1.0ECB_DWA1DWA
ESTAT1.0NA_SECGovernment Tax and Social Contributions Receipts Statistics (Eurostat ESA2010 TP, table 9)
ESTAT1.0NA_SECClassification of the Functions of Government Statistics (Eurostat ESA2010 TP, table 11)
ECB1.0ECB_SUR2ECS
ESTAT1.0NA_SECEDP tables
ECB.DISS1.0NA_SECEDP tables - Published series
ESTAT1.0NA_MAINNational accounts, Employment (Eurostat ESA2010 TP, table 0110, 0111)
ECB.DISS1.0NA_MAINNational accounts, Employment (Eurostat ESA2010 TP, table 0110, 0111) - Published series
ECB1.0ECB_EON1EONIA: Euro Interbank Offered Rate
ECB1.0ECB_ESA1ESA95 National Accounts
ECB1.0EUROSTAT_BOP_01European Union Balance of Payments (Source Eurostat)
ECB1.0ECB_EST1Euro Short-Term Rate
ECB1.0ECB_EWT1ECB wage tracker
ECB1.0ECB_EXR1Exchange Rates
ECB.DISS1.0ECB_EXR1Exchange Rates - Published series
ECB1.0ECB_FMD2Financial market data
ECB.DISS1.0ECB_FMD2Financial market data - Published series
ECB1.0ECB_FVC1Financial Vehicle Corporation
ECB.DISS1.0ECB_FVC1Financial Vehicle Corporation - Published series
ECB1.0ECB_FXS1Foreign Exchange Statistics
ESTAT1.0NA_SECGovernment Finance Statistics
ECB.DISS1.0NA_SECGovernment Finance Statistics - Published series
ECB1.0ECB_GST1Government Statistics
ECB1.0ECB_ICPF1Insurance Corporations Assets and Liabilities
ECB.DISS1.0ECB_ICPF1Insurance Corporations Assets and Liabilities - Published series
ECB1.0ECB_ICO1Insurance Corporations Operations
ECB.DISS1.0ECB_ICO1Insurance Corporations Operations - Published series
ECB1.0ECB_ICP1Indices of Consumer prices
EUROSTAT1.0ESTAT_ESAIEAInsurance Corporations & Pension Funds Statistics
ECB.DISS1.0ESTAT_ESAIEAInsurance Corporations & Pension Funds Statistics - Published series
ECB.DISS1.0ECB_ICP1Indices of Consumer prices - Published series
ESTAT1.0NA_MAINNational accounts, Main aggregates in the International Data Cooperation TF context
ECB.DISS1.0NA_MAINNational accounts, Main aggregates in the International Data Cooperation TF context - Published series
ESTAT1.0NA_SECNational accounts, Sector Accounts in the International Data Cooperation TF context
EUROSTAT1.0ESTAT_ESAIEAQuarterly non-financial accounts, QSA by country
EUROSTAT1.0ESTAT_ESAIEAQuarterly Euro Area Accounts
ECB.DISS1.0ESTAT_ESAIEAQuarterly Euro Area Accounts - Published series
EUROSTAT1.0EUROSTAT_LFS1Labour Force Survey Indicators - IESS definition
ECB.DISS1.0EUROSTAT_LFS1Labour Force Survey Indicators - IESS definition - Published series
ECB1.0ECB_IFI1Indicators of Financial Integration
ECB1.0ECB_ILM1Internal Liquidity Management
ECB.DISS1.0ECB_ILM1Internal Liquidity Management - Published series
ECB1.0ECB_IRS1Interest rate statistics
ECB.DISS1.0ECB_IRS1Interest rate statistics - Published series
ECB1.0ECB_IVF1Investment Funds Balance Sheet Statistics
ECB.DISS1.0ECB_IVF1Investment Funds Balance Sheet Statistics - Published series
ECB.DISS1.0ECB_BSI1Aggregated balance sheet of euro area monetary financial institutions, excluding the Eurosystem (millions of euro)
ECB.DISS1.0ECB_BSI1Domestic and cross-border positions of euro area monetary financial institutions, excluding the Eurosystem (EUR billions, outstanding amounts at end of period)
ECB.DISS1.0ECB_BSI1Growth rates for the national contributions to the aggregated balance sheet of euro area monetary financial institutions(annual growth rates at end of period)
ECB.DISS1.0ECB_EXR1Harmonised competitiveness indicators based on consumer price indices (period averages - index 1999 Q1=100)
ECB.DISS1.0ECB_EXR1Harmonised competitiveness indicators based on GDP deflators (period averages - index 1999 Q1=100)
ECB.DISS1.0ECB_EXR1Harmonised competitiveness indicators based on unit labour costs indices for the total economy (period averages - index 1999 Q1=100)
ECB.DISS1.0ESTAT_ESAIEAAggregated balance sheet of euro area insurance corporations and pension funds(EUR millions, outstanding amounts at the end of the period)
ECB.DISS1.0ECB_ICP1HICP - Annual percentage changes, breakdown by purpose of consumption(percentage change)
ECB.DISS1.0ECB_ICP1HICP - Expenditure weights, breakdown by purpose of consumption(Parts per 1000. HICP total = 1000)
ECB.DISS1.0ECB_ICP1HICP - Indices, breakdown by purpose of consumption(2015=100)
ECB.DISS1.0ECB_ICP1HICP - Annual percentage changes, breakdown by type of product (mainly used by the ECB)(percentage change)
ECB.DISS1.0ECB_ICP1HICP - Expenditure weights, breakdown by type of product (mainly used by the ECB)(Parts per 1000. HICP total = 1000)
ECB.DISS1.0ECB_ICP1HICP - Indices, breakdown by type of product (mainly used by the ECB)(2015=100)
ECB.DISS1.0ECB_IVF1Euro area and national investment fund statistics - assets and liabilities(EUR billions; amounts outstanding at the end of the period, transactions during the period)
ECB.DISS1.0ECB_IVF1Euro area and national investment fund statistics - by investment policy and type of fund(EUR billions; amounts outstanding at the end of the period, transactions during the period)
ECB.DISS1.0ECB_MFI1Number of monetary financial institutions (MFIs) in the euro area (pure number)
ECB.DISS1.0ECB_MFI1Number of monetary financial institutions (MFIs) in the non-participating Member States(pure number)
ECB.DISS1.0ECB_MIR1Euro area and national MFI interest rates (MIR)(percentages per annum ; rates on new business as average of the period ; rates on outstanding amounts as end-of-period, unless otherwise indicated)
ECB.DISS1.0NA_MAINQuarter-on-quarter volume growth of GDP and expenditure components(quarter-on-quarter percentage changes)
ECB.DISS1.0NA_MAINYear-on-year volume growth of GDP and expenditure components(annual percentage changes)
ECB.DISS1.0NA_MAINContributions to quarter-on-quarter volume growth of GDP and expenditure components(contributions to quarter-on-quarter percentage changes of GDP in percentage points)
ECB.DISS1.0NA_MAINContributions to year-on-year volume growth of GDP and expenditure components(contributions to annual percentage changes of GDP in percentage points)
ECB.DISS1.0ECB_PSS1Total number of transactions in the euro area(millions; total for the period)
ECB.DISS1.0ECB_PSS1Total number of transactions in the non-participating Member States(millions; total for the period)
ECB.DISS1.0ECB_PSS1Relative importance of payment services in the euro area(percentage of total number of national transactions)
ECB.DISS1.0ECB_PSS1Relative importance of payment services in the non-participating Member States(percentage of total number of national transactions)
ECB.DISS1.0ECB_PSS1Total value of transactions in the euro area(EUR millions; total for the period)
ECB.DISS1.0ECB_PSS1Total value of transactions in the non-participating Member States(EUR millions; total for the period)
ECB.DISS1.0ECB_BSI1JDF_PUB_BSI_CROSS_BORDER_POSITIONS - Published series
ECB.DISS1.0ECB_BSI1JDF_PUB_BSI_MFI_BALANCE_SHEET - Published series
ECB.DISS1.0ECB_IVF1JDF_PUB_IVF_INVESTMENT_FUNDS - Published series
ECB.DISS1.0ECB_MIR1JDF_PUB_MIR_BANK_INTEREST_RATES - Published series
ECB.DISS1.0BOPOfficial reserve assets, other foreign currency assets and related short-term liabilities(EUR millions)
ECB.DISS1.0ECB_SEC1Outstanding amounts and transactions of euro-denominated debt securities by country of residence, sector of the issuer and original maturity(EUR millions; nominal values)
ECB.DISS1.0ECB_SEC1Outstanding amounts and transactions of listed shares by country of residence and sector of the issuer(EUR millions; nominal values)
EUROSTAT1.0EUROSTAT_JVC2Eurostat Job Vacancy Statistics
ECB.DISS1.0EUROSTAT_JVC2Eurostat Job Vacancy Statistics - Published series
ESTAT1.0JVSJob Vacancy Statistics
ECB.DISS1.0JVSJob Vacancy Statistics - Published series
ECB1.0ECB_CBD1EBA Key Risk Indicators
ESTAT1.0LCILabour Cost Indices
ECB.DISS1.0LCILabour Cost Indices - Published series
EUROSTAT1.0EUROSTAT_LFS1Labour Force Survey
ECB.DISS1.0EUROSTAT_LFS1Labour Force Survey - Published series
ECB1.0ECB_LIG1Large Insurance Groups Statistics
ECB1.0ECB_MFI1List of MFIs
ECB1.0ECB_MIR1MFI Interest Rate Statistics
ECB.DISS1.0ECB_MIR1MFI Interest Rate Statistics - Published series
ECB1.0ECB_MMS1Money Market Survey
ECB1.0ECB_MMSR1Money Market Statistical Reporting
ESTAT1.0NA_MAINNational accounts, Main aggregates (Eurostat ESA2010 TP, table 1)
ECB.DISS1.0NA_MAINNational accounts, Main aggregates (Eurostat ESA2010 TP, table 1) - Published series
ECB.DISS1.0ECB_BSI1Euro area monetary aggregates
ECB.DISS1.0ECB_EXR1Exchange rates
ECB.DISS1.0NA_SECGovernment finance
ECB.DISS1.0ECB_ICP1Inflation
ECB.DISS1.0ECB_ICP1Key euro area indicators (ICP)
ECB.DISS1.0ECB_BSI1Key euro area indicators (BSI)
ECB.DISS1.0NA_MAINKey euro area indicators (ESA)
ECB.DISS1.0ECB_DD1Key euro area indicators (DD)
ECB.DISS1.0ECB_STS1Key euro area indicators (STS)
ECB.DISS1.0ECB_EXR1Key euro area indicators (EXR)
ECB.DISS1.0NA_SECKey euro area indicators (GST)
ECB.DISS1.0ECB_FMD2Key euro area indicators (FM)
ECB.DISS1.0ECB_MIR1Bank interest rates
ECB1.0ECB_MPD1Macroeconomic Projection Database
ECB1.0ECB_BCS1NEC
ECB1.0ECB_OFI1Other Financial Intermediaries
ECB1.0ECB_OMO1Open market operations
ECB1.0ECB_PAY1PAY
ECB1.0ECB_PAY11PCN
ECB1.0ECB_PAY5PCP
ECB1.0ECB_PAY2PCT
ECB1.0ECB_PAY3PDD
ECB1.0ECB_PAY4PEM
ECB1.0ECB_ICPF1Pension Fund Assets and Liabilities
ECB1.0ECB_PFM1Pension funds number of members
ECB1.0ECB_ICPF1Pension funds Regulation
ECB.DISS1.0ECB_ICPF1Pension funds Regulation - Published series
ECB.DISS1.0ECB_ICPF1Pension Fund Assets and Liabilities - Published series
ECB1.0ECB_PAY6PIS
ECB1.0ECB_PAY7Losses due to fraud by liability bearer
ECB1.0ECB_PAY10PMC
ECB1.0ECB_PAY9PPC
ECB1.0ECB_PAY13PSN
ECB1.0ECB_PSS1Payments and Settlement Systems Statistics
ECB1.0ECB_PAY14PST
ECB1.0ECB_PAY12PTN
ECB1.0ECB_PAY8PTT
ESTAT1.0NA_SECQuarterly Sector Accounts (MUFA and NFA Eurostat ESA2010 TP, table 801)
ECB.DISS1.0NA_SECQuarterly Sector Accounts (MUFA and NFA Eurostat ESA2010 TP, table 801) - Published series
ECB1.0ECB_BOP1International Reserves of the Eurosystem
IMF1.0BOP1_15International Reserves of the Eurosystem (BPM6)
ECB.DISS1.0BOP1_15International Reserves of the Eurosystem (BPM6) - Published series
ECB1.0ECB_RAI1Risk Assessment Indicators
IMF1.0BOPInternational Reserves of the Eurosystem
ECB.DISS1.0BOPInternational Reserves of the Eurosystem - Published series
ECB1.0ECB_FMD2Risk Dashboard data
ECB1.0ECB_FMD2Risk Dashboard data
ECB1.0ECB_RES1Commercial Property Prices
ECB.DISS1.0ECB_RES1Commercial Property Prices - Published series
ECB1.0ECB_RES1Structural Housing Indicators
ECB1.0ECB_RES1Real Estate Statistics
ECB.DISS1.0ECB_RES1Real Estate Statistics - Published series
ECB1.0ECB_RES1Residential Property Valuation
ECB1.0ECB_RIR2Retail Interest Rates
ECB1.0ECB_RPP1Residential Property Price Index Statistics
ECB.DISS1.0ECB_RPP1Residential Property Price Index Statistics - Published series
ECB1.0ECB_RPP1Residential Property Valuation
ECB1.0ECB_RTD1Real Time Database (research database)
ECB1.0ECB_SAFESurvey on the Access to Finance of SMEs
ECB1.0ECB_SEC1Securities
ECB.DISS1.0ECB_SEC1Securities - Published series
ECB1.0ECB_SEE1Securities exchange - Trading Statistics
ECB1.0ECB_SESFODSurvey on credit terms and conditions in euro-denominated securities financing and over-the-counter derivatives markets
ECB1.0ECB_SHI1Structural Housing Indicators Statistics
ECB1.0ECB_SHS6Securities Holding Statistics
ECB1.0NA_SECSHSS
ECB1.0ECB_FCT1Survey of Professional Forecasters
ECB1.0ECB_SSI1Banking structural statistical indicators
ECB1.0ECB_SSI1Structural Financial Indicators for Payments
ECB1.0ECB_SSS1Securities Settlement Statistics
ECB1.0ECB_BOP1Balance of Payments statistics, national data
ECB1.0ECB_BOP1Euro Area Balance of Payments and International Investment Position Statistics, Geographical Breakdown
ECB1.0ECB_BCS1Short-Term Business Statistics
ECB1.0ECB_STP1STEP data
ECB1.0ECB_STS1Short-Term Statistics
ECB.DISS1.0ECB_STS1Short-Term Statistics - Published series
ECB1.0ECB_SUP1Supervisory Banking Statistics
ECB1.0ECB_SUR1Opinion Surveys
ECB.DISS1.0ECB_SUR1Opinion Surveys - Published series
ECB1.0ECB_TGB1Target Balances
ECB1.0ECB_TRD1External Trade
ECB.DISS1.0ECB_TRD1External Trade - Published series
ECB1.0ECB_WTS1Trade weights
ECB1.0ECB_FMD2Financial market data - yield curve
ECB.DISS1.0ECB_FMD2Financial market data - yield curve - Published series

In the table above we can see all the dataflows available in the ECB API. Each dataflow has a unique identifier (id), a responsible agency (agency), a version (version), a dsd_id that will be used to identify the data structure, and a descriptive name (name).

Let’s suppose we are interested in the EST dataflow, which shows the euro short-term interest rate (€STR) and reflects the wholesale unsecured euro funding costs of euro area banks for overnight borrowing.

# Keep only the EST dataflow
est <- flows_tbl |> 
  dplyr::filter(id == "EST")

est |>
  kableExtra::kable()
idagencyversiondsd_iddsd_agencydsd_versionname
ESTECB1.0ECB_EST1ECB1.0Euro Short-Term Rate

Data structure: the dimensions

As mentioned earlier, the next step is to understand which variables (dimensions) are available in the data structure of the EST dataflow. In this particular case, the ECB has detailed information on its website about what this dataflow contains. However, we are going to verify its dimensions via the API:

# 1. Define DSD location explicitly

base_url_ecb <- "https://data-api.ecb.europa.eu/service/"
dsd_id       <- "ECB_EST1"
dsd_agency   <- "ECB"
dsd_version  <- "latest"   # or "1.0" if you want a fixed version


# 2. Download the DSD (Structure with children: codelists, concepts)

url_dsd <- paste0(
  base_url_ecb,
  "datastructure/", dsd_agency, "/", dsd_id, "/", dsd_version,
  "?references=children"
)

dsd_resp <- httr2::request(url_dsd) |>
  httr2::req_perform()

xml_dsd <- xml2::read_xml(
  httr2::resp_body_string(dsd_resp)
)


# 3. Extract series-level dimensions only

series_dims <- xml2::xml_find_all(
  xml_dsd,
  ".//*[local-name()='DataStructure']
     /*[local-name()='DataStructureComponents']
     /*[local-name()='DimensionList']
     /*[local-name()='Dimension']"
)


# 4. Extract Dimension ID and ConceptIdentity/Ref

dimensions_tbl <- tibble::tibble(
  id = purrr::map_chr(
    series_dims,
    ~ xml2::xml_attr(.x, "id")
  ),
  
  concept_ref = purrr::map_chr(
    series_dims,
    ~ {
      concept_node <- xml2::xml_find_first(
        .x,
        ".//*[local-name()='ConceptIdentity']/*[local-name()='Ref']"
      )
      xml2::xml_attr(concept_node, "id")
    }
  ),
  
  position = seq_along(series_dims)
)

dimensions_tbl |> 
  kableExtra::kable()
idconcept_refposition
FREQFREQ1
BENCHMARK_ITEMBENCHMARK_ITEM2
DATA_TYPE_ESTDATA_TYPE_EST3

Codelists: the valid codes

Once we know the dimensions of the EST dataflow, the next step is to extract the codelists associated with each dimension. In some cases, the DSD contains additional information about the codelists, such as constraints that help us understand which codes are valid. In other cases, it does not.

Suppose the DSD did not show the codelist constraints. In that case, we would have to use a more pragmatic approach and extract a data sample to understand what values our dimensions actually take. This is relatively easy with the ECB because the API returns structured data in .csv format. We can also use the lastNObservations parameter to control the sample size:

est_sample <- httr2::request(
  "https://data-api.ecb.europa.eu/service/data/EST"
) |>
  httr2::req_url_query(
    format = "csvdata",
    lastNObservations = 100
  ) |>
  httr2::req_perform() |>
  httr2::resp_body_string() |>
  readr::read_csv(show_col_types = FALSE)

est_sample |> 
  dplyr::select(KEY, FREQ, BENCHMARK_ITEM, DATA_TYPE_EST, TIME_PERIOD, OBS_VALUE) |>
  head() |> 
  kableExtra::kable()
KEYFREQBENCHMARK_ITEMDATA_TYPE_ESTTIME_PERIODOBS_VALUE
EST.B.EU000A2QQF08.CIBEU000A2QQF08CI2025-07-22107.2684
EST.B.EU000A2QQF08.CIBEU000A2QQF08CI2025-07-23107.2742
EST.B.EU000A2QQF08.CIBEU000A2QQF08CI2025-07-24107.2799
EST.B.EU000A2QQF08.CIBEU000A2QQF08CI2025-07-25107.2856
EST.B.EU000A2QQF08.CIBEU000A2QQF08CI2025-07-28107.3028
EST.B.EU000A2QQF08.CIBEU000A2QQF08CI2025-07-29107.3086

To check the unique values, we run the following query on the dataflow dimensions:

est_sample |> 
  dplyr::distinct(FREQ, BENCHMARK_ITEM, DATA_TYPE_EST, TITLE) |> 
  kableExtra::kable()
FREQBENCHMARK_ITEMDATA_TYPE_ESTTITLE
BEU000A2QQF08CICompounded euro short-term rate index (1 Oct 2019 = 100)
BEU000A2QQF16CRCompounded euro short-term rate average rate, 1 week tenor
BEU000A2QQF24CRCompounded euro short-term rate average rate, 1 month tenor
BEU000A2QQF32CRCompounded euro short-term rate average rate, 3 months tenor
BEU000A2QQF40CRCompounded euro short-term rate average rate, 6 months tenor
BEU000A2QQF57CRCompounded euro short-term rate average rate, 12 months tenor
BEU000A2X2A25CMEuro short-term rate - Calculation method
BEU000A2X2A25NBEuro short-term rate - Number of active banks
BEU000A2X2A25NTEuro short-term rate - Number of transactions
BEU000A2X2A25R25Euro short-term rate - Rate at 25th percentile of volume
BEU000A2X2A25R75Euro short-term rate - Rate at 75th percentile of volume
BEU000A2X2A25RPEuro short-term rate - Publication type
BEU000A2X2A25TTEuro short-term rate - Total volume
BEU000A2X2A25VLEuro short-term rate - Share of volume of the 5 largest active banks
BEU000A2X2A25WTEuro short-term rate - Volume-weighted trimmed mean rate

Extracting data: the final query

Now suppose we want to extract the euro short-term compounded interest rate (€STR) with a maturity of 12 months. According to the sample above, the relevant codes are:

est_all <- httr2::request(
  "https://data-api.ecb.europa.eu/service/data/EST/B.EU000A2QQF57.CR"
) |>
  httr2::req_url_query(
    format = "csvdata",
    startPeriod = "2025-01-01",
    endPeriod   = "2025-12-06"
  ) |>
  httr2::req_perform() |>
  httr2::resp_body_string() |>
  readr::read_csv(show_col_types = FALSE)

est_all |> 
  head() |> 
  kableExtra::kable()
KEYFREQBENCHMARK_ITEMDATA_TYPE_ESTTIME_PERIODOBS_VALUE
EST.B.EU000A2QQF57.CRBEU000A2QQF57CR2025-01-023.70759
EST.B.EU000A2QQF57.CRBEU000A2QQF57CR2025-01-033.70480
EST.B.EU000A2QQF57.CRBEU000A2QQF57CR2025-01-063.69739
EST.B.EU000A2QQF57.CRBEU000A2QQF57CR2025-01-073.69557
EST.B.EU000A2QQF57.CRBEU000A2QQF57CR2025-01-083.69083
EST.B.EU000A2QQF57.CRBEU000A2QQF57CR2025-01-093.68803

We can now plot the results:

library(ggplot2)

est_all |> 
  ggplot(aes(x = TIME_PERIOD, y = OBS_VALUE)) +
  geom_line(color = "steelblue", linewidth = 1.2) +
  labs(
    title = "Euro short-term compounded interest rate (€STR) - 12 months",
    x = "Date",
    y = "Interest rate (%)"
  ) +
  theme_bw()

A new library: imfapi

A few months ago, someone in my LinkedIn network shared a post by Christopher Smith announcing the creation of a new library that “provides user-friendly functions for programmatic access to macroeconomic data from the ‘SDMX 3.0 IMF Data API’ of the International Monetary Fund.” The library in question is available on CRAN and works stably. Furthermore, it is part of the EconDataverse, which I recommend exploring. Below I will show, in a simplified way, how to access the same WEO data we saw earlier.

Using imfapi to extract WEO data

First, we load the library:

library(imfapi)

Then we retrieve the dataflows available in the IMF API:

dataflows <- imfapi::imf_get_dataflows()

dataflows |> 
  dplyr::glimpse()
## Rows: 72
## Columns: 6
## $ id           <chr> "IMTS", "IL", "MFS_OFC", "ISORA_LATEST_DATA_PUB", "WPCPER…
## $ name         <chr> "International Trade in Goods (by partner country) (IMTS)…
## $ description  <chr> "The International trade in goods by partner country data…
## $ version      <chr> "1.0.0", "13.0.1", "7.0.0", "4.0.0", "6.0.0", "1.0.0", "9…
## $ agency       <chr> "IMF.STA", "IMF.STA", "IMF.STA", "ISORA", "IMF.STA", "IMF…
## $ last_updated <chr> "2025-03-28T17:18:44.429573Z", "2025-08-29T09:08:22.00741…

Now we filter for the WEO dataflow:

weo_flow <- dataflows |> 
  dplyr::filter(id == "WEO")

weo_flow |> 
  kableExtra::kable()
idnamedescriptionversionagencylast_updated
WEOWorld Economic Outlook (WEO)The World Economic Outlook (WEO) database is created during the biannual WEO exercise, which begins in January and June of each year and results in the April and September/October WEO publication. Selected series from the publication are available in a database format.9.0.0IMF.RES2025-10-08T18:00:33.590181Z

And we obtain the dimensions of the WEO dataflow:

dsd_weo <- imfapi::imf_get_datastructure(dataflow_id = "WEO")
dsd_weo |> 
  kableExtra::kable()
dimension_idtypeposition
COUNTRYDimension0
INDICATORDimension1
FREQUENCYDimension2

Next, we extract the codelists associated with the WEO dataflow:

dim_list <- list()

for (dim_id in dsd_weo$dimension_id) {
  dim_list[[dim_id]] <- imfapi::imf_get_codelists(
    dimension_ids = dim_id,
    dataflow_id   = "WEO"
  )
}

dim_list |> 
  dplyr::bind_rows() |> 
  dplyr::group_by(dimension_id) |> 
  dplyr::summarise(n_codes = dplyr::n()) |> 
  kableExtra::kable()
dimension_idn_codes
COUNTRY338
FREQUENCY6
INDICATOR145

In this case we have 338 codes for the COUNTRY dimension, 6 codes for the FREQUENCY dimension, and 145 valid codes for INDICATOR.

Finally, we extract WEO data for Bolivia and real GDP growth:

data <- imfapi::imf_get(
  dataflow_id = "WEO",
  dimensions  = list(
    COUNTRY   = "BOL",
    INDICATOR = "NGDP_RPCH",
    FREQUENCY = "A"
  )
)

data |>
  kableExtra::kable()
COUNTRYINDICATORFREQUENCYTIME_PERIODOBS_VALUE
BOLNGDP_RPCHA19800.610
BOLNGDP_RPCHA19810.300
BOLNGDP_RPCHA1982-3.939
BOLNGDP_RPCHA1983-4.042
BOLNGDP_RPCHA1984-0.201
BOLNGDP_RPCHA1985-1.676
BOLNGDP_RPCHA1986-2.574
BOLNGDP_RPCHA19872.463
BOLNGDP_RPCHA19882.910
BOLNGDP_RPCHA19893.790
BOLNGDP_RPCHA19904.636
BOLNGDP_RPCHA19915.267
BOLNGDP_RPCHA19921.646
BOLNGDP_RPCHA19934.269
BOLNGDP_RPCHA19944.667
BOLNGDP_RPCHA19954.678
BOLNGDP_RPCHA19964.361
BOLNGDP_RPCHA19974.954
BOLNGDP_RPCHA19985.029
BOLNGDP_RPCHA19990.427
BOLNGDP_RPCHA20002.508
BOLNGDP_RPCHA20011.684
BOLNGDP_RPCHA20022.486
BOLNGDP_RPCHA20032.711
BOLNGDP_RPCHA20044.173
BOLNGDP_RPCHA20054.421
BOLNGDP_RPCHA20064.797
BOLNGDP_RPCHA20074.564
BOLNGDP_RPCHA20086.148
BOLNGDP_RPCHA20093.357
BOLNGDP_RPCHA20104.127
BOLNGDP_RPCHA20115.204
BOLNGDP_RPCHA20125.122
BOLNGDP_RPCHA20136.796
BOLNGDP_RPCHA20145.461
BOLNGDP_RPCHA20154.857
BOLNGDP_RPCHA20164.264
BOLNGDP_RPCHA20174.195
BOLNGDP_RPCHA20184.224
BOLNGDP_RPCHA20192.217
BOLNGDP_RPCHA2020-8.738
BOLNGDP_RPCHA20216.111
BOLNGDP_RPCHA20223.606
BOLNGDP_RPCHA20233.082
BOLNGDP_RPCHA20240.729
BOLNGDP_RPCHA20250.600

In the table above we can see the WEO data for Bolivia and real GDP growth at annual frequency. The imfapi library considerably simplifies the data extraction process from the IMF API, allowing users to focus on data analysis rather than on the technical details of the SDMX API.

Conclusions

The aim of this post was to show that, for the applied analyst, working with SDMX APIs is not an end in itself but a way to build cleaner, more reproducible, and more scalable workflows. Instead of relying on manual downloads and poorly documented steps, SDMX allows us to find, understand, and extract macroeconomic data from different organizations under a common logic.

Through the IMF and ECB examples we saw that the general process—identifying the dataflow, reviewing its structure, consulting the codelists, and finally extracting the data—is repeated consistently, which makes it easier to automate and apply to other datasets. Moreover, tools like imfapi show how this approach can be simplified even further, reducing the technical burden so that most of the effort can be dedicated to analysis.


  1. These two modifiers tell the server that we want the full structural information of the dataflow and all related elements—that is, the DSD, the concepts, and, crucially, all the codelists that depend on the dataflow. This saves us from having to manually search for which codelists exist, what they are called, or which version they are in. Instead, we obtain the complete WEO structure in a single, clean, and consistent call. ↩︎

  2. For example, the EST dataflow, which corresponds to the euro short-term rate (€STR) and which we will investigate later, can be found here↩︎