TutorialAPIISIN

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 API

Getting 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

bash
curl --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:

  • data has 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

typescript
const 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

python
import os
import requests
BASE = "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", ... } }.

StatusCodeWhat to do
400invalid_isinShow a validation message; the request was not counted
401missing_api_key / invalid_api_keyCheck the header and the key
404fund_not_foundThe ISIN is valid but is not a fund/ETF we can cover; data is an empty skeleton
429rate_limitedWait retryAfter seconds (also in the Retry-After header)
502upstream_errorTemporary 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.

typescript
for (const isin of universe) {
await getFund(isin); // populates the 24h cache
await new Promise((r) => setTimeout(r, 500));
}

Next steps

Try it on your own ISINs

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