Client reporting and branded factsheets from structured fund data
Quarterly packs, one-page fund summaries and branded factsheets all need the same thing: current, sourced fund data laid out consistently. This page shows how to generate PDF and HTML reports from FundFacts API responses, how to handle as-of dates and how to keep a reporting run within quota.The problem
Client reporting is repetitive by design. Every quarter, for every client, for every fund held, someone gathers the latest factsheet, copies the key figures into a template, checks the risk indicator has not moved and re-exports the pack. The work is not intellectually hard, but it is error-prone and it scales with the number of clients.
The failure modes are familiar: a stale TER because the factsheet used was last quarter's, a mismatched share class because the template was cloned, or a performance chart whose end date is not printed anywhere.
What you need from the data
A fund page in a client pack usually shows:
- Name, ISIN, share class, currency and distribution policy
- Investment objective in the fund house's own words
- Risk rating on the 1–7 scale
- Ongoing charges or TER
- Calendar-year returns for the fund and its benchmark
- Cumulative returns for standard periods
- An indexed NAV chart
- Top ten holdings and sector or country breakdowns
- The as-of date for every figure
Consistency across funds matters more than richness. If one page has a five-year chart and another has three years because the source document differed, the client notices.
How it works with FundFacts API
Build a template once, then feed it the data object for each ISIN. The example below renders a single fund section as HTML; a PDF engine such as a headless browser takes it from there.
typescriptexport async function fundSection(isin: string): Promise<string> {const res = await fetch(`https://fundfactsapi.com/api/v1/funds/${isin}`, {headers: { Authorization: `Bearer ${process.env.FUNDFACTS_API_KEY}` },});if (!res.ok) throw new Error(`${res.status} loading ${isin}`);const { name, data } = await res.json();const years = (data.calendarReturns?.years ?? []) as number[];const fund = (data.calendarReturns?.fund ?? []) as (number | null)[];const rows = years.map((y, i) => `<tr><td>${y}</td><td>${fund[i] ?? "n/a"}</td></tr>`).join("");return `<section><h2>${name} <small>${isin}</small></h2><p>${data.investmentObjective ?? ""}</p><dl><dt>Risk rating</dt><dd>${data.riskRating ?? "n/a"} / 7</dd><dt>Ongoing charges</dt><dd>${data.headlineMetrics?.ter ?? "n/a"}%</dd><dt>Fund size</dt><dd>${data.headlineMetrics?.aum ?? "n/a"}</dd></dl><table><tbody>${rows}</tbody></table><footer>Data as of ${data.dataAsOf ?? "unknown"}. Not investment advice.</footer></section>`;}
For the NAV chart, indexedPerformance.points is an array of { date, value } rebased to 100, ready for any charting library. The fund performance data guide explains how the calendar, cumulative and indexed series relate to one another.
Design notes
As-of dates. Print dataAsOf on every fund page and, at the pack level, the range of as-of dates across all funds. If a fund's dataAsOf is older than your reporting cut-off, flag it in the pack rather than silently using it.
Caching. Reporting runs are bursty: hundreds of funds in a night, then nothing for a quarter. Load the distinct ISINs across all clients once, store each response with expiresAt, and render every pack from that store. Data refreshes every 24 hours, so a run that spans two days should re-check expiresAt before rendering.
Quota. The number of requests you need is the number of distinct ISINs, not the number of clients. Count them before the run, compare with X-RateLimit-Limit, and schedule across days if needed. A 429 should pause the job until X-RateLimit-Reset.
Errors. 404 fund_not_found in a live client portfolio is a data-quality event; route it to a human. 502 upstream_error is transient: retry the ISIN at the end of the run.
Branding and disclaimers. The response contains no logos or images, which is deliberate: you apply your own branding. Include the standard wording that past performance is not a reliable indicator of future results and that figures are not investment advice, and state that fund data was read from the documents each fund house publishes.
For the end-to-end approach, see fund factsheet automation and the factsheet glossary entry.