Should I embed fund factsheets in a vector store or call an API at query time?
Why fund figures should be fetched by ISIN at answer time rather than embedded from PDFs, and how to combine an API tool with retrieval over your own documents.Updated 12 September 2026 · by FundFacts APIShort answer
Call the API at query time. Fund figures are tabular, change monthly and must carry an as-of date; chunked PDF embeddings lose the table structure, mix old and new versions and return the wrong share class. Keep retrieval for your own unstructured documents (research notes, client files) and add a tool that fetches the ISIN's current JSON from GET https://fundfactsapi.com/api/v1/funds/{isin}, so numbers in the answer are exact and dated.
Why embeddings are the wrong tool for factsheet figures
A factsheet is mostly tables: holdings, sector weights, calendar returns, cost lines. Chunking it for a vector store breaks rows from headers, so "5.48" ends up near "NVIDIA" by luck. Fund houses publish a new version every month, so the store fills with near-duplicate chunks from different dates and the retriever cannot tell which is current. And the same fund has several share classes with separate documents whose text differs by one letter in the name; similarity search returns the wrong one routinely.
The result is confident answers with stale or mismatched numbers, which in a financial product is worse than no answer.
What works: structured data as a tool
Give the model a function that takes an ISIN and returns the current JSON. The payload is already structured (headlineMetrics.ter, topHoldings[], sector[], calendarReturns, dataAsOf), so the model reads exact values with a date and never has to parse a PDF.
python# pip install fundfactsfrom fundfacts import FundFactsff = FundFacts() # reads FUNDFACTS_API_KEYdef fund_context(isin: str) -> str:"""Compact, dated context block for the prompt."""f = ff.get_fund(isin)d = f["data"]lines = [f"{f['name']} ({isin}), data as of {d['dataAsOf']}",f"Category {d['profile']['category']}; risk {d['riskRating']}/7; TER {d['headlineMetrics']['ter']}; size {d['keyFacts']['aum']}","Top holdings: " + ", ".join(f"{h['name']} {h['weight']}%" for h in d["topHoldings"][:10]),"Sectors: " + ", ".join(f"{s['label']} {s['weight']}%" for s in d["sector"][:6]),"Calendar returns: " + ", ".join(f"{y} {r}%" for y, r in zip(d["calendarReturns"]["years"], d["calendarReturns"]["fund"]))]return "\n".join(lines)
Inject that block into the prompt (or expose it as a tool and let the model call it), and keep the vector store for the material that genuinely is unstructured: your research, meeting notes, client correspondence.
Where documents still matter
For a fund the API does not cover, POST /extract (Pro and above) turns any KID or factsheet PDF into the same data shape, so your application handles both cases with one code path instead of falling back to raw text.
Guardrails worth adding
- Instruct the model to quote
dataAsOfwith every figure and to say "not disclosed" for empty fields. - Validate ISINs before calling (12 characters, check digit) so the model cannot invent one that happens to parse.
- Cache by ISIN until
expiresAt; the figures do not change more than once a day.
The fund data for AI agents and LLMs guide covers evaluation and prompt patterns in more depth.
Verify it yourself
The demo endpoint returns the live payload for a fund that is already in the store, without a key. Everything on this page can be checked against it.bashcurl -s https://fundfactsapi.com/api/v1/demo/funds/IE00B4L5Y983 | jq '{name, asOf: .data.dataAsOf, ter: .data.headlineMetrics.ter, risk: .data.riskRating, top: .data.topHoldings[:3]}'