Docs/ Endpoints

Endpoints

GET /funds/{isin}, POST /funds (batch), GET /search, POST /portfolio, GET /overlap, GET /me, the demo endpoint.
EndpointWhat it doesCredits
GET /funds/{isin}Structured factsheet for one ISIN.1
POST /fundsBatch lookup, up to the plan's batch size per call.1 per ISIN answered
GET /searchName, issuer or ISIN-prefix search over loaded funds.free
POST /portfolioWeighted look-through across funds.1 per position
GET /overlapHoldings overlap between two or more funds.1 per ISIN
GET /changesChange feed: tracked fields that moved between refreshes (Scale+).free
GET /webhooksSigned POSTs when a fund changes; manage endpoints (Scale+).free
GET /exportEvery matching fund as NDJSON, optionally gzipped (Scale+).1 per call
POST /extractTurn any KID / KIID / factsheet PDF into the same JSON shape (Pro+).5 per document
GET /mePlan and credits for the calling key.free
GET /demo/funds/{isin}Keyless read of an already-loaded fund (30/min per IP).none, no key
GET /scpi/{idOrIsin}Structured data for one French SCPI (slug, ISIN or name).1
GET /scpiName, manager or ISIN search over the SCPI universe.free

GET /funds/{isin}

Returns the structured factsheet for one ISIN. The path segment is validated (12 characters, country prefix, checksum) and normalised to upper case. One credit.

Response envelope

FieldTypeDescription
isinstring
Canonical upper-case ISIN that was resolved.
namestring | null
Fund / share-class name.
cachedboolean
true when the payload was served from the 24-hour cache, false when it was refreshed for this request.
generatedAtstring (ISO 8601)
When the payload was produced.
expiresAtstring (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.
dataobject
The structured factsheet. See the field reference below.
200 OK — IE00B4L5Y983 (performance series shortened)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
},
{
"name": "AMAZON.COM INC",
"weight": 2.71
},
{
"name": "ALPHABET CLASS A",
"weight": 2.16
},
{
"name": "BROADCOM INC",
"weight": 1.83
},
{
"name": "ALPHABET CLASS C",
"weight": 1.7
},
{
"name": "META PLATFORMS CLASS A",
"weight": 1.39
},
{
"name": "MICRON TECHNOLOGY",
"weight": 1.16
},
{
"name": "TESLA INC",
"weight": 1.12
}
],
"geography": [
{
"label": "United States",
"weight": 72.07
},
{
"label": "Japan",
"weight": 5.85
},
{
"label": "United Kingdom",
"weight": 3.49
},
{
"label": "Canada",
"weight": 3.4
},
{
"label": "France",
"weight": 2.37
},
{
"label": "Switzerland",
"weight": 2.23
},
{
"label": "Germany",
"weight": 2.16
},
{
"label": "Australia",
"weight": 1.59
},
{
"label": "Netherlands",
"weight": 1.38
},
{
"label": "Spain",
"weight": 0.98
}
],
"sector": [
{
"label": "Information Technology",
"weight": 29.8
},
{
"label": "Financials",
"weight": 16.46
},
{
"label": "Industrials",
"weight": 11.03
},
{
"label": "Health Care",
"weight": 9.34
},
{
"label": "Consumer Discretionary",
"weight": 8.72
},
{
"label": "Communication",
"weight": 7.94
},
{
"label": "Consumer Staples",
"weight": 4.92
},
{
"label": "Energy",
"weight": 4.18
},
{
"label": "Materials",
"weight": 3.39
},
{
"label": "Utilities",
"weight": 2.38
}
],
"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
},
{
"label": "Canada",
"weight": 3.4
},
{
"label": "France",
"weight": 2.37
},
{
"label": "Switzerland",
"weight": 2.23
},
{
"label": "Germany",
"weight": 2.16
},
{
"label": "Australia",
"weight": 1.59
},
{
"label": "Netherlands",
"weight": 1.38
},
{
"label": "Spain",
"weight": 0.98
}
],
"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-04",
"fund": 455.4965,
"benchmark": 286.4748
},
{
"date": "2026-05",
"fund": 480.7259,
"benchmark": 304.0676
},
{
"date": "2026-06",
"fund": 476.5422,
"benchmark": 301.1682
},
{
"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-04",
"fund": 555.5,
"index": 386.47
},
{
"date": "2026-05",
"fund": 580.73,
"index": 404.07
},
{
"date": "2026-06",
"fund": 576.54,
"index": 401.17
},
{
"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"
}
}
}

The complete data shape is in the field reference.

POST /funds — batch

Send up to the plan's batch size (Free 1, Starter 10, Pro 50, Scale 200, Enterprise 1,000) in one call. Funds already in the 24-hour store are answered immediately; cold ones are loaded a few at a time within the request budget, and anything left is returned as pending and warmed in the background — re-send those ISINs a minute later. One credit per ISIN with status ok or not_found; pending and invalid are free.

Request bodyjson
{ "isins": ["IE00B4L5Y983", "IE00B3RBWM25", "not-an-isin"], "wait": true }
Response (data shortened)json
{
"count": 3,
"results": [
{ "input": "IE00B4L5Y983", "isin": "IE00B4L5Y983", "status": "ok", "name": "iShares Core MSCI World UCITS ETF USD (Acc)", "cached": true, "generatedAt": "…", "expiresAt": "…", "data": { … } },
{ "input": "IE00B3RBWM25", "isin": "IE00B3RBWM25", "status": "pending", "name": null, "cached": false, "generatedAt": null, "expiresAt": null, "data": null },
{ "input": "not-an-isin", "isin": null, "status": "invalid", "name": null, "cached": false, "generatedAt": null, "expiresAt": null, "data": null }
],
"pending": ["IE00B3RBWM25"],
"plan": "pro",
"quota": { "limit": 9000, "remaining": 8711, "used": 289, "overageUsed": 0, "resetAt": "2026-10-01T00:00:00.000Z", "burst": { "limit": 120, "remaining": 118 } }
}

wait: false answers from the store only and returns cold ISINs as pending at once — use it from a request handler that must respond quickly, then poll.

?q= a fund name, issuer, or the first characters of an ISIN; &limit= up to 50. Searches the funds already loaded into the store (it grows with every request and with the nightly refresh), so a brand-new fund appears once someone has requested it by ISIN. Free; counts towards the per-minute burst limit.

curl "https://fundfactsapi.com/api/v1/search?q=msci+world&limit=5" -H "Authorization: Bearer ffk_..."
json
{ "query": "msci world", "count": 5, "results": [
{ "isin": "IE00B4L5Y983", "name": "iShares Core MSCI World UCITS ETF USD (Acc)", "issuer": "iShares", "currency": "USD", "assetClass": "Equity", "shareClass": "USD Acc", "income": "Accumulating", "category": "Global Equity", "dataAsOf": "2026-08-31", "url": null }
], "plan": "free", "quota": { … } }

POST /portfolio — look-through

Post positions with weights (any scale; they are normalised to 100). The response aggregates each fund's breakdowns by its portfolio weight: asset allocation, sector, country, region, credit quality, currency split and combined top holdings, plus the blended TER, the weighted SRRI and a coverage figure on every block (the share of the portfolio whose funds disclose that breakdown). One credit per position answered.

Request bodyjson
{ "positions": [ { "isin": "IE00B4L5Y983", "weight": 60 }, { "isin": "IE00B3RBWM25", "weight": 25 }, { "isin": "LU1681043599", "weight": 15 } ] }
Response (shortened)json
{
"positions": [ { "isin": "IE00B4L5Y983", "name": "…", "weight": 60, "covered": true, "ter": 0.2, "riskRating": 6, "category": "Global Equity", "kind": "equity" }, … ],
"coverage": 100,
"fees": { "weightedTer": 0.186, "coverage": 100, "annualCostPer10k": 18.6 },
"risk": { "weightedSrri": 5.4, "band": "high", "coverage": 100 },
"kinds": [ { "label": "equity", "weight": 85 }, { "label": "fixedIncome", "weight": 15 } ],
"assetAllocation": { "items": [ { "label": "Equity", "weight": 85 }, { "label": "Bond", "weight": 15 } ], "coverage": 100 },
"sector": { "items": [ { "label": "Information Technology", "weight": 23.4 }, … ], "coverage": 85 },
"geography": { "items": [ { "label": "United States", "weight": 58.1 }, … ], "coverage": 100 },
"topHoldings": { "items": [ { "label": "NVIDIA", "weight": 3.9 }, … ], "coverage": 100, "note": "Funds disclose their largest positions only, so aggregated holdings are a lower bound." },
"pending": [], "invalid": [], "rules": "fundfacts-portfolio/1", "generatedAt": "…", "plan": "pro", "quota": { … }
}

GET /overlap

?isins=A,B,C — for every pair, the overlap is the sum over shared holdings of the smaller of the two weights, matched on the issuer name (so “Apple Inc” and “APPLE INC.” match). Funds that disclose only their top ten show a lower bound; disclosed tells you how much of each portfolio was visible. One credit per ISIN answered.

curl "https://fundfactsapi.com/api/v1/overlap?isins=IE00B4L5Y983,IE00B3RBWM25" -H "Authorization: Bearer ffk_..."
json
{ "funds": [ { "isin": "IE00B4L5Y983", "name": "…", "holdings": 1355 }, { "isin": "IE00B3RBWM25", "name": "…", "holdings": 3600 } ],
"pairs": [ { "a": "IE00B4L5Y983", "b": "IE00B3RBWM25", "overlap": 84.2, "sharedCount": 1290, "shared": [ { "label": "NVIDIA", "a": 5.48, "b": 4.9 }, … ], "disclosed": { "a": 99.8, "b": 99.6 } } ],
"pending": [], "notFound": [], "note": "…", "rules": "fundfacts-portfolio/1", "plan": "pro", "quota": { … } }

GET /changes — change feed

Every refresh is compared with the payload it replaces. When one of the tracked fields moves — headlineMetrics.ter, riskRating, keyFacts.aum, keyFacts.manager, keyFacts.distribution, keyFacts.holdings, benchmarkName, investmentObjective or the top-5 topHoldings — a fund.changed row is written; when the as-of date moves, a fund.refreshed row. Poll with ?since= (ISO timestamp; default the last 7 days), optionally &isins= and &event=; use nextSince for the next call. Scale and Enterprise; free.

curl "https://fundfactsapi.com/api/v1/changes?since=2026-09-01T00:00:00Z&isins=IE00B4L5Y983" -H "Authorization: Bearer ffk_..."
json
{ "since": "2026-09-01T00:00:00.000Z", "count": 2, "hasMore": false, "nextSince": "2026-09-08T03:12:44.000Z",
"changes": [
{ "id": 412, "isin": "IE00B4L5Y983", "name": "…", "event": "fund.changed", "field": "headlineMetrics.ter", "old": "0.20%", "new": "0.19%", "dataAsOf": "2026-08-31", "changedAt": "2026-09-08T03:12:44.000Z" },
{ "id": 413, "isin": "IE00B4L5Y983", "name": "…", "event": "fund.refreshed", "field": "dataAsOf", "old": "2026-07-31", "new": "2026-08-31", "dataAsOf": "2026-08-31", "changedAt": "2026-09-08T03:12:44.000Z" }
], "plan": "scale", "quota": { … } }

A fund only changes when it is refreshed, so the feed reflects the ISINs your account (and everyone else) requests plus the scheduled universe; a fund nobody has loaded for weeks has no entries.

Webhooks

Instead of polling, register an https endpoint and receive a signed POST for every fund.changed / fund.refreshed event, for every fund or for a list of ISINs. Manage endpoints from the dashboard or the API: GET /webhooks, POST /webhooks { "url", "events", "isins" } (returns the signing secret once), POST /webhooks/{id} { "action": "test" | "enable" | "disable" }, DELETE /webhooks/{id}. Scale and Enterprise.

Delivery bodyjson
{ "id": "evt_9f2c…", "event": "fund.changed", "createdAt": "2026-09-08T03:12:45.000Z", "isin": "IE00B4L5Y983", "name": "…",
"changes": [ { "field": "headlineMetrics.ter", "old": "0.20%", "new": "0.19%" } ], "dataAsOf": "2026-08-31", "generatedAt": "2026-09-08T03:12:44.000Z",
"url": "https://fundfactsapi.com/api/v1/funds/IE00B4L5Y983" }

Verify the X-FundFacts-Signature: t=<unix>,v1=<hex> header: v1 is HMAC-SHA256 over `${t}.${rawBody}` with your secret; reject timestamps older than five minutes. Answer 2xx within 10 seconds; anything else is retried up to six times with back-off (1 min → 24 h), after which the endpoint is disabled.

Node.js verificationts
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret: string, rawBody: string, header: string) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

GET /export — bulk NDJSON

Streams every fund in the store that matches ?issuer=, &assetClass=, &currency=, &since= (refreshed after this date), one { isin, name, generatedAt, expiresAt, data } envelope per line. Gzipped when the client accepts it (force with &gzip=1 or off with &gzip=0). One credit per call regardless of size. Scale and Enterprise.

curl "https://fundfactsapi.com/api/v1/export?issuer=iShares&gzip=1" -H "Authorization: Bearer ffk_..." -o ishares.ndjson.gz

POST /extract — any document to JSON

The extraction that powers the API, on your own documents: send a PRIIPs KID, a UCITS KIID or a factsheet and get the same data shape back — for funds whose issuer is not covered, private funds, insurers' unit-linked funds, or a document pile from a due-diligence pack. The regulated KID layout is read deterministically (risk indicator, cost table, SFDR article, holding period); the rest is read by the document model. Five credits per document. Pro and up.

curl -X POST https://fundfactsapi.com/api/v1/extract -H "Authorization: Bearer ffk_..." -F "file=@kid.pdf" -F "isin=LU0123456789" -F "kind=kid"
# or by URL / raw text
curl -X POST https://fundfactsapi.com/api/v1/extract -H "Authorization: Bearer ffk_..." -H "Content-Type: application/json" -d '{"url": "https://example.com/documents/factsheet.pdf"}'
json
{ "document": { "label": "kid.pdf", "kind": "kid", "pages": 3, "chars": 9120 }, "isin": "LU0123456789", "name": "…", "fieldsFilled": 14, "credits": 5, "data": { … }, "plan": "pro", "quota": { … } }

Scanned PDFs without a text layer are not supported. Files up to 8 MB; URLs are fetched respecting the publisher's robots.txt.

GET /me

Plan and credit state for the key used; free. Call it once at startup to confirm the key works, not on every request.

{
"email": "you@company.com",
"plan": {
"id": "pro",
"name": "Pro",
"credits": 9000,
"burstPerMinute": 120,
"batchMax": 50,
"overageUsd": 0.015,
"quota": "9,000 credits / month"
},
"quota": {
"limit": 9000,
"remaining": 8711,
"used": 289,
"overageUsed": 0,
"resetAt": "2026-10-01T00:00:00.000Z",
"burst": {
"limit": 120,
"remaining": 120
}
},
"usage": {
"thisMonth": 289,
"monthStart": "2026-09-01T00:00:00.000Z"
}
}

GET /demo/funds/{isin}

No key needed. Serves a fund that is already in the store (the same payload as the authenticated endpoint, without plan and quota), 30 requests per minute per IP. It never loads anything: an ISIN nobody has requested yet returns 404 not_cached. Use it to inspect the payload shape before signing up, or from the free tools.

curl https://fundfactsapi.com/api/v1/demo/funds/IE00B4L5Y983

Response headers

X-RateLimit-Limit (credits per month), X-RateLimit-Remaining, X-RateLimit-Reset (unix seconds of the monthly reset), X-Burst-Limit and X-Burst-Remaining (per minute) on every authenticated response; Retry-After on 429. See credits & rate limits.