Humanitarian data API integration: merging UNHCR, ACLED, and OCHA sources for longitudinal research
How to pull refugee statistics, conflict events, and humanitarian indicators from their respective APIs, reconcile them onto a shared unit of analysis, and produce a research-ready panel that a reviewer can cite and reproduce.
TL;DR
Integrate humanitarian data APIs by fixing one unit of analysis first (usually country-year or admin1-month), then aggregating each source to it before joining. UNHCR Refugee Data Finder needs no key and is paginated; ACLED needs a free key and returns one row per event; OCHA HDX exposes a CKAN catalogue of files rather than a query API. Join on ISO3 or OCHA P-codes, never place names, and store retrieval dates so the merged panel stays citable.
Why integration is harder than access
Humanitarian data is unusually open. UNHCR, ACLED, OCHA, IDMC, and IPC all publish machine-readable interfaces, and pulling a single country series from any of them takes a few minutes. The difficulty starts at the second source. Each organisation counts a different thing, at a different frequency, against a different geography, with a different revision policy, and none of that is visible in the JSON.
A researcher who joins UNHCR year-end refugee totals to ACLED event counts on country name alone will get a table that runs cleanly and describes something that does not exist. The joins succeed for the easy cases, fail quietly for Cote d'Ivoire and the Democratic Republic of the Congo, and the resulting correlation is an artefact of which countries happened to match.
This guide treats integration as the analytical step it is. The code is straightforward; the decisions around it are what make a dataset defensible under peer review.
Six steps to a merged research panel
Step 1.Decide what a single row in your final dataset means
Before writing a request, define the unit of analysis: country-month, admin1-quarter, or camp-year. UNHCR reports stock figures by year and country of asylum, ACLED codes discrete events with a timestamp, and OCHA/HDX publishes mixed-frequency indicator tables. Almost every integration failure traces back to merging three different units of analysis into one table and treating the result as comparable.
Step 2.Pull UNHCR population statistics
The UNHCR Refugee Data Finder API at api.unhcr.org/population/v1/population/ serves refugee, asylum-seeker, IDP, and stateless stock figures. Key parameters are yearFrom, yearTo, coo (country of origin), coa (country of asylum), and cf_type=ISO for three-letter codes. Responses are paginated; read the total field and loop pages until you have every row.
Step 3.Pull ACLED conflict events and collapse them to your unit
ACLED returns one row per coded event. To join it against annual stock figures, aggregate first: group by country and period, then sum fatalities and count events per event_type. Keep the raw event pull cached on disk so you can re-aggregate at a different granularity without re-hitting the API.
Step 4.Pull OCHA and HDX indicator tables through CKAN
HDX runs on CKAN. Use data.humdata.org/api/3/action/package_search with a query such as q=idp+sudan to find datasets, then read the resources array for a direct CSV or XLSX download URL. HDX resources are files rather than a query interface, so plan for schema drift: column names and admin codings change between vintages.
Step 5.Normalise geography with P-codes and ISO3
Join on ISO3 country codes at national level and on OCHA P-codes at subnational level, never on place names. Names differ by transliteration, punctuation, and administrative reform. Where a source ships only names, map them once through a lookup table you maintain and version, and log every unmatched name rather than silently dropping it.
Step 6.Merge, validate, and record provenance
Left-join onto a complete country-period spine so gaps stay visible as nulls instead of disappearing rows. Store the retrieval timestamp and source vintage alongside every value. A merged table without provenance columns is not citable, and reviewers will ask which UNHCR release a 2026 figure came from.
The five APIs worth wiring up
| Source | Endpoint | Unit of analysis | What to watch |
|---|---|---|---|
| UNHCR Refugee Data Finder | api.unhcr.org/population/v1/ | Country-year stock and flow | Refugees, asylum-seekers, IDPs of concern, returnees, stateless. Annual figures are finalised mid-year for the previous year; mid-year releases are provisional. |
| ACLED | api.acleddata.com/acled/read | Individual event | Requires a free non-commercial key. Events are revised retroactively as sources emerge, so refresh recent windows monthly. |
| OCHA HDX (CKAN) | data.humdata.org/api/3/action/ | Varies by dataset | A catalogue rather than a single schema. Check the dataset update frequency and the organisation field before treating a resource as authoritative. |
| IDMC GIDD | helix-tools-api.idmcdb.org | Country-year, disaggregated by conflict and disaster | The reference series for new internal displacements. Distinguishes flow (new displacements in a year) from stock (people living in displacement at year end). |
| IPC / FEWS NET | api.ipcinfo.org | Admin area-analysis period | Phase classifications are consensus-based estimates over a projection window, not measurements at a point in time. |
Methodology: endpoints and units summarised from each provider's public API documentation as of July 2026. Access terms and rate limits change; verify against the provider before building a scheduled pipeline.
Working code
Python: pull UNHCR and ACLED, merge onto a spine
import requests
import pandas as pd
# 1. UNHCR population statistics (no key required)
def unhcr_population(coa_iso3: str, year_from: int, year_to: int) -> pd.DataFrame:
rows, page = [], 1
while True:
r = requests.get(
"https://api.unhcr.org/population/v1/population/",
params={
"coa": coa_iso3,
"yearFrom": year_from,
"yearTo": year_to,
"cf_type": "ISO",
"limit": 1000,
"page": page,
},
timeout=60,
)
r.raise_for_status()
batch = r.json().get("items", [])
if not batch:
break
rows.extend(batch)
page += 1
return pd.DataFrame(rows)
# 2. ACLED events, aggregated to country-year before joining
def acled_country_year(country: str, key: str, email: str) -> pd.DataFrame:
r = requests.get(
"https://api.acleddata.com/acled/read",
params={"key": key, "email": email, "country": country, "limit": 5000},
timeout=60,
)
r.raise_for_status()
ev = pd.DataFrame(r.json().get("data", []))
ev["year"] = pd.to_numeric(ev["year"], errors="coerce")
ev["fatalities"] = pd.to_numeric(ev["fatalities"], errors="coerce").fillna(0)
return (
ev.groupby("year")
.agg(events=("event_id_cnty", "count"), fatalities=("fatalities", "sum"))
.reset_index()
)
# 3. Merge onto a complete spine so gaps stay visible
spine = pd.DataFrame({"year": range(2015, 2027)})
pop = unhcr_population("SDN", 2015, 2026)
pop = pop.groupby("year", as_index=False)["refugees"].sum()
pop["year"] = pd.to_numeric(pop["year"], errors="coerce")
conflict = acled_country_year("Sudan", "YOUR_KEY", "you@example.org")
panel = (
spine.merge(pop, on="year", how="left")
.merge(conflict, on="year", how="left")
)
panel["retrieved_at"] = pd.Timestamp.utcnow().date().isoformat()
panel["sources"] = "UNHCR Refugee Data Finder; ACLED"
print(panel)
R: the same pull with httr2 and dplyr
library(httr2)
library(dplyr)
library(purrr)
unhcr_population <- function(coa, year_from, year_to) {
page <- 1
out <- list()
repeat {
resp <- request("https://api.unhcr.org/population/v1/population/") |>
req_url_query(coa = coa, yearFrom = year_from, yearTo = year_to,
cf_type = "ISO", limit = 1000, page = page) |>
req_perform() |>
resp_body_json(simplifyVector = TRUE)
if (length(resp$items) == 0) break
out[[page]] <- resp$items
page <- page + 1
}
bind_rows(out)
}
panel <- unhcr_population("SDN", 2015, 2026) |>
mutate(year = as.integer(year), refugees = as.numeric(refugees)) |>
group_by(year) |>
summarise(refugees = sum(refugees, na.rm = TRUE), .groups = "drop") |>
right_join(tibble(year = 2015:2026), by = "year") |>
arrange(year) |>
mutate(retrieved_at = as.character(Sys.Date()),
source = "UNHCR Refugee Data Finder")
panel
Discovering HDX resources through CKAN
curl -G "https://data.humdata.org/api/3/action/package_search" \
--data-urlencode "q=internally displaced sudan" \
--data-urlencode "rows=5" | jq '.result.results[] | {name, organization: .organization.title, resources: [.resources[] | {format, url}]}'
HDX returns dataset metadata with a resources array. Read the format and url fields, download the file, and record the dataset name and last-modified date in your provenance columns.
Rules that keep a merged panel defensible
- Join on codes. ISO3 nationally, OCHA P-codes subnationally. Maintain one versioned lookup for any source that ships names only, and log unmatched entries as errors.
- Aggregate before joining. Collapse event data to the panel unit first. Joining events directly to stock figures duplicates the stock across every event row.
- Use a complete spine. Build the full country-period grid, then left-join. Silent row loss is the failure mode nobody catches in review.
- Carry provenance per value. Source name, dataset vintage, and retrieval date belong in the table, not only in the methods section.
- Archive raw pulls. Write untouched responses to dated files. Reproducibility depends on the snapshot, not the endpoint.
- Test the join. Assert expected row counts and zero unmatched keys in your pipeline so a schema change fails loudly rather than shrinking the sample.
Common pitfalls
Stock and flow are not interchangeable
Country of origin is not country of asylum
Revisions break reproducibility
Missing is not zero
Frequently asked questions
Which humanitarian data APIs should I use for longitudinal research?
Four cover most needs: the UNHCR Refugee Data Finder API for refugee, asylum-seeker, and stateless stock figures; ACLED for coded conflict events; the OCHA HDX CKAN API for the broader catalogue of humanitarian indicator datasets; and IDMC GIDD for internal displacement flows. IPC adds food-security phase classifications where analysis requires them.
How do I merge UNHCR, ACLED, and OCHA data into one dataset?
Pick a single unit of analysis such as country-year or admin1-month, aggregate each source to that unit before joining, and join on ISO3 codes or OCHA P-codes rather than place names. Left-join onto a complete spine of country-period combinations so unreported periods appear as nulls, and carry a source and retrieval-date column for every merged value.
Does the UNHCR API require an API key?
No. The Refugee Data Finder population API is open and requires no authentication, though it is paginated and rate-sensitive under heavy looping. ACLED does require a free key and registered email for non-commercial use, and HDX allows anonymous reads of public resources.
Why do UNHCR and IDMC report different displacement numbers for the same country?
They measure different things. UNHCR reports stock, meaning people living in displacement at a point in time, and its IDP figures cover only situations where UNHCR has an operational mandate. IDMC reports both stock and new displacements during a year, counting movements rather than people, so an individual displaced three times contributes three events. Neither number is wrong; citing them as if interchangeable is.
How should I handle missing years when building a longitudinal panel?
Keep them as explicit nulls on a complete country-year spine and never interpolate silently. If a model needs a balanced panel, document the imputation method and publish both the imputed and raw series so reviewers can check how much of the trend is real reporting.
Can I redistribute data pulled from these APIs?
Terms differ. HDX resources carry per-dataset licences, many of them open. UNHCR statistics are reusable with attribution. ACLED non-commercial access permits derived analysis and charts but prohibits bulk redistribution of raw event data. Publish code and aggregates rather than mirrored raw exports, and cite each source with its retrieval date.
Related resources on Humanity Centered Data
Filtering conflict events by event_type and fatalities
How HDX organises datasets and licences
Reading IDP, refugee, IPC, and ACLED figures correctly
Datasets, APIs, and methodologies
Satellite layers alongside tabular humanitarian data
Pre-merged displacement panels with provenance columns
