# AGENTS.md — FundFacts API > Public edition, generated 2026-09-09 from the live field reference (https://fundfactsapi.com/llms-full.txt). Get a key at https://fundfactsapi.com/signup; subscribers can download a copy with their plan and key baked in from https://fundfactsapi.com/dashboard. You are working in a project that uses **FundFacts API**: a REST API that turns any fund or ETF ISIN into one structured JSON factsheet (identity, key facts, fees, risk indicator, holdings, sector and country exposure, performance series, risk statistics). Read this file fully before writing code against the API. Everything you need is here; do not scrape https://fundfactsapi.com or any third-party site for fund data. ## 1. What the API is, and is not - One resource: `GET https://fundfactsapi.com/api/v1/funds/{isin}`. Input is a 12-character ISIN; output is the envelope described in section 3 with a `data` object described in section 7. - Coverage: UCITS funds, ETFs, money-market funds and most open-end funds worldwide. Equities, bonds, crypto, indices and structured products are not funds and return `404 fund_not_found`. - Every payload is the fund's latest published figures. It is refreshed at most once per 24 hours per ISIN; `cached` tells you whether this call reused the stored payload. - The API does not stream. To work on a universe of funds, send ISINs in batches of up to the plan's batch size (`POST /funds`) rather than one call per ISIN; `/search` turns names into ISINs but only over funds that have already been loaded. - Values are shaped for display: money and percentages arrive as formatted strings (`"USD 105.4 bn"`, `"0.20%"`); breakdown weights and return series are numbers in percent. ## 2. Setup and the API key The key is **not** in this file. It is read from the environment variable `FUNDFACTS_API_KEY`. - If it is not set, ask the user for their FundFacts API key (it starts with `ffk_`, created at https://fundfactsapi.com/dashboard) and store it in `.env` / `.env.local`, which must be listed in `.gitignore`. - Never print the key in logs, commit it, or ship it to a browser bundle. Calls must go through server-side code or a backend route. Authentication is a bearer token on every request (`X-Api-Key: ` is accepted as well): ```bash curl -s https://fundfactsapi.com/api/v1/funds/IE00B4L5Y983 -H "Authorization: Bearer $FUNDFACTS_API_KEY" ``` ## 3. Endpoint and response envelope `GET https://fundfactsapi.com/api/v1/funds/{isin}` — `isin` is case-insensitive; the response echoes the canonical upper-case form. | Path | Type | Meaning | |---|---|---| | `isin` | `string` | Canonical upper-case ISIN that was resolved. | | `name` | `string \| null` | Fund / share-class name. | | `cached` | `boolean` | true when the payload was served from the 24-hour cache, false when it was refreshed for this request. | | `generatedAt` | `string (ISO 8601)` | When the payload was produced. | | `expiresAt` | `string (ISO 8601)` | When the payload will be considered stale and refreshed on the next request. | | `plan` | `"free" \| "starter" \| "pro" \| "scale" \| "enterprise"` | Plan of the API key used for the request. | | `quota` | `{ limit: number \| null; remaining: number \| null; used: number; overageUsed: number; resetAt: string; burst: { limit: number \| null; remaining: number \| null } }` | Credit state after this request: monthly credits (limit), credits left, credits used this month, requests billed as overage, the next monthly reset (UTC) and the per-minute burst allowance. null limit means metered / unlimited. | | `data` | `object` | The structured factsheet. See the field reference below. | ### Other endpoints | Endpoint | Purpose | Credits | |---|---|---| | `POST https://fundfactsapi.com/api/v1/funds` body `{ "isins": ["…"], "wait": true }` | Batch lookup; `results[]` carries `{ input, isin, status, name, cached, generatedAt, expiresAt, data }` with `status` = `ok` / `not_found` / `pending` / `invalid` / `error`. `pending` ISINs are being warmed in the background: re-send them later. | 1 per ok / not_found | | `GET https://fundfactsapi.com/api/v1/search?q=msci world&limit=10` | Name, issuer or ISIN-prefix search over already-loaded funds. `results[]`: `{ isin, name, issuer, currency, assetClass, shareClass, income, category, dataAsOf, url }`. | free | | `POST https://fundfactsapi.com/api/v1/portfolio` body `{ "positions": [{ "isin": "…", "weight": 60 }, …] }` | Look-through: weights normalised to 100; returns `positions[]`, `coverage`, `fees.weightedTer`, `risk.weightedSrri`, `kinds`, `assetAllocation`, `sector`, `geography`, `region`, `creditQuality`, `currency`, `topHoldings` (each breakdown `{ items: [{ label, weight }], coverage }`), `pending[]`. | 1 per position | | `GET https://fundfactsapi.com/api/v1/overlap?isins=A,B,C` | Pairwise holdings overlap: `pairs[] = { a, b, overlap, sharedCount, shared: [{ label, a, b }], disclosed }`. | 1 per ISIN | | `GET https://fundfactsapi.com/api/v1/changes?since=&isins=A,B` | Change feed (Scale+): tracked fields that moved between refreshes (`fund.changed`) and as-of date moves (`fund.refreshed`); page with `nextSince`. Webhooks (`/webhooks`) push the same events, signed with `X-FundFacts-Signature`. | free | | `GET https://fundfactsapi.com/api/v1/export?issuer=&assetClass=` | Bulk NDJSON of every stored fund matching the filter (Scale+). | 1 per call | | `POST https://fundfactsapi.com/api/v1/extract` (multipart `file`, or JSON `{ url }` / `{ text }`) | Any KID / KIID / factsheet PDF → the same `data` shape, for funds outside the covered issuers (Pro+). | 5 per document | | `POST https://fundfactsapi.com/api/v1/factsheets` body `{ "isin": "…", "format": "html" | "pdf", "deliver": "file" | "link", "theme", "accent", "title", "branding" }` | One-page factsheet rendered with @fundfactsapi/widgets. Returns the document (text/html or application/pdf; share links in `X-Factsheet-Url` / `X-Factsheet-Pdf-Url`) or, with `deliver: "link"`, JSON `{ id, isin, name, issuer, format, options, favorite, createdAt, dataAsOf, urls: { html, pdf, embed } }`. The `urls` need no key: mail the PDF, link the page, embed the iframe. `GET /factsheets` lists, `GET /factsheets/{id}?format=pdf` re-renders, `PATCH` `{ favorite }`, `DELETE`. | 1 (HTML) / 2 (PDF); list, get, share links free | | `GET https://fundfactsapi.com/api/v1/me` | `{ email, plan: { id, name, credits, burstPerMinute, batchMax, overageUsd }, quota, usage: { thisMonth, monthStart } }`. Use once at startup to confirm the key, not on every request. | free | | `GET https://fundfactsapi.com/api/v1/demo/funds/{isin}` | Keyless read of a fund already in the store (30/min per IP). For verifying the shape, never for production. | none | ## 4. Plan and rate limits Plans are credit-based. Free: 150 credits / month, batch 1. One credit is spent per ISIN that receives an answer (found or not found) on `/funds/{isin}`, `POST /funds`, `/portfolio` and `/overlap`, whether the payload comes from the 24-hour store or is freshly loaded. `/search` and `/me` are free. Invalid ISINs, pending entries and rejected (401/429) calls cost nothing. Read the quota from the response headers and never guess: | Header | Meaning | |---|---| | `X-RateLimit-Limit` | Credits per month | | `X-RateLimit-Remaining` | Credits left this month after this call (0 while running on overage) | | `X-RateLimit-Reset` | Unix time (seconds) of the next monthly reset | | `X-Burst-Limit` / `X-Burst-Remaining` | Requests allowed per minute, and how many are left | | `Retry-After` | Only on 429: seconds to wait (60 for a burst, until the reset for exhausted credits) | The same numbers are in the `quota` object of every response (`limit`, `remaining`, `used`, `overageUsed`, `resetAt`, `burst`). A 429 carries `reason`: `burst` (slow down), `credits_exhausted` or `overage_cap` (upgrade or wait for the reset). Other plans for reference: Starter: 1,500 credits / month, batch 10; Pro: 9,000 credits / month, batch 50; Scale: 60,000 credits / month, batch 200. Upgrading is done at https://fundfactsapi.com/dashboard/billing, never from code. ## 5. Freshness and latency: plan for slow first calls - A **warm** ISIN (loaded by anyone in the last 24 hours) returns in well under a second with `cached: true`. - A **cold** ISIN is loaded from the fund's published documents on demand. Expect **15 seconds to 3 minutes**; the server allows up to **300 seconds**. Set your HTTP client timeout to 300 s for this endpoint. Never retry a request that is still in flight: the second call waits on the same load and burns quota. - Two callers asking for the same cold ISIN at once share one load; the second one receives the same payload. - After 24 hours the next request refreshes the payload; `expiresAt` tells you when. `data.dataAsOf` is the date of the underlying figures (monthly for most funds), `generatedAt` is when the payload was produced. Show `dataAsOf` to end users, not `generatedAt`. - **Cache locally.** Persist payloads keyed by ISIN with `expiresAt`; re-fetch only after expiry. Pre-warm a universe with a background job (sequentially, a few in parallel at most) rather than on the user's first page view. ## 6. Errors All errors have the shape `{ "error": { "code": string, "message": string, ...details } }`. | HTTP | `error.code` | When | What to do | |---|---|---|---| | 400 | `invalid_isin` | The ISIN is not syntactically valid (format + checksum), or a batch / portfolio / overlap call contained no valid ISIN. | Fix the input; validate ISINs client-side first. | | 400 | `invalid_body` | POST body is not the expected JSON shape (batch: { isins: [] }; portfolio: { positions: [{ isin, weight }] }). | Surface the message to the user. | | 400 | `invalid_query` | Search: ?q= is missing or shorter than two characters. | Surface the message to the user. | | 400 | `batch_too_large` | More ISINs than the plan's batch size (Free 1, Starter 10, Pro 50, Scale 200, Enterprise 1,000). Includes batchMax. | Surface the message to the user. | | 401 | `missing_api_key` | No Authorization: Bearer header (or X-Api-Key) was sent. | Check FUNDFACTS_API_KEY / the Authorization header; ask the user for a valid key. | | 401 | `invalid_api_key` | The key is malformed or unknown. | Check FUNDFACTS_API_KEY / the Authorization header; ask the user for a valid key. | | 401 | `revoked_api_key` | The key exists but was revoked from the dashboard. | Ask the user to create a new key in the dashboard. | | 404 | `fund_not_found` | No public data could be retrieved for this ISIN (not a fund/ETF, delisted, or not covered yet). The response still contains the empty `data` skeleton. | Not a covered fund. Report to the user; do not retry or look elsewhere. | | 403 | `plan_required` | The endpoint needs a higher plan: changes, webhooks and export need Scale; extract needs Pro. Includes `required`. | Surface the message to the user. | | 404 | `not_cached` | Demo endpoint only: the ISIN has not been loaded yet. Use an API key to load it. | Surface the message to the user. | | 404 | `not_found` | Webhooks: no such endpoint on this account. | Surface the message to the user. | | 429 | `rate_limited` | Monthly credits (and overage cap) exhausted, or the per-minute burst limit hit. `reason` is credits_exhausted \| overage_cap \| burst; includes retryAfter (seconds) and resetAt; the Retry-After header is also set. | Wait Retry-After seconds; reduce parallelism; suggest an upgrade if it recurs. | | 502 | `upstream_error` | The fund data could not be retrieved (temporary upstream outage). Retry later; the request is counted. | Retry after 60 s, at most twice; then surface the error. | Validate ISINs before calling (12 characters: 2 letters, 9 alphanumerics, 1 check digit, Luhn over the digit expansion) to avoid spending a request on a typo. ## 7. Field reference (`data`) Fields that do not apply to the fund's asset class are empty strings, `null` or empty arrays, never missing. Treat empty as "not disclosed", not as zero. ### Identity & key facts Who issues the fund, what it invests in, how large it is and how it is structured. | Path | Type | Meaning | |---|---|---| | `data.investmentObjective` | `string` | The fund's stated investment objective / strategy paragraph. Example: `"The Fund seeks to track the performance of an index composed of developed market companies."` | | `data.securityType` | `string` | Security-type label of the share class. Example: `"ETF" \| "Open-End Fund"` | | `data.structure` | `string` | Normalized structure token derived from securityType. Example: `"ETF" \| "UCITS Fund"` | | `data.shareClass` | `string \| undefined` | Share-class letter/label when it can be inferred (e.g. from the fund name). Example: `"A" \| "I" \| "USD Acc"` | | `data.keyFacts.assetClass` | `string` | Broad asset class. Example: `"Equity" \| "Fixed Income" \| "Money Market" \| "Allocation"` | | `data.keyFacts.subAsset` | `string` | FundFacts category, identical to profile.category (composed from the disclosed exposures; see the profile group). Example: `"Global Blend Equity"` | | `data.keyFacts.currency` | `string` | Share-class base currency (ISO 4217). Example: `"USD"` | | `data.keyFacts.aum` | `string` | Fund size / net assets, formatted as reported, with unit and currency. Example: `"USD 105.4 bn"` | | `data.keyFacts.inception` | `string` | Inception date of the share class. Example: `"25/09/2009"` | | `data.keyFacts.distribution` | `string` | Income treatment. Example: `"Accumulating" \| "Distributing"` | | `data.keyFacts.holdings` | `string \| number` | Number of holdings in the portfolio. Example: `1355` | | `data.keyFacts.manager` | `string` | Portfolio manager(s) or management company. Example: `"BlackRock Asset Management Ireland"` | | `data.managerTenure` | `string` | Tenure of the longest-serving manager, when disclosed. Example: `"5.3 years"` | | `data.benchmarkName` | `string` | Primary prospectus benchmark / index tracked. Example: `"MSCI World NR USD"` | ### Risk rating & profile The regulatory risk indicator, plus the FundFacts profile: a classification derived from the fund's own disclosed data with fixed, published rules, so every value can be recomputed from the same payload. Rules are versioned in profile.rules. | Path | Type | Meaning | |---|---|---| | `data.riskRating` | `number \| null` | SRRI / SRI risk indicator on a 1 (lowest) to 7 (highest) scale. Falls back to an asset-class heuristic when not disclosed. Example: `6` | | `data.profile.kind` | `"equity" \| "fixedIncome" \| "moneyMarket" \| "allocation" \| "alternative" \| "other"` | Broad kind, derived from keyFacts.assetClass. Example: `"equity"` | | `data.profile.category` | `string` | Composed label: region + valuation (or sector for sector funds) for equity; currency + credit grade + duration for bonds; equity share for allocation funds. Example: `"Global Blend Equity" \| "EUR High-Grade Bond, Extensive Duration" \| "Balanced Allocation (58% equity)"` | | `data.profile.riskBand` | `"low" \| "medium" \| "high" \| null` | SRRI 1–2 low, 3–4 medium, 5–7 high. Example: `"medium"` | | `data.profile.concentration` | `"concentrated" \| "balanced" \| "diversified" \| null` | Weight of the ten largest holdings: ≥50% concentrated, 30–50% balanced, <30% diversified. Example: `"diversified"` | | `data.profile.regionFocus` | `string \| null` | Largest region (or country) when it is ≥80% of the portfolio; otherwise "Global". Example: `"Global"` | | `data.profile.regionTilt` | `string \| null` | For Global funds, the largest region when it is 50–80% of the portfolio. Example: `"North America"` | | `data.profile.sectorTilt` | `string \| null` | Equity only. Largest sector when ≥30%, otherwise "Broad". A fund with ≥50% in one sector is categorised as a sector fund. Example: `"Technology"` | | `data.profile.valuation` | `"value" \| "blend" \| "growth" \| null` | Equity only, from the portfolio P/E: <15 value, 15–22 blend, >22 growth. Example: `"blend"` | | `data.profile.creditQuality` | `"high" \| "medium" \| "low" \| null` | Bonds and money market, from the credit buckets: AAA–A ≥60% high; investment grade (≥BBB) ≥80% medium; otherwise low. Example: `"high"` | | `data.profile.rateSensitivity` | `"limited" \| "moderate" \| "extensive" \| null` | Bonds, from effective or modified duration: <3.5 years limited, 3.5–6 moderate, >6 extensive. Example: `"moderate"` | | `data.profile.equityShare` | `number \| null` | Allocation funds: equity weight of the asset allocation, in percent. Example: `58` | | `data.profile.rules` | `string` | Version of the rule set that produced the profile. Example: `"fundfacts-profile/1"` | ### Portfolio breakdowns Every breakdown is an array of { label, weight } where weight is a percentage (0–100). Arrays are empty when the panel does not apply to the asset class. | Path | Type | Meaning | |---|---|---| | `data.topHoldings` | `Array<{ name: string; weight: number }>` | Largest positions with their portfolio weight in percent. Example: `[{ "name": "NVIDIA Corp", "weight": 5.1 }, …]` | | `data.geography` | `Array<{ label: string; weight: number }>` | Country exposure. Example: `[{ "label": "United States", "weight": 71.8 }, …]` | | `data.region` | `Array<{ label: string; weight: number }>` | Regional exposure. Mirrors geography when no distinct regional panel exists. Example: `[{ "label": "North America", "weight": 75.4 }, …]` | | `data.sector` | `Array<{ label: string; weight: number }>` | Sector exposure (GICS-style for equity; instrument type for fixed income). Example: `[{ "label": "Technology", "weight": 27.5 }, …]` | | `data.creditQuality` | `Array<{ label: string; weight: number }>` | Credit-rating buckets for fixed income and money-market funds. Example: `[{ "label": "AAA", "weight": 12.3 }, …]` | | `data.assetAllocation` | `Array<{ label: string; weight: number }>` | Asset-class split (equity / bond / cash / other) for allocation funds. Example: `[{ "label": "Equity", "weight": 60.2 }, …]` | | `data.instrument` | `Array<{ label: string; weight: number }>` | Instrument-type breakdown, mostly for money-market funds. Example: `[{ "label": "Commercial paper", "weight": 41 }, …]` | | `data.maturity` | `Array<{ label: string; weight: number }>` | Maturity buckets for fixed income / money-market funds. Example: `[{ "label": "1-3 years", "weight": 35.2 }, …]` | | `data.strategy` | `Array` | Strategy allocation (e.g. for multi-strategy / alternative funds). Usually empty. Example: `[]` | | `data.exposure` | `Array` | Additional exposure panel when available. Usually empty. Example: `[]` | ### Performance Calendar-year, cumulative, indexed and annualised returns. When the issuer publishes a NAV history, the series and the returns are computed from it (monthly, last observation of each month); otherwise the figures are the ones stated in the factsheet. Series can be empty for funds whose issuer does not publish a NAV history. | Path | Type | Meaning | |---|---|---| | `data.calendarReturns.years` | `string[]` | Calendar years covered, oldest first, clamped to the inception year. Partial (current) years are excluded. Example: `["2020", "2021", "2022", "2023", "2024"]` | | `data.calendarReturns.fund` | `number[]` | Fund total return per calendar year, in percent, aligned with years. Example: `[15.9, 22.9, -17.7, 23.8, 18.9]` | | `data.calendarReturns.benchmark` | `(number \| null)[]` | Benchmark return per calendar year, in percent, when the factsheet states it (null otherwise). Example: `[15.9, 21.8, -18.1, 23.8, 18.7]` | | `data.cumulativePerformance` | `Array<{ date: string; fund: number \| null; benchmark: number \| null }>` | Monthly cumulative return series (percent from the first point, rebased at the share-class inception). Example: `[{ "date": "2024-12", "fund": 112.4, "benchmark": null }, …]` | | `data.indexedPerformance.points` | `Array<{ date: string; fund: number \| null; index: number \| null }>` | Same series rebased to 100 at the first point, ready to plot. Example: `[{ "date": "2020-01", "fund": 100, "index": null }, …]` | | `data.indexedPerformance.hasIndex` | `boolean` | Whether the index series is populated. Example: `false` | | `data.annualisedReturns` | `Array<{ label: string; fund: number \| null; index: number \| null }>` | Trailing returns: 1 Year, 3/5/10 Years p.a. and Since Inception. Multi-year figures are annualised. Example: `[{ "label": "1 Year", "fund": 18.2, "index": 18.0 }, { "label": "3 Years p.a.", … }]` | ### Headline metrics & risk statistics Values are strings formatted as published (with % or units) so nothing is lost in conversion. Fields that do not apply to the asset class are empty strings. Volatility, Sharpe ratio and maximum drawdown are computed from the issuer's NAV history over the trailing 3 years (monthly returns, 0% risk-free rate) when it is published, otherwise taken from the factsheet. | Path | Type | Meaning | |---|---|---| | `data.headlineMetrics.ter` | `string` | Total expense ratio / ongoing charge, as stated in the KID or factsheet. Example: `"0.20%"` | | `data.headlineMetrics.aum` | `string` | Fund size (duplicate of keyFacts.aum for factsheet layouts). Example: `"USD 105.4 bn"` | | `data.headlineMetrics.volatility3y` | `string` | 3-year annualised standard deviation of monthly returns. Example: `"14.2%"` | | `data.headlineMetrics.sharpe3y` | `string` | 3-year Sharpe ratio (annualised return over annualised volatility, 0% risk-free rate). Example: `"0.85"` | | `data.headlineMetrics.yieldToMaturity` | `string` | Yield to maturity (fixed income). Example: `"4.1%"` | | `data.headlineMetrics.modifiedDuration` | `string` | Modified duration in years (fixed income). Example: `"6.2"` | | `data.metrics.peRatio` | `string` | Portfolio price/earnings ratio (equity). Example: `"21.4"` | | `data.metrics.incomeYield` | `string` | Distribution / dividend yield. Example: `"1.6%"` | | `data.metrics.volatility3y` | `string` | 3-year volatility (per-asset-class metric slot). Example: `"14.2%"` | | `data.metrics.sharpe3y` | `string` | 3-year Sharpe ratio (per-asset-class metric slot). Example: `"0.85"` | | `data.metrics.yieldToMaturity` | `string` | Yield to maturity (fixed income slot). Example: `"4.1%"` | | `data.metrics.effectiveDuration` | `string` | Effective duration in years. Example: `"5.9"` | | `data.metrics.effectiveMaturity` | `string` | Effective / average maturity in years. Example: `"8.4"` | | `data.metrics.averageRating` | `string` | Average credit rating of the portfolio. Example: `"AA-"` | | `data.metrics.sevenDayYield` | `string` | 7-day yield (money market). Example: `"3.9%"` | | `data.metrics.wam` | `string` | Weighted average maturity in days (money market). Example: `"38"` | | `data.metrics.wal` | `string` | Weighted average life in days (money market). Example: `"61"` | | `data.metrics.maxDrawdown` | `string` | Maximum peak-to-trough drawdown over the trailing 3 years. Example: `"-25.4%"` | | `data.metrics.equityCorrelation` | `string` | Correlation to equities (alternatives / allocation). Example: `"0.62"` | | `data.metrics.equityBondSplit` | `string` | Equity/bond split summary for allocation funds. Example: `"60/40"` | ### Costs & sustainability The PRIIPs KID cost table and the SFDR classification, read from the KID (deterministically where the regulated layout allows) and the factsheet. Strings keep the format printed in the document; empty when the document does not state the line. | Path | Type | Meaning | |---|---|---| | `data.sfdrArticle` | `number \| null` | SFDR classification stated in the fund's documents: 6 (no sustainability objective), 8 (promotes E/S characteristics) or 9 (sustainable investment objective). null when not disclosed. Example: `8` | | `data.costs.entry` | `string` | Entry costs (one-off), as printed in the KID. Example: `"0.00%"` | | `data.costs.exit` | `string` | Exit costs (one-off). Example: `"0.00%"` | | `data.costs.ongoing` | `string` | Management fees and other administrative or operating costs per year (the KID's ongoing-cost line). Falls back to headlineMetrics.ter. Example: `"0.20%"` | | `data.costs.transaction` | `string` | Portfolio transaction costs per year. Example: `"0.05%"` | | `data.costs.performanceFee` | `string` | Performance fee, when the fund charges one. Example: `"0.00%"` | | `data.costs.riy1y` | `string` | Annual cost impact if you exit after one year (reduction in yield). Example: `"0.35%"` | | `data.costs.riyRhp` | `string` | Annual cost impact at the recommended holding period. Example: `"0.25%"` | | `data.costs.recommendedHoldingPeriod` | `string` | Recommended holding period stated in the KID. Example: `"5 years"` | ### Freshness When the underlying figures are dated and when the payload was produced. | Path | Type | Meaning | |---|---|---| | `data.dataAsOf` | `string` | 'As of' date of the underlying figures: the latest NAV observation when a NAV history is used, otherwise the factsheet's reporting date. Example: `"2026-07-31"` | | `data.generatedAt` | `string (ISO 8601)` | Timestamp of the refresh that produced this payload. Payloads are refreshed when older than 24 hours. Example: `"2026-09-02T13:41:07.112Z"` | ## 8. Rules for agents 1. Read the key as described in section 2. Never hard-code it elsewhere, never expose it client-side. 2. Cache by ISIN with `expiresAt`; respect `X-RateLimit-Remaining` and stop before it reaches 0; on 429 wait `Retry-After` seconds. 3. Use timeouts of 300 s on `/funds/{isin}` and show a "loading fund data, first load can take a couple of minutes" state in UIs. 4. Retry only 502 (`upstream_error`), at most twice, with a delay of 60 s or more. Never retry 4xx. 5. Do not scrape https://fundfactsapi.com, fund websites or data vendors as a workaround; the API is the only permitted source and covers what is published. 6. Display `data.dataAsOf` next to any figure, keep the formatted strings as published (do not reformat `"0.20%"` into `0.002` unless the user asks for numeric conversion, then parse with care for `"bn"`, `"m"`, `"%"`). 7. Use `data.profile` (kind, category, riskBand, concentration, regionFocus...) for classification and filtering; it is derived from the payload with published rules and is consistent across funds. 8. Treat `404 fund_not_found` as "not a covered fund", not as a bug: report it to the user and move on. 9. Attribute nothing to third parties; the product to name in UIs and docs is "FundFacts API". ## 9. Reference client Write one small client and reuse it. TypeScript (Node 18+, no dependencies): ```ts // fundfacts.ts const BASE = "https://fundfactsapi.com/api/v1"; const KEY = process.env.FUNDFACTS_API_KEY; export class FundFactsError extends Error { constructor(public status: number, public code: string, message: string, public retryAfter?: number) { super(message); } } const memory = new Map(); export async function getFund(isin: string, opts: { signal?: AbortSignal } = {}) { const id = isin.trim().toUpperCase(); const hit = memory.get(id); if (hit && hit.expiresAt > Date.now()) return hit.body as FundResponse; const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 300_000); opts.signal?.addEventListener("abort", () => ctrl.abort()); try { const res = await fetch(`${BASE}/funds/${id}`, { headers: { Authorization: `Bearer ${KEY}` }, signal: ctrl.signal }); const body = await res.json(); if (!res.ok) { const retryAfter = Number(res.headers.get("Retry-After")) || undefined; throw new FundFactsError(res.status, body?.error?.code ?? "unknown", body?.error?.message ?? res.statusText, retryAfter); } const expiresAt = body.expiresAt ? Date.parse(body.expiresAt) : Date.now() + 3_600_000; memory.set(id, { expiresAt, body }); return body as FundResponse; } finally { clearTimeout(timer); } } export type FundResponse = { isin: string; name: string | null; cached: boolean; generatedAt: string; expiresAt: string; plan: string; quota: { limit: number | null; remaining: number | null; resetAt: string | null }; data: FundData; }; // FundData is declared in fundfacts.d.ts (shipped with this kit). ``` Python (3.9+, `requests`): ```python import os, time, requests BASE = "https://fundfactsapi.com/api/v1" KEY = os.environ["FUNDFACTS_API_KEY"] def get_fund(isin: str) -> dict: r = requests.get(f"{BASE}/funds/{isin.strip().upper()}", headers={"Authorization": f"Bearer {KEY}"}, timeout=300) if r.status_code == 429: time.sleep(int(r.headers.get("Retry-After", "60"))) return get_fund(isin) body = r.json() if not r.ok: raise RuntimeError(f"{r.status_code} {body['error']['code']}: {body['error']['message']}") return body ``` ## 10. Recipes When the user asks for one of these, follow the outline instead of inventing a design. **Fund comparison table** ("compare these ISINs"): fetch them in one `POST /funds` batch (chunk by the plan's batch size), then render one row per fund with `name`, `data.keyFacts.assetClass`, `data.profile.category`, `data.headlineMetrics.ter`, `data.riskRating`, `data.keyFacts.aum`, `data.keyFacts.distribution`, the 1-year and 3-year entries of `data.annualisedReturns`, `data.headlineMetrics.volatility3y` and `data.metrics.maxDrawdown`. Footnote every row with `data.dataAsOf`. **Portfolio look-through** ("what am I really holding"): call `POST /portfolio` with the ISIN weights; it returns the aggregated `sector`, `geography`, `assetAllocation`, `topHoldings`, the blended TER and weighted SRRI, each with a `coverage` figure. Show `coverage` and the `topHoldings.note` (largest positions only, so aggregated holdings are a lower bound). Do the arithmetic yourself only when the user needs a breakdown the endpoint does not return. **Screener over a universe** ("filter my list by ..."): pre-warm the ISINs with `POST /funds` batches from a background job that respects the quota, store payloads, then filter locally on `profile.*`, `headlineMetrics.ter` (parse the percent), `riskRating`, `keyFacts.aum`. Never filter by calling the API repeatedly. **Factsheet page** ("build a page for this fund"): sections in this order: header (`name`, `isin`, `data.securityType`, `data.keyFacts.currency`), objective (`data.investmentObjective`), key facts, risk indicator as a 1–7 scale with `profile.riskBand`, growth chart from `data.indexedPerformance.points` (plot `index` only when `hasIndex`), calendar bars from `data.calendarReturns`, annualised table, top holdings, sector and country donuts, metrics grid; footer with `dataAsOf`. **Change monitor** ("alert me when fees or risk change"): store the previous payload per ISIN; after each daily refresh compare `headlineMetrics.ter`, `riskRating`, `keyFacts.aum`, `keyFacts.manager`, `benchmarkName` and the first five `topHoldings`; emit one message per changed field with old and new values. **Excel / CSV export**: flatten the envelope with dotted keys (`keyFacts.ter`), one row per ISIN, arrays as `label:weight;label:weight` or as separate sheets (holdings, sector, geography, performance). ## 11. Sample response (trimmed) Arrays are shortened here; a real payload carries the full holdings, breakdowns and series. ```json { "isin": "IE00B4L5Y983", "name": "iShares Core MSCI World UCITS ETF", "cached": true, "generatedAt": "2026-09-02T20:32:58.872Z", "expiresAt": "2026-09-03T20:32:58.872Z", "plan": "pro", "quota": { "limit": 100, "remaining": 99, "resetAt": "2026-09-03T20:32:58.872Z" }, "data": { "investmentObjective": "The Fund seeks to track the performance of an index composed of companies from developed countries.", "securityType": "ETF", "structure": "ETF", "keyFacts": { "assetClass": "Equities", "currency": "USD", "aum": "USD 151.7bn", "inception": "2009-09-25", "subAsset": "Global Equity", "distribution": "Accumulating", "holdings": 1253, "manager": "ETF" }, "riskRating": 6, "managerTenure": "", "topHoldings": [ { "name": "NVIDIA", "weight": 5.48 }, { "name": "APPLE", "weight": 5.24 }, { "name": "MICROSOFT", "weight": 3.88 } ], "geography": [ { "label": "United States", "weight": 72.07 }, { "label": "Japan", "weight": 5.85 }, { "label": "United Kingdom", "weight": 3.49 } ], "sector": [ { "label": "Information Technology", "weight": 29.8 }, { "label": "Financials", "weight": 16.46 }, { "label": "Industrials", "weight": 11.03 } ], "creditQuality": [], "assetAllocation": [ { "label": "Equity", "weight": 99.75 }, { "label": "Cash Collateral and Margins", "weight": 0.02 } ], "region": [ { "label": "United States", "weight": 72.07 }, { "label": "Japan", "weight": 5.85 }, { "label": "United Kingdom", "weight": 3.49 } ], "instrument": [], "maturity": [], "strategy": [], "exposure": [], "calendarReturns": { "years": [ "2020", "2021", "2022", "2023", "2024", "2025" ], "fund": [ 15.9, 21.9, -18, 23.9, 18.7, 21.2 ], "benchmark": [ 15.9, 21.8, -18.1, 23.8, 18.7, 21.1 ] }, "headlineMetrics": { "yieldToMaturity": "", "modifiedDuration": "", "ter": "0.20%", "aum": "USD 151.7bn", "volatility3y": "11.8%", "sharpe3y": "1.68" }, "metrics": { "peRatio": "", "incomeYield": "", "volatility3y": "11.8%", "sharpe3y": "1.68", "yieldToMaturity": "", "effectiveDuration": "", "effectiveMaturity": "", "averageRating": "", "sevenDayYield": "", "wam": "", "wal": "", "maxDrawdown": "-16.5%", "equityCorrelation": "", "equityBondSplit": "100 / 0" }, "cumulativePerformance": [ { "date": "2026-07", "fund": 479.5776, "benchmark": 303.2269 }, { "date": "2026-08", "fund": 494.48, "benchmark": 313.6179 }, { "date": "2026-09", "fund": 490.3719, "benchmark": 310.7434 } ], "indexedPerformance": { "points": [ { "date": "2026-07", "fund": 579.58, "index": 403.23 }, { "date": "2026-08", "fund": 594.48, "index": 413.62 }, { "date": "2026-09", "fund": 590.37, "index": 410.74 } ], "hasIndex": true }, "annualisedReturns": [ { "label": "1 Year", "fund": 19.57, "index": 19.55 }, { "label": "3 Years p.a.", "fund": 19.82, "index": 19.78 }, { "label": "5 Years p.a.", "fund": 11.05, "index": 10.98 }, { "label": "10 Years p.a.", "fund": 12.97, "index": 12.9 }, { "label": "Since Inception", "fund": 11.12, "index": null } ], "benchmarkName": "MSCI World Index (Net)", "dataAsOf": "2026-09-01", "generatedAt": "2026-09-02T20:32:58.871Z", "profile": { "kind": "equity", "category": "Global Equity", "riskBand": "high", "concentration": "diversified", "regionFocus": "Global", "regionTilt": "United States", "sectorTilt": "Broad", "valuation": null, "creditQuality": null, "rateSensitivity": null, "equityShare": null, "rules": "fundfacts-profile/1" } } } ``` ## 12. Plans | Plan | Price | Credits / month | Burst / min | Batch size | Overage | AI kit | |---|---|---|---|---|---|---| | Free | $0 | 150 | 10 | 1 | none | no | | Starter | $9/month | 1,500 | 30 | 10 | $0.015/request, capped at $18 | yes | | Pro | $49/month | 9,000 | 120 | 50 | $0.015/request, capped at $98 | yes | | Scale | $249/month | 60,000 | 600 | 200 | $0.015/request, capped at $498 | yes | | Enterprise | $0.01 / request | metered | 1200 | 1000 | $0.01/request | yes | Credits reset on the first of each month (UTC). One credit per ISIN answered by `/funds/{isin}`, `POST /funds`, `/portfolio` and `/overlap`; `/search`, `/me` and the demo endpoint are free. Sign up at https://fundfactsapi.com/signup; pricing at https://fundfactsapi.com/pricing. ## 13. MCP server FundFacts API exposes a remote Model Context Protocol server over Streamable HTTP at `https://fundfactsapi.com/api/mcp`. Authenticate with the same API key as the REST API (`Authorization: Bearer ffk_...`). Tools: `get_fund` (ISIN → factsheet), `search_funds` (name → ISINs), `compare_funds` (side-by-side table for several ISINs), `analyze_portfolio` (weighted look-through) and `fund_overlap` (shared holdings). Credits are charged exactly as for the REST endpoints. Claude Desktop / Cursor / Windsurf configuration: ```json { "mcpServers": { "fundfacts": { "url": "https://fundfactsapi.com/api/mcp", "headers": { "Authorization": "Bearer ffk_..." } } } } ``` Clients that only speak stdio can use `npx -y mcp-remote https://fundfactsapi.com/api/mcp --header "Authorization: Bearer ffk_..."`. ## 14. Documentation pages - Overview: https://fundfactsapi.com/docs - Quickstart: https://fundfactsapi.com/docs/quickstart - Authentication: https://fundfactsapi.com/docs/authentication - Endpoints: https://fundfactsapi.com/docs/endpoints - Credits & rate limits: https://fundfactsapi.com/docs/credits - Freshness & sourcing: https://fundfactsapi.com/docs/freshness - Errors: https://fundfactsapi.com/docs/errors - Field reference: https://fundfactsapi.com/docs/fields - AI agents & MCP: https://fundfactsapi.com/docs/ai-agents - SCPI: https://fundfactsapi.com/docs/scpi - OpenAPI 3.1: https://fundfactsapi.com/openapi.json (YAML: https://fundfactsapi.com/openapi.yaml) - Index for language models: https://fundfactsapi.com/llms.txt ## 15. Glossary - **Accumulating vs distributing share class** (Acc vs Dist, Accumulation share class, Income share class, Capitalising share class): Accumulating share classes reinvest income inside the fund; distributing share classes pay it out to investors, usually quarterly, semi-annually or annually. → https://fundfactsapi.com/glossary/accumulating-vs-distributing - **Active fund** (Actively managed fund, Active management): An active fund is one whose manager selects holdings at their own discretion, aiming to outperform or manage risk against a benchmark rather than replicate it. → https://fundfactsapi.com/glossary/active-fund - **Assets under management (AUM)** (AUM, Fund size, Net assets, Total net assets): Assets under management is the total market value of the assets a fund holds, net of liabilities, reported either for the whole fund or for a single share class. → https://fundfactsapi.com/glossary/aum - **Benchmark** (Benchmark index, Reference index, Comparator index): A benchmark is the index a fund is measured against: tracked by index funds and ETFs, or used by active funds as a target or reference for relative performance. → https://fundfactsapi.com/glossary/benchmark - **Country exposure** (Geographic exposure, Country allocation, Geographic breakdown, Regional allocation): Country exposure is the share of a fund's portfolio invested in each country, normally assigned by the issuer's country of domicile or listing as classified by the index provider or fund house. → https://fundfactsapi.com/glossary/country-exposure - **Credit quality** (Credit rating breakdown, Rating allocation, Average credit rating): Credit quality describes the creditworthiness of the bonds a fund holds, shown as a breakdown by rating bucket from AAA down to below BBB or as an average rating. → https://fundfactsapi.com/glossary/credit-quality - **CUSIP** (CUSIP number, Committee on Uniform Securities Identification Procedures): A CUSIP is a nine-character identifier for securities issued in the United States and Canada; it forms the middle nine characters of a US ISIN. → https://fundfactsapi.com/glossary/cusip - **Distribution yield** (Dividend yield (fund), Income yield, Trailing 12-month yield, Historic yield): Distribution yield is the income a fund paid out over the past twelve months divided by its current NAV or market price, expressed as a percentage. → https://fundfactsapi.com/glossary/distribution-yield - **Duration (modified / effective)** (Modified duration, Effective duration, Macaulay duration, Interest rate sensitivity): Duration measures how sensitive a bond fund's value is to interest rates: a modified duration of 5 implies roughly a 5% price fall for a one percentage point rise in yields. → https://fundfactsapi.com/glossary/duration - **ETF (exchange-traded fund)** (ETF, Exchange-traded fund, UCITS ETF, Tracker): An ETF is an open-ended fund whose shares trade on a stock exchange during the day, with creations and redemptions by authorised participants keeping the price close to NAV. → https://fundfactsapi.com/glossary/etf - **Fund domicile** (Domicile, Country of domicile, Fund jurisdiction): A fund's domicile is the country in which it is legally established and regulated; for UCITS funds sold across Europe it is most often Ireland or Luxembourg. → https://fundfactsapi.com/glossary/fund-domicile - **Fund factsheet** (Factsheet, Fund fact sheet, Monthly factsheet): A fund factsheet is the short document a fund house publishes, usually monthly, summarising a share class's key facts, holdings, exposures, performance and charges. → https://fundfactsapi.com/glossary/factsheet - **Hedged share class** (Currency-hedged share class, EUR Hedged, Hedged ETF, H share class): A hedged share class uses currency forwards to reduce the effect of exchange-rate moves between the fund's underlying currencies and the currency of the share class. → https://fundfactsapi.com/glossary/hedged-share-class - **Inception date** (Launch date, Fund launch, Share class inception): The inception date is the day a fund or share class was launched and began calculating a NAV; it determines how much performance history and which risk statistics exist. → https://fundfactsapi.com/glossary/inception-date - **Index fund** (Passive fund, Tracker fund, Index tracker, Passive investing): An index fund aims to replicate the return of a specified market index rather than beat it, and can be either a listed ETF or a traditional unlisted fund dealt at NAV. → https://fundfactsapi.com/glossary/index-fund - **ISIN** (International Securities Identification Number, ISO 6166, ISIN code): An ISIN is the 12-character ISO 6166 code that uniquely identifies a security worldwide; each fund share class has its own, which makes it the natural key for fund data. → https://fundfactsapi.com/glossary/isin - **KID (Key Information Document, PRIIPs)** (PRIIPs KID, Key Information Document, KID document): The PRIIPs KID is the standardised three-page pre-contractual document EU retail investors receive, showing a 1–7 summary risk indicator, performance scenarios and costs. → https://fundfactsapi.com/glossary/kid - **KIID (Key Investor Information Document, UCITS)** (UCITS KIID, Key Investor Information Document, KII, KIID document): The UCITS KIID is the two-page document introduced under UCITS IV that presents a fund's objectives, a 1–7 SRRI risk indicator, charges and ten years of calendar-year past performance. → https://fundfactsapi.com/glossary/kiid - **Look-through** (Portfolio look-through, Look-through analysis, Underlying exposure, Fund of funds look-through): Look-through means analysing a fund by its underlying holdings rather than as a single line, so a portfolio of funds can be aggregated by security, sector, country or asset class. → https://fundfactsapi.com/glossary/look-through - **Maximum drawdown** (Max drawdown, MDD, Peak-to-trough loss): Maximum drawdown is the largest peak-to-trough fall in a fund's value over a period, expressed as a negative percentage, before a new high is reached. → https://fundfactsapi.com/glossary/maximum-drawdown - **MiFID II** (Markets in Financial Instruments Directive II, MiFID 2, Directive 2014/65/EU): MiFID II is the EU directive, applied since January 2018, that governs investment services and requires firms distributing funds to disclose costs, define target markets and assess suitability. → https://fundfactsapi.com/glossary/mifid-ii - **NAV (net asset value)** (NAV, Net asset value per share, NAV per unit, Unit price): Net asset value is a fund's assets minus its liabilities; divided by the shares in issue it gives the NAV per share, the price at which unlisted funds are bought and sold. → https://fundfactsapi.com/glossary/nav - **OCF (ongoing charges figure)** (Ongoing charges figure, OCF %): The OCF is the UK label for a fund's ongoing charges: the annual running cost, as a percentage of assets, disclosed in the KIID or KID. → https://fundfactsapi.com/glossary/ocf - **Ongoing charges** (Ongoing costs, Ongoing charges figure): Ongoing charges are the annual recurring costs a UCITS fund deducts from its assets, shown as a percentage in the KIID or KID; the regulatory name for what many call the TER. → https://fundfactsapi.com/glossary/ongoing-charges - **PRIIPs** (PRIIPs Regulation, PRIIPs KID, Packaged Retail and Insurance-based Investment Products): PRIIPs is the EU regulation requiring a standardised three-page KID for retail investment products, including UCITS funds since 2023, with an SRI and cost tables. → https://fundfactsapi.com/glossary/priips - **Prospectus** (Fund prospectus, Offering document, Supplement): The prospectus is a fund's full legal offering document: investment policy, risks, fees, share classes, dealing rules and service providers, approved by the home regulator. → https://fundfactsapi.com/glossary/prospectus - **Replication method (physical, sampled, synthetic)** (Physical replication, Full replication, Optimised sampling, Synthetic replication, Swap-based ETF): The replication method is how an index fund or ETF reproduces its index: holding every constituent, a representative sample, or a swap that delivers the index return. → https://fundfactsapi.com/glossary/replication-method - **Sector exposure** (Sector allocation, Sector breakdown, Sector weights): Sector exposure is the share of a fund's portfolio invested in each industry sector, such as technology, financials or healthcare, usually shown as percentage weights. → https://fundfactsapi.com/glossary/sector-exposure - **SEDOL** (Stock Exchange Daily Official List number, SEDOL code): A SEDOL is a seven-character security identifier issued by the London Stock Exchange for UK and Irish securities; it is embedded in GB and IE ISINs. → https://fundfactsapi.com/glossary/sedol - **SFDR (Articles 6, 8 and 9)** (Sustainable Finance Disclosure Regulation, Article 8 fund, Article 9 fund, Light green / dark green): SFDR is the EU disclosure regulation under which funds are classified as Article 6, 8 or 9 depending on whether and how they promote sustainability characteristics or objectives. → https://fundfactsapi.com/glossary/sfdr - **Share class** (Unit class, Fund class, Share class ISIN): A share class is one version of a fund with its own ISIN, currency, distribution policy, fee level and eligibility rules, all backed by the same underlying portfolio. → https://fundfactsapi.com/glossary/share-class - **Sharpe ratio** (Sharpe, Risk-adjusted return): The Sharpe ratio measures return per unit of risk: a fund's excess return over the risk-free rate divided by the volatility of its returns over the same period. → https://fundfactsapi.com/glossary/sharpe-ratio - **SRI (summary risk indicator, PRIIPs)** (Summary risk indicator, PRIIPs risk indicator, Risk class): The SRI is the 1–7 summary risk indicator shown in a PRIIPs KID; it combines a market risk measure with a credit risk measure and replaced the SRRI for EU UCITS in 2023. → https://fundfactsapi.com/glossary/sri - **SRRI (synthetic risk and reward indicator, UCITS)** (Synthetic risk and reward indicator, UCITS risk indicator, Risk and reward profile): The SRRI is the 1–7 risk score in a UCITS KIID, assigned purely from the fund's five-year annualised volatility using fixed buckets; it preceded the PRIIPs SRI. → https://fundfactsapi.com/glossary/srri - **Swing pricing** (Swung NAV, Anti-dilution levy, Dilution adjustment): Swing pricing adjusts a fund's NAV up or down on days of large net flows so that the trading costs caused by subscribing or redeeming investors are borne by them, not the fund. → https://fundfactsapi.com/glossary/swing-pricing - **TER (total expense ratio)** (Total expense ratio, Expense ratio, TER %): The TER is the industry's long-standing name for a fund's annual running costs as a percentage of assets; in UCITS documents the same figure is disclosed as ongoing charges or OCF. → https://fundfactsapi.com/glossary/ter - **Ticker** (Ticker symbol, Exchange symbol, Trading symbol): A ticker is the short exchange-specific symbol used to trade a listed security; one ETF share class can have several tickers, one per exchange, but only one ISIN. → https://fundfactsapi.com/glossary/ticker - **Top holdings** (Top 10 holdings, Largest positions, Holdings list): Top holdings are a fund's largest positions by portfolio weight, usually the ten biggest, as published in its factsheet or holdings file at a stated date. → https://fundfactsapi.com/glossary/top-holdings - **Tracking difference** (TD, Return difference vs index, Index underperformance): Tracking difference is the gap between a fund's return and its benchmark's return over a period; for index funds it is the real cost of ownership after fees and frictions. → https://fundfactsapi.com/glossary/tracking-difference - **Tracking error** (TE, Active risk, Relative volatility): Tracking error is the annualised standard deviation of the difference between a fund's returns and its benchmark's returns; it measures how consistently the fund follows its index. → https://fundfactsapi.com/glossary/tracking-error - **UCITS** (Undertakings for Collective Investment in Transferable Securities, UCITS fund, UCITS ETF): UCITS is the EU regulatory framework for retail investment funds; a UCITS fund meets rules on diversification, liquidity and disclosure and can be sold across the EU under a passport. → https://fundfactsapi.com/glossary/ucits - **Volatility** (Standard deviation, Annualised volatility, Vol): Volatility is the annualised standard deviation of a fund's returns; it measures how widely returns have swung around their average and underpins the SRRI and Sharpe ratio. → https://fundfactsapi.com/glossary/volatility - **WKN** (Wertpapierkennnummer, German securities code): A WKN (Wertpapierkennnummer) is the six-character German national securities identifier; it is widely used on German retail platforms and embedded in DE ISINs. → https://fundfactsapi.com/glossary/wkn - **Yield to maturity** (YTM, Portfolio yield, Yield to worst, Gross redemption yield): Yield to maturity is the weighted average annualised return a bond fund's holdings would earn if held to maturity at today's prices, before fees; a snapshot of the portfolio's yield. → https://fundfactsapi.com/glossary/yield-to-maturity