# Endpoints

GET /funds/{isin} and /history, POST /funds (batch), GET /search (name, ticker, WKN), /resolve, /coverage, POST /portfolio, GET /overlap, /watchlist, /changes, webhooks, POST /factsheets, POST /reports, GET /me, the demo endpoint.

HTML version: https://fundfactsapi.com/docs/endpoints · Updated 2026-10-25

## 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 store, false when it was refreshed for this request. |
| `stale` | `boolean` | Present, and true, only when the payload is older than 24 hours (the response came from the store at once); it is refreshed in the background, usually within a minute: ask again then for the new payload. Never older than 7 days: past that the fund is read before answering. |
| `generatedAt` | `string (ISO 8601)` | When the payload was produced. |
| `expiresAt` | `string (ISO 8601)` | When the payload is due for a refresh (24 hours after generatedAt), not when it disappears. A request after that date is answered at once from the store with stale: true and the payload is refreshed in the background; ?refresh=true reads first (at most once an hour per ISIN). |
| `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 } }` | Quota state after this request: the monthly request allowance (limit), requests left, requests 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 | Cost |
|---|---|---|
| `GET https://fundfactsapi.com/api/v1/funds/{isin}/history?fields=ter,costs&since=2026-09-15&changesOnly=1` | Fund history (Pro+): TER, KID cost lines, fund size, risk indicator and top-10 holdings per as-of date from the snapshots kept since 15 Sept 2026 (a payload without a stated date is dated the day it was generated; newest payload of a date wins; oldest first; ≤500 points). `changesOnly=1` drops repeats (formatting ignored, holdings by name). 403 `plan_required` below Pro; 404 `fund_not_found` (not counted) for a fund never loaded. | 1 request |
| `POST https://fundfactsapi.com/api/v1/funds` body `{ "isins": ["…"], "wait": true }` | Batch lookup; `results[]` carries `{ input, isin, status, name, cached, stale?, generatedAt, expiresAt, data }` with `status` = `ok` / `not_found` / `pending` / `invalid` / `error`. ISINs in the store answer at once (`stale: true` past 24 hours; a few per call, up to 20, are refreshed in the background, the rest on later calls). `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, ISIN-prefix, ticker or WKN search: loaded funds plus listed ETFs not loaded yet. `results[]`: `{ isin, name, issuer, currency, assetClass, shareClass, income, category, dataAsOf, url, coverage: "covered" \| "unknown", listing: { ticker, mic, exchange, currency } \| null, lookup: string \| null }`. Covered first. | free |
| `GET https://fundfactsapi.com/api/v1/coverage?isins=A,B` | Whether up to 100 ISINs are covered, from the store: `covered` (`cached`), `not_covered` with `reason`, the queue's `recheck` promise and `upload` (whether the plan can make the sheet now from the fund's own KID), or `unknown`. | free |
| `GET` / `POST` / `DELETE https://fundfactsapi.com/api/v1/watchlist` body `{ "isins": ["…"] }` | The funds the account follows (a dashboard fund list named "API watchlist", alerts on): `{ list, count, limit, watched, funds: [{ isin, name, generatedAt }], added?, alreadyWatched?, removed?, notWatched? }`. 10 funds on Starter, 100 on Pro, 500 on Scale across every list with alerts on (400 `watchlist_full` past it); Free has none (403 `plan_required`). Webhooks, `/changes` and e-mail alerts cover these funds on Starter and Pro; Scale covers every fund. MCP: `watch_fund`, `unwatch_fund`, `my_watched_funds`. | 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[]`, `stale[]` (funds used as stored past their 24 hours, refreshed in the background; call `POST /funds` first if every input must be within 24 hours). Starter+. | 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 }], sharedNames, disclosed }`; `overlap` is null (not 0) when a fund publishes names without weights (listed in `namesOnly`); `stale[]` lists funds used as stored past their 24 hours. Starter+. | 1 per ISIN |
| `GET https://fundfactsapi.com/api/v1/changes?since=<ISO>&isins=A,B` | Change feed: tracked fields that moved between refreshes (`fund.changed`) and as-of date moves (`fund.refreshed`); page with `nextSince`. Starter and Pro read their watched funds (`scope: "watchlist"`, `?isins=` cut to the watched ones, the rest in `unwatched`); Scale reads every fund (`scope: "all"`). Webhooks (`/webhooks`) push the same events, signed with `X-FundFacts-Signature`; on Starter and Pro a webhook without `isins` follows every watched fund and named `isins` must be watched (400 `watchlist_required`). | 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", "logo" (https URL, Pro+), "template" (id or name, or "none"; Pro+: the account's default template applies when omitted), "font", "header", "palette", "secondary", "hide" (`"costs"` leaves the KID cost lines card out), "contact", "disclaimer" (Pro+) }` | One-page factsheet rendered with @fundfactsapi/widgets: key facts, the KID's cost lines as stated, holdings, exposures, risk, returns, metrics, and a footer linking the issuer's own documents (`data.documents`). 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`; `GET /factsheets/templates` lists the saved templates (designed in the dashboard). | 1 (HTML) / 2 (PDF); list, get, share links free |
| `POST https://fundfactsapi.com/api/v1/reports` body `{ "holdings": [{ "isin", "weight"? }], "title", "preparedFor", "note", "template", "hide" }` | Client portfolio report (Pro+, trial included): cover, allocation, a Costs & charges page from the funds' KIDs (per-fund cost lines, portfolio-weighted costs, cost of 10,000 over one year and the recommended holding period; advice fees stated separately by the adviser; `hide: ["costs"]` leaves it out), look-through, overlap, one page per fund, an Issuer documents table and the methodology. Returns `{ id, title, holdings, found, missing, urls: { html, pdf }, requests, quota }`; a fund the store cannot load is filled from the KID the account uploaded in the dashboard (not through the API yet). | 1 per fund found |
| `GET https://fundfactsapi.com/api/v1/me` | `{ email, plan: { id, name, requests, 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 |

Full reference: https://fundfactsapi.com/llms-full.txt
