AutomationWealth managementData

Automating fund factsheets: from PDF scraping to structured JSON

Why parsing factsheet PDFs breaks, what a normalised fund data schema looks like, and how wealth managers and fintechs generate client-ready factsheets automatically.Published 22 July 2026 · 3 min read · by FundFacts API

Every wealth manager has a version of the same script: download this month's factsheets, copy the numbers into a spreadsheet, paste them into the client report. Every fintech has tried to automate it with a PDF parser and regretted it. This article explains why factsheet scraping fails, what "structured fund data" should actually mean, and how teams replace the whole pipeline with one API call per ISIN.

Why PDF parsing does not scale

Factsheets are designed for humans. The same fact appears as "Fund size", "Net assets", "AUM" or "Total net assets"; the same table is sometimes two columns and sometimes four; the layout changes with every rebrand. Concretely, a scraper has to cope with:

  • Layout drift. Fund houses redesign factsheets every one to two years, and each redesign silently breaks column detection.
  • Unit ambiguity. "151.7" — millions or billions? USD or the share-class currency? The unit lives in a header three lines away.
  • Locale. 1.234,5 versus 1,234.5; dates as 25/09/2009 or Sep 25, 2009.
  • Missing panels. A bond fund has a credit-quality table where an equity fund has a sector table, in the same position on the page.
  • Timing. Factsheets lag month-end by two to four weeks; the PDF you parse today may describe a portfolio from six weeks ago.

Each of these is solvable in isolation. Together, across a universe of a few hundred funds from thirty fund houses, they turn into a maintenance job for a full-time engineer.

What a normalised schema looks like

The alternative is a single schema, filled for every fund regardless of who issues it or what asset class it holds. That is the contract FundFacts API exposes:

json
{
"keyFacts": { "assetClass": "Equities", "currency": "USD", "aum": "USD 151.7bn", "inception": "Sep 25, 2009", "distribution": "Accumulating", "holdings": 1344 },
"riskRating": 4,
"profile": { "category": "Global Blend Equity", "valuation": "blend", "concentration": "diversified", "regionFocus": "Global", "regionTilt": "North America" },
"topHoldings": [{ "name": "NVIDIA Corp", "weight": 5.43 }],
"sector": [{ "label": "Technology", "weight": 30.5 }],
"geography": [{ "label": "North America", "weight": 75.54 }],
"creditQuality": [],
"maturity": [],
"calendarReturns": { "years": ["2024", "2025"], "fund": [18.7, 21.2] },
"annualisedReturns": [{ "label": "5 Years p.a.", "fund": 11.45, "index": 10.98 }],
"headlineMetrics": { "ter": "0.20%", "volatility3y": "11.9%", "sharpe3y": "1.21" },
"metrics": { "peRatio": "18.7x", "maxDrawdown": "-9.29%" }
}

The design choices that make it usable:

  1. Same keys for every fund. Panels that do not apply are empty, not absent. One template renders equity, bond, multi-asset and money-market funds.
  2. Breakdowns as `{ label, weight }`. One donut component, one bar component.
  3. Formatted values keep their unit. "USD 151.7bn" is unambiguous; 151.7 is not.
  4. Numbers you chart are numbers. Returns and weights are numeric so you can compute without parsing.
  5. Provenance dates. dataAsOf (what the fund published) and generatedAt (when it was refreshed) travel with the payload.

The new pipeline

text
list of client ISINs ──► nightly job: GET /api/v1/funds/{isin} ──► store JSON ──► render template ──► PDF / web factsheet
  • The nightly job keeps every ISIN inside the API's 24-hour freshness window and makes daytime rendering instant.
  • Rendering is a template over the schema — HTML + print CSS is enough, and inline SVG charts print crisply.
  • Because the schema is stable, a redesign of the *fund house's* factsheet changes nothing in *your* factsheet.

A minimal factsheet template

tsx
export function Factsheet({ fund }: { fund: FundResponse }) {
const d = fund.data;
return (
<article className="factsheet">
<header>
<h1>{fund.name}</h1>
<p>{fund.isin} · {d.keyFacts.assetClass} · {d.keyFacts.currency}</p>
</header>
<section className="facts">
<dl>
<dt>Fund size</dt><dd>{d.keyFacts.aum}</dd>
<dt>Ongoing charges</dt><dd>{d.headlineMetrics.ter}</dd>
<dt>Inception</dt><dd>{d.keyFacts.inception}</dd>
<dt>Risk indicator</dt><dd>{d.riskRating} / 7</dd>
</dl>
</section>
<section className="exposure">
<Donut title="Sectors" slices={d.sector} />
<Bars title="Top holdings" items={d.topHoldings.map((h) => ({ label: h.name, weight: h.weight }))} />
</section>
<section className="performance">
<GrowthChart points={d.indexedPerformance.points} />
<ReturnsTable rows={d.annualisedReturns} />
</section>
<footer>Data as of {d.dataAsOf || fund.generatedAt.slice(0, 10)}. Past performance is not a guide to future returns.</footer>
</article>
);
}

Compliance considerations

  • Keep the as-of date and the currency on the page; both are required in most jurisdictions.
  • Show calendar-year returns for at least five years where available, not just cumulative figures.
  • Add the standard past-performance disclaimer and the risk indicator explanation.
  • Store the raw JSON you rendered from. When a client questions a number a year later, you can reproduce the page.

Where to start

Take five ISINs from a real client portfolio, create a free key, and request them from the dashboard. If the data object covers what your current factsheet shows — for most equity, bond and multi-asset funds it does — the rest is a template. The field reference lists every key; the comparison-tool tutorial covers caching and charting.

Try it on your own ISINs

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

Questions this answers

For your project

Where do I get fund data to enrich a wealth management CRM?

Advisor CRMs hold ISINs on client positions but not what they mean. FundFacts API turns each ISIN into name, category, risk, fees, exposures and a one-page factsheet link, so the CRM shows what a client holds and flags what changed. Integration pattern and cost.
Choosing a data source

Is there an API that returns fund factsheets as structured data instead of PDFs?

Yes. FundFacts API reads each fund's factsheet, KID, holdings and NAV files and returns the factsheet as one JSON object per ISIN, refreshed daily. It also converts a factsheet PDF you already have into the same structure. How it works and what it returns.
Alternatives

Should I scrape fund factsheets myself or use an API?

What a factsheet pipeline actually involves (per-issuer adapters, PDF tables, share classes, monthly layout changes, KID parsing) and when building it is worth it versus one API call per ISIN.
Workflows

How do I generate a PDF or HTML factsheet from an ISIN?

POST /api/v1/factsheets with an ISIN returns a one-page factsheet as HTML or PDF, or keyless share and embed links, with your title, theme and accent colour. Every plan, Free included; white label on Pro. Code and options.
Workflows

How do I convert a KID or factsheet PDF to JSON?

POST /api/v1/extract with a PDF (file upload, URL or text) returns the same structured data shape as the ISIN endpoint: key facts, fees, risk indicator, KID cost table, holdings, breakdowns and returns. For funds outside the covered issuers. Code included.