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.holdings`number`Total 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.