HoldingsExposurePortfolio

ETF holdings API: fetch top holdings, sector and country exposure as JSON

How to pull ETF and fund holdings, sector weights, geographic exposure and asset allocation from an API, and how to turn them into portfolio look-through and concentration checks.Published 26 August 2026 · 3 min read · by FundFacts API

"What is actually inside this fund?" is the question behind most fund research tools: look-through for a client portfolio, concentration limits for a compliance check, a sector donut on a product page. This article walks through the holdings and exposure blocks returned by FundFacts API and shows how to use them.

The exposure blocks at a glance

FieldShapeTypical use
data.topHoldings[{ name, weight }] (top 10)Holdings table, overlap analysis
data.sector[{ label, weight }]Sector donut, GICS tilt
data.geography[{ label, weight }]Country/region map, home bias
data.region[{ label, weight }]Coarser regional split
data.assetAllocation[{ label, weight }]Equity / bond / cash split for multi-asset funds
data.creditQuality[{ label, weight }]Rating buckets for bond funds
data.maturity[{ label, weight }]Maturity ladder for bond and money-market funds
data.instrument[{ label, weight }]Instrument mix for money-market funds
data.keyFacts.holdingsnumberTotal number of positions

Weights are percentages of the portfolio (0–100). Arrays that do not apply to the asset class are empty, so an equity ETF has an empty creditQuality and a bond fund has an empty sector or a sector array describing instrument types — one renderer handles both.

Example: a global equity ETF

bash
curl https://fundfactsapi.com/api/v1/funds/IE00B4L5Y983 \
-H "Authorization: Bearer $FUNDFACTS_API_KEY"
json
{
"topHoldings": [
{ "name": "NVIDIA Corp", "weight": 5.43 },
{ "name": "Apple Inc", "weight": 5.08 },
{ "name": "Microsoft Corp", "weight": 3.92 }
],
"sector": [
{ "label": "Technology", "weight": 30.5 },
{ "label": "Financial Services", "weight": 16.34 },
{ "label": "Industrials", "weight": 10.83 }
],
"geography": [
{ "label": "North America", "weight": 75.54 },
{ "label": "Europe Developed", "weight": 12.26 },
{ "label": "Japan", "weight": 5.78 }
]
}

Use case 1: portfolio look-through

Given a client portfolio of funds with weights, the effective exposure to a sector is the weight-averaged sum of each fund's sector weights:

typescript
type Weighted = { label: string; weight: number };
​
export function lookThrough(
positions: { isin: string; weight: number }[], // portfolio weights, sum = 100
funds: Record<string, { sector: Weighted[] }>, // API responses by ISIN
): Weighted[] {
const acc = new Map<string, number>();
for (const p of positions) {
for (const s of funds[p.isin].sector) {
acc.set(s.label, (acc.get(s.label) ?? 0) + (p.weight / 100) * s.weight);
}
}
return [...acc].map(([label, weight]) => ({ label, weight: +weight.toFixed(2) }))
.sort((a, b) => b.weight - a.weight);
}

Run the same function over geography for country exposure and over topHoldings to spot the single names your client holds through three different funds at once.

Use case 2: concentration and overlap checks

  • Top-10 concentration: sum(topHoldings.weight). Above 40–50 % the fund is effectively a bet on a handful of names.
  • Largest position: topHoldings[0].weight. UCITS diversification rules cap single issuers at 10 %, so a value near 10 % is worth flagging.
  • Overlap between two funds: intersect the name fields of two topHoldings arrays and sum the minimum weight of each shared name.
typescript
export function overlap(a: Weighted[], b: Weighted[]) {
const byName = new Map(b.map((h) => [h.label, h.weight]));
return a.reduce((sum, h) => sum + Math.min(h.weight, byName.get(h.label) ?? 0), 0);
}
Note
Top holdings cover the ten largest positions, which is what factsheets publish. For a 1,344-holding index fund they represent roughly a quarter of the portfolio; overlap computed on them is a lower bound, not the full picture.

Use case 3: charts on a product page

Because every breakdown is already { label, weight }, feeding a chart library is a one-liner:

typescript
const donut = fund.data.sector.map((s) => ({ name: s.label, value: s.weight }));

The landing page renders sector donuts, holdings bars and country exposure directly from the sample response — no transformation layer.

Use case 4: bond and money-market funds

For fixed income the interesting arrays are creditQuality (AAA … below B), maturity (1–3 years, 3–5 years …) and, for money-market funds, instrument (commercial paper, certificates of deposit, repos). Combine them with metrics.averageRating, metrics.effectiveDuration and metrics.wam / metrics.wal from the metrics block to build a credit-and-duration profile.

Freshness

Holdings change; the API refreshes each ISIN at most every 24 hours and tells you when in generatedAt and data.dataAsOf (the "as of" date published with the portfolio). For compliance reporting, store dataAsOf next to the numbers you used.

Try it

Create a free key, request one of your funds and inspect data.topHoldings and data.sector in the dashboard. The complete field list, including every exposure array, is in the documentation.

Try it on your own ISINs

One request returns key facts, holdings, risk and performance as JSON. Free plan, no card.

Questions this answers

By field

How do I get the holdings of an ETF or fund by ISIN?

Top holdings with weights for any ETF or fund from its ISIN, as JSON: data.topHoldings, the holdings count, and the overlap endpoint that compares two funds' holdings.
By field

How do I get the sector exposure of a fund or ETF by ISIN?

Sector weights (Information Technology, Financials, Health Care…) for any fund or ETF from its ISIN as a JSON array: data.sector, with a rule-based sector tilt label.
By field

How do I get the country or region exposure of a fund by ISIN?

Country and region weights for any fund or ETF from its ISIN as JSON arrays: data.geography and data.region, plus a region-focus label for filtering.
Alternatives

Yahoo Finance doesn't give me ETF holdings or fund data by ISIN. What can I use instead?

Yahoo Finance and yfinance cover prices, not factsheets, and key by ticker per exchange rather than ISIN. For holdings, sector weights, TER, risk indicator and KID costs of UCITS funds, use an ISIN-keyed fund data API. Comparison and code.
Choosing a data source

What is the best free ETF data API?

For ETF factsheet data (holdings, TER, risk indicator, sector and country weights, returns) by ISIN, FundFacts API's free plan gives 15 full lookups a month with no card and a keyless demo endpoint. What is free, what is not, and the free alternatives for prices.
By language & tool

How do I get fund or ETF data by ISIN in Python?

Python fund data by ISIN: requests (or httpx) example returning name, TER, risk, holdings and returns; timeout=300 so a cold ISIN does not fail.
By language & tool

How do I get fund or ETF data by ISIN in JavaScript (Node.js)?

JavaScript (Node.js) fund data by ISIN: native fetch (undici) example, no dependency, AbortSignal.timeout(300000) for the slow first load of a cold ISIN.
By language & tool

How do I get fund or ETF data by ISIN in TypeScript?

TypeScript fund data by ISIN: typed client with the SDK's FundResponse and FundData types, so headlineMetrics.ter autocompletes and nulls are explicit.
By language & tool

How do I get fund or ETF data by ISIN in Go?

Go fund data by ISIN: net/http and encoding/json example that decodes only the fields you use, with a 300-second client timeout for cold ISINs.
By language & tool

How do I get fund or ETF data by ISIN in Java?

Java fund data by ISIN: java.net.http.HttpClient (Java 11+) and Jackson example with a 300-second request timeout for the first load of a cold ISIN.
By language & tool

How do I get fund or ETF data by ISIN in PHP?

PHP fund data by ISIN: curl extension example (Guzzle works the same in Laravel or Symfony) with CURLOPT_TIMEOUT 300 for the first load of a cold ISIN.
By language & tool

How do I get fund or ETF data by ISIN in Ruby?

Ruby fund data by ISIN: Net::HTTP example from the standard library with read_timeout = 300, because a cold ISIN is loaded on demand.