Skip to content

API

The Refugee Data Portal Data API gives programmatic access to all of the data that is published on the portal. It is a read-only service: it lets analysts, researchers and developers access the data and metadata, query it with filters, and download complete data series, without visiting the data finder.

This guide introduces how the API works and walks through the most common tasks with examples that can be copied and run as they are. It complements the interactive technical reference, which lists every endpoint, parameter and response field.

  • Base URL. All examples in this guide use https://api-data.unhcr.org/api/v0.
  • No account is required. The API is open and needs no key or token.
  • Responses are JSON, except for downloads, which are CSV files.
  • Every request is rate-limited per client. Data queries allow 60 requests per minute, downloads 10 per minute, and catalogue, constraints and metadata requests 120 per minute. Requests over the limit receive an HTTP 429 response and should be retried after a short pause.

The Data API is provided to support the analysis and extraction of the data published on the Refugee Data Portal. It is designed for analysts, researchers, journalists and developers who need to retrieve data series for their own analysis, reports, research or occasional integration into tools and scripts.

The API is not designed to serve as a data backend for other products or services. In particular, it is not intended for:

  • production systems that depend on high availability or guaranteed response times;
  • continuous or automated high-volume access, such as scheduled bulk extraction or frequent polling for changes;
  • applications, dashboards or services offered to third parties that rely on the API for their operation.

If you would like to access the data from the portal for one of these purposes, please reach out to us.

Service level. The API is offered as a best-effort service. Availability, response times and rate limits are not guaranteed and may change without prior notice. Endpoints and response formats under the current version may also change.

Fair use. Requests are rate-limited per client, as described above. Clients are expected to request one page at a time, to pause between requests, to use the download endpoint rather than paginated queries to obtain complete data series, and to keep a local copy of data that is used repeatedly rather than requesting it again.

Advanced requirements. Organisations that need a higher rate limit, a service-level commitment, continuous or large-scale access, or that plan to build a product on top of UNHCR data are invited to contact UNHCR at [contact channel] to discuss their requirements.

Attribution. Data retrieved through the API is subject to the same terms of use and attribution requirements as the data published on the portal. See [terms of use page].

The API offers two complementary ways to obtain data. Choosing the right one is the single most important decision when using the service, and the most common source of incomplete results.

NeedUseWhat is returned
All the data published as part of a data seriesGET /data/export/{code}One CSV file with every row of the data series, in a single download
A selection of records filtered by the dimensions of interest such as population type (pop_type), country of asylum (ref_area_adm0), country of origin (area_ori_adm0), time range (time_period), etc.GET /data/{code}JSON, paginated: at most 100 records per request, with filters and column selection

To obtain the complete set of data associated with a data series, use the download endpoint. It streams the full data series in one request and does not require pagination. At present it does not accept filters: the file always contains the whole data series. A filtered download will be made available in a forthcoming release.

The query endpoint never returns the full data series in one response. It returns one page at a time, 20 records by default and 100 at most. A client that reads only the first page will miss the rest of the data. The section Retrieving all matching records explains how to walk through every page.

The examples below use the official population statistics data series, whose code is PopStatsOfficial. The same steps apply to every data series.

The catalogue lists every data series with its code, labels and definition. It supports free-text search and filters by dataset, topic, country, region, population type and framework.

GET https://api-data.unhcr.org/api/v0/catalog?q=population%20statistics

Each entry in array carries the data series code together with ready-made links to its data, metadata and constraints:

{
"code": "PopStatsOfficial",
"label_short": "Official population statistics on people affected by forced displacement and statelessness",
"dataset": "POP",
"data_url": ".../api/v0/data/PopStatsOfficial",
"metadata_url": ".../api/v0/metadata/PopStatsOfficial",
"constraints_url": ".../api/v0/constraints/PopStatsOfficial"
}

The catalogue is itself paginated (page, page_size), so a search that returns more than one page must be walked in the same way as data queries.

2. Learn how the data series is structured

Section titled “2. Learn how the data series is structured”

Before querying, ask the API which dimensions the data series has, which values each dimension accepts, and which units are available. This is the reference for every filter used later.

GET https://api-data.unhcr.org/api/v0/constraints/PopStatsOfficial

The response describes, among other things:

  • dimensions: one entry per dimension, with its role and the list of valid values. For PopStatsOfficial the dimensions include pop_type, ref_area_adm0 (country of asylum), area_ori_adm0 (country of origin), sex, age, urbanisation and setting.
  • units.default and units.available: the unit that queries return unless another one is requested, and the alternatives.
  • time_period.granularity: whether the data series is reported by year or by month, which determines how time filters are written.

Two special values appear at the top of many dimensions:

  • _T (Total) selects the aggregated record, for example both sexes together.
  • _X (Unspecified) selects records for which the dimension was not reported.

Population type codes follow the population definitions used across the portal, for example REF_UNHCR for refugees under UNHCR's mandate, REF_UNRWA for refugees under UNRWA's mandate, OIP for other's in need of international protection and ASY for asylum-seekers.

Filters use the SDMX 3.0 convention c[dimension]=value. Several values for the same dimension are separated by commas and combined with OR. Different dimensions are combined with AND.

The request below returns the number of refugees under UNHCR's mandate in Colombia from 2025 onwards, one record per reporting period:

GET https://api-data.unhcr.org/api/v0/data/PopStatsOfficial?c[pop_type]=REF_UNHCR&c[ref_area_adm0]=COL&time_period=ge:2025&dimensions=default
{
"data": [
{
"time_period": "2025-12",
"pop_type": "REF_UNHCR", "pop_type_label": "Refugees [UNHCR]",
"ref_area_region_unhcr": "RBA", "ref_area_region_unhcr_label": "Regional Bureau for the Americas",
"ref_area_adm0": "COL", "ref_area_adm0_label": "Colombia",
"unit_measure": "IND", "unit_mult": 1, "unit_comp": "TOT",
"obs_value": 1464
}
],
"pagination": {
"page": 1, "page_size": 20, "total_rows": 1, "total_pages": 1,
"unfiltered_total_rows": 650016, "total_rows_estimated": false, "has_next": false
}
}

Every response includes a query block echoing the parameters that were applied, a data array with the records, and a pagination block. Each coded column is accompanied by a _label column with its human-readable name.

The dimensions parameter controls the level of detail:

  • omitted: every record stored for the data series, at every level of disaggregation;
  • default: the data series' headline view, keeping only its required and default dimensions (here period, population type, region and country) and returning the totals;
  • a comma-separated list: a custom breakdown. The list must include every dimension the data series marks as required in its constraints and every dimension used in a filter; the remaining names add the breakdown.

For example, the refugee population of Uganda from 2025 onwards, disaggregated by sex. pop_type is required in PopStatsOfficial and ref_area_adm0 is filtered, so both stay in the list and in the output; sex is the breakdown:

GET https://api-data.unhcr.org/api/v0/data/PopStatsOfficial?c[pop_type]=REF_UNHCR&c[ref_area_adm0]=UGA&time_period=ge:2025&dimensions=pop_type,ref_area_adm0,sex
time_periodpop_typeref_area_adm0sexobs_value
2025-12REF_UNHCRUGAF991,958
2025-12REF_UNHCRUGAM944,761
2025-12REF_UNHCRUGA_T1,936,719

Records can be ordered with sort=field:asc or sort=field:desc, for instance sort=obs_value:desc to rank countries by population.

GET https://api-data.unhcr.org/api/v0/data/export/PopStatsOfficial

The response is a CSV file (PopStatsOfficial.csv) with one row per data point and the same coded and labelled columns as the query endpoint. Large data series run to hundreds of thousands of rows, so the download should be saved to disk with a command-line tool or a script rather than opened in the interactive reference:

Terminal window
curl -L -o PopStatsOfficial.csv "https://api-data.unhcr.org/api/v0/data/export/PopStatsOfficial"

The metadata endpoint returns the data series' definition, rationale, method of computation, comparability notes, limitations, data sources, related frameworks and coverage.

GET https://api-data.unhcr.org/api/v0/metadata/PopStatsOfficial

The same information is available as a CSV file from GET /metadata/export/PopStatsOfficial.

Retrieving all matching records with the query endpoint

Section titled “Retrieving all matching records with the query endpoint”

A query is answered one page at a time. To retrieve every record that matches a set of filters:

  1. Request page=1 with page_size=100, the maximum.
  2. Read pagination.has_next. While it is true, request the next page with exactly the same filters, dimensions and sort.
  3. Stop when has_next is false.

has_next is always exact, so it is the safest signal to stop on. total_rows and total_pages are exact in almost all cases, but for some custom multi-dimension breakdowns the first request returns an estimate, flagged by total_rows_estimated: true, until the exact count is available a few seconds later.

Three rules keep a client within the rate limit while walking pages:

  • One request at a time. Request the next page only after the previous one has been received; never fetch pages in parallel.
  • Pause between pages. A one-second pause keeps the client under the rate-limit.
  • Wait and retry on 429. When the limit is exceeded, wait a few seconds and request the same page again rather than abandoning the walk.

Check total_rows on the first page before walking. If it runs to tens of thousands of records, the download endpoint is the right tool: at 100 records per request and one request per second, an entire data series of several hundred thousand records would take hours through the query endpoint, while the download returns it in a single request.

A minimal Python example that follows these rules:

import time
import requests
BASE = "https://api-data.unhcr.org/api/v0"
params = {
"c[pop_type]": "REF_UNHCR",
"time_period": "ge:2025",
"dimensions": "default",
"page_size": 100,
"page": 1,
}
rows = []
while True:
response = requests.get(f"{BASE}/data/PopStatsOfficial", params=params, timeout=60)
if response.status_code == 429:
time.sleep(10) # over the limit: wait, then retry the same page
continue
response.raise_for_status()
body = response.json()
rows.extend(body["data"])
if not body["pagination"]["has_next"]:
break
params["page"] += 1
time.sleep(1) # one request per second stays within the limit
print(f"{len(rows)} records retrieved")

Why a query returns fewer records than expected

Section titled “Why a query returns fewer records than expected”

When a result looks incomplete, the cause is almost always one of the following.

  1. Only the first page was read. The default page holds 20 records. Check pagination.has_next and total_rows, and walk through every page as described above. To get the whole data series, use the download endpoint instead.
  2. Only the default unit is returned. A query returns records for one unit at a time, the data series' default unless unit= is given. constraints.units.available lists the alternatives with their record counts. unfiltered_total_rows in every response is the size of the full data series for the unit in use.
  3. The dimensions parameter changed the level of detail. dimensions=default returns the headline totals only. Omitting dimensions returns every disaggregated record. A custom list returns one record per combination of the listed dimensions.
  4. A filtered dimension was left out of dimensions. The API rejects the request with 400 INVALID_FILTER rather than silently dropping the filter. Add the dimension to the list.
  5. A code did not match. Filter values must be the exact codes advertised by the constraints endpoint. REF returns no records in PopStatsOfficial; REF_UNHCR does. A wrong dimension name is rejected with the list of valid dimensions in the error details.
  6. The time filter was written incorrectly. Time filters take the form time_period=op:year, where op is one of eq, ge, gt, le, lt, for example time_period=ge:2025. Two conditions are joined with + to form a range, for example time_period=ge:2020+le:2023. A year filter covers every period reported within that year, whatever the data series' granularity.
  7. The value is stored as _X (Unspecified). Records without a reported value for a dimension carry the _X code. Filtering on a specific category excludes them. Filtering on _X selects them.

Every error uses the same envelope, with a machine-readable code and a plain-language message:

{
"error": {
"status": 404,
"code": "INDICATOR_NOT_FOUND",
"message": "Indicator 'DoesNotExist' not found",
"details": {}
}
}
StatusCodeMeaning
400INVALID_FILTER, INVALID_PARAMETERA filter, dimension or parameter is not valid. details lists what is accepted.
404INDICATOR_NOT_FOUNDThe indicator code does not exist. Check it in the catalogue.
429RATE_LIMIT_EXCEEDEDToo many requests. Wait a moment and retry.
503SERVICE_UNAVAILABLEThe service is starting up. Retry shortly.
EndpointPurposePaginated
GET /catalogSearch and list data seriesYes
GET /metadata/{code}Data series definition, sources and coverageNo
GET /metadata/export/{code}Same metadata as a CSV fileNo
GET /constraints/{code}Dimensions, valid values and units of a data seriesNo
GET /data/{code}Filtered query of a data seriesYes, 100 records max
GET /data/export/{code}Complete data series as a CSV fileNo

Parameters of GET /data/{code}:

ParameterExampleNotes
c[dimension]c[ref_area_adm0]=COL,PEROne or more values, comma-separated. Repeat for each dimension.
time_periodge:2025 or ge:2020+le:2023Operators eq, ge, gt, le, lt, applied to a year.
unitIND.1.TOTDefaults to the data series' default unit.
dimensionsdefault or pop_type,sexColumn selection and level of detail.
sortobs_value:descOne or more field:direction pairs.
page, page_sizepage=2&page_size=100page_size is 20 by default and 100 at most.
footnotestrueAdds source and annotation columns.

The full specification of every parameter and response field is available in the interactive technical reference.

Please reach out directly for any questions or concerns.