Where do I get fund and ETF data for a portfolio tracker app?
A portfolio tracker needs to turn the ISINs a user holds into names, categories, fees, holdings and exposures, then aggregate them. FundFacts API does the fund layer by ISIN; pair it with a price feed for valuations. What to use and how.Updated 12 September 2026 · by FundFacts APIShort answer
Use FundFacts API for everything about the funds a user holds: GET /funds/{isin} turns each ISIN into name, category, fees, risk indicator, top holdings and sector, country and asset weights, and POST /portfolio aggregates a whole portfolio into its real exposure with a weighted TER. Use your existing price source for valuations; the API is the factsheet layer, not a quote feed. The Free plan (15 lookups a month) is enough to prototype; Pro ($49) adds look-through and commercial use.
What a portfolio tracker app needs from a data source
- ISIN → fund name and share class, so a holding shows as "iShares Core MSCI World (USD Acc)" rather than a code.
- Category, fees and risk per holding, and the top holdings and exposures behind each fund.
- Aggregation: what does the whole portfolio actually hold by sector, country and asset class, and what does it cost.
- Prices for valuation (from the broker or a market feed).
- Freshness signals so the app can say "holdings as of 31 August".
Why FundFacts API fits
Users enter or import ISINs; the app resolves each one once and caches the payload until expiresAt. GET /search?q= (free) lets users find a fund by name when they only know that. POST /portfolio returns the look-through in one call: sector, geography, assetAllocation, creditQuality, topHoldings with a coverage figure each, plus fees.weightedTer and risk.weightedSrri. GET /overlap shows a user how much two of their ETFs duplicate each other. The React components in @fundfactsapi/widgets render the donuts, bars and growth chart from the payload directly.
What it does not do
- Prices and daily valuation come from your broker connection or a market-data API; the API's series is monthly and for charts.
- Stocks and bonds in the same portfolio are not funds; the API returns 404 for them and your app keeps them as plain positions.
Start in ten minutes
- Get a key at fundfactsapi.com/signup (Free plan, 15 lookups a month, no card).
- Look up one fund and read the shape:
python# pip install fundfacts (or call https://fundfactsapi.com/api/v1/funds/{isin} with any HTTP client)from fundfacts import FundFactsff = FundFacts() # reads FUNDFACTS_API_KEY; free key at https://fundfactsapi.com/signupfund = ff.get_fund("IE00B4L5Y983")d = fund["data"]print(fund["name"], d["profile"]["category"], d["headlineMetrics"]["ter"], d["riskRating"], d["dataAsOf"])print(d["topHoldings"][:5], d["sector"][:3], d["geography"][:3])
- Read the field reference for every key, and the endpoints for batch, search, portfolio, overlap, factsheets and SCPI.
- If a coding agent is building it, point it at /llms-full.txt, the complete AGENTS.md for the API.
What it costs
Each distinct ISIN costs one request per day at most (cache until expiresAt), so an app with 2,000 users holding 300 distinct funds is roughly 300 requests a day: Pro ($49). Look-through is one request per position per call; cache it per portfolio until the earliest expiry. One request is counted per ISIN answered; search is free. Plans are on the pricing page.
Alternatives worth knowing
- A market-data API alone (prices, tickers) when you only need valuations and no look-through, holdings or fees.
- Broker aggregation providers when the problem is importing positions rather than describing them; they rarely carry factsheet fields, so the two combine well.
The provider comparison and the compare pages go into each in more depth.
Verify it yourself
The demo endpoint returns the live payload for a fund that is already in the store, without a key. Everything on this page can be checked against it.bashcurl -s https://fundfactsapi.com/api/v1/demo/funds/IE00B4L5Y983 | jq '{name, asOf: .data.dataAsOf, ter: .data.headlineMetrics.ter, risk: .data.riskRating, top: .data.topHoldings[:3]}'