How to get fund data from an ISIN with a REST API (curl, JavaScript, Python)
Step-by-step: call a fund data API with an ISIN and get key facts, holdings, risk and performance as JSON. Includes curl, fetch and requests examples and error handling.Published 19 August 2026 · 2 min read · by FundFacts APIGetting reliable fund data used to mean either an enterprise data licence or a fragile PDF scraper. This tutorial shows the third option: one HTTP request that takes an ISIN and returns a complete, structured factsheet as JSON. We will use FundFacts API, but the patterns — bearer auth, a long timeout for cold requests, quota headers — apply to any fund data API.
1. Get an API key
Sign up for a free account, open the dashboard and create a key. Keys start with ffk_ and are shown once; store them in an environment variable, never in client-side code. The free plan gives you one request per hour, enough to explore the schema; paid plans raise that to 100 or 1,000 requests per day (see pricing).
2. Make your first request
bashcurl --max-time 300 https://fundfactsapi.com/api/v1/funds/IE00B4L5Y983 \-H "Authorization: Bearer $FUNDFACTS_API_KEY"
Note --max-time 300. The first request for an ISIN nobody has asked for yet triggers a live refresh, which takes one to three minutes. Every request in the following 24 hours is served from cache in a few hundred milliseconds. If you cannot wait that long in a request cycle, pre-warm the ISINs you care about in a background job (more on that below).
3. Read the response
The envelope is the same for every fund:
json{"isin": "IE00B4L5Y983","name": "iShares Core MSCI World ETF USD Acc","cached": true,"generatedAt": "2026-09-02T13:41:07.112Z","expiresAt": "2026-09-03T13:41:07.112Z","plan": "pro","quota": { "limit": 100, "remaining": 87, "resetAt": "2026-09-03T09:12:44.000Z" },"data": {"keyFacts": { "assetClass": "Equities", "currency": "USD", "aum": "USD 151.7bn", "inception": "Sep 25, 2009", "holdings": 1344 },"riskRating": 4,"topHoldings": [{ "name": "NVIDIA Corp", "weight": 5.43 }],"sector": [{ "label": "Technology", "weight": 30.5 }],"calendarReturns": { "years": ["2021", "2022"], "fund": [21.9, -18] },"headlineMetrics": { "ter": "0.20%", "volatility3y": "11.9%", "sharpe3y": "1.21" }}}
Three things worth internalising:
datahas the same keys for every fund. Panels that do not apply (credit quality for an equity ETF, say) are empty arrays or empty strings, never missing. You can write one renderer.- Breakdowns are arrays of
{ label, weight }with weights in percent. Holdings use{ name, weight }. - Formatted metrics stay strings with their unit (
"0.20%","USD 151.7bn") so you never lose the currency or the scale. Numbers you will chart (returns, weights) are numbers.
The full list of fields is in the documentation.
4. JavaScript / TypeScript
typescriptconst BASE = "https://fundfactsapi.com/api/v1";export async function getFund(isin: string) {const res = await fetch(`${BASE}/funds/${isin}`, {headers: { Authorization: `Bearer ${process.env.FUNDFACTS_API_KEY}` },signal: AbortSignal.timeout(300_000),});if (res.status === 429) {const { error } = await res.json();throw new Error(`Quota exceeded, retry in ${error.retryAfter}s`);}if (!res.ok) {const { error } = await res.json();throw new Error(`${res.status} ${error.code}: ${error.message}`);}return res.json();}const fund = await getFund("IE00B4L5Y983");console.log(fund.name, fund.data.headlineMetrics.ter, fund.quota.remaining);
5. Python
pythonimport osimport requestsBASE = "https://fundfactsapi.com/api/v1"HEADERS = {"Authorization": f"Bearer {os.environ['FUNDFACTS_API_KEY']}"}def get_fund(isin: str) -> dict:r = requests.get(f"{BASE}/funds/{isin}", headers=HEADERS, timeout=300)if r.status_code == 429:retry = r.json()["error"]["retryAfter"]raise RuntimeError(f"Quota exceeded, retry in {retry}s")r.raise_for_status()return r.json()fund = get_fund("IE00B4L5Y983")top = fund["data"]["topHoldings"][:5]for h in top:print(f"{h['name']:<30} {h['weight']:>6.2f}%")
6. Handle errors properly
Every error uses one envelope: { "error": { "code", "message", ... } }.
| Status | Code | What to do |
|---|---|---|
| 400 | invalid_isin | Show a validation message; the request was not counted |
| 401 | missing_api_key / invalid_api_key | Check the header and the key |
| 404 | fund_not_found | The ISIN is valid but is not a fund/ETF we can cover; data is an empty skeleton |
| 429 | rate_limited | Wait retryAfter seconds (also in the Retry-After header) |
| 502 | upstream_error | Temporary upstream issue; retry with back-off |
The X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers let you throttle before you hit 429.
7. Pre-warm your universe
For a screener or a portfolio tool you know the ISINs in advance. Run a nightly job that requests each one; the first run is slow, every subsequent run and every user request is a cache hit. With a Scale plan (1,000 requests / day) that covers a universe of several hundred funds with headroom for ad-hoc lookups.
typescriptfor (const isin of universe) {await getFund(isin); // populates the 24h cacheawait new Promise((r) => setTimeout(r, 500));}
Next steps
- Learn what each block contains: ETF holdings and exposure, performance series, risk indicators.
- Put it together: build a fund comparison tool.
- Or just grab a free key and paste your own ISIN into the dashboard's "Try it" box.