---
title: "Automating fund factsheets: from PDF scraping to structured JSON"
description: "Why parsing factsheet PDFs breaks, what a normalised fund data schema looks like, and how wealth managers and fintechs generate client-ready factsheets automatically."
canonical: https://fundfactsapi.com/blog/fund-factsheet-automation
published: 2026-07-22
updated: 2026-07-22
publisher: FundFacts API
---

# 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.

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](https://fundfactsapi.com/) 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](https://fundfactsapi.com/blog/srri-risk-indicator-explained) 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](https://fundfactsapi.com/signup), 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](https://fundfactsapi.com/docs) lists every key; the [comparison-tool tutorial](https://fundfactsapi.com/blog/build-fund-comparison-tool) covers caching and charting.

## About FundFacts API

FundFacts turns any fund or ETF ISIN into one dated factsheet (fees and KID costs, the 1–7 risk indicator, holdings, sector and country exposure, returns) read from the fund house's own documents: as a client-ready PDF under your firm's name for advisers and wealth managers, as JSON through its API for developers, or inside ChatGPT and Claude through its MCP server.

FundFacts works inside ChatGPT and Claude as a connector (a remote MCP server): ask about any fund or ETF by ISIN, ticker or name and the assistant answers from FundFacts' dated figures, fees, risk, holdings, exposure and returns, with fund cards, side-by-side comparisons, holdings overlap and portfolio look-through.

- Free API key, no card: https://fundfactsapi.com/signup
- Documentation: https://fundfactsapi.com/docs (OpenAPI: https://fundfactsapi.com/openapi.json)
- MCP server: https://fundfactsapi.com/api/mcp (set-up for every client: https://fundfactsapi.com/mcp-server)
- Pricing: https://fundfactsapi.com/pricing
- Site index for AI assistants: https://fundfactsapi.com/llms.txt
- Informational data, not investment advice.
