How to Generate Chart.js PDF Reports Programmatically
Automated PDF reports — weekly analytics summaries, monthly client reports, invoice attachments with usage charts — are one of the most common places teams need Chart.js output outside a browser.
If your report is genuinely a full HTML page with complex CSS layout — multi-column text, custom typography, print-specific styling — a headless browser rendering that whole page to PDF is still a reasonable tool for the job; that's a real strength of the browser-based approach and not something worth fighting. Where it gets less efficient is when the only reason you're running a full page-print pipeline is to get one or two chart images onto the page. In that case, decoupling chart rendering from document layout is the cleaner design: generate just the chart as an image (or PDF) via API, then place it into your report using whatever PDF library or template engine already assembles the rest of the document (PDFKit, a templating service, or yes, still Puppeteer for the surrounding layout if that's what you use). You're not replacing your PDF pipeline — you're removing the chart as a dependency of it.
Two ways to get a chart into a PDF
Option 1: Render the chart as a PNG, embed it into your PDF template. This is the most flexible option if your report has other content — tables, text, multiple charts, a cover page — assembled with a PDF library like PDFKit or a templating service.
const response = await fetch("https://www.chart-output.com/api/v1/render", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.CHART_OUTPUT_API_KEY}`
},
body: JSON.stringify({
type: "bar",
data: {
labels: ["Q1", "Q2", "Q3", "Q4"],
datasets: [{ label: "Revenue", data: [42000, 51000, 47500, 63000] }]
},
format: "png"
})
});
const chartImage = Buffer.from(await response.arrayBuffer());
// pass chartImage into PDFKit, pdf-lib, or your PDF template engineIf you want to see what a report-style chart looks like before wiring up the pipeline, open this stacked revenue chart in the playground and switch the preview to the PDF tab. The playground itself renders PNG and SVG — format: "pdf" needs an account — but it's the fastest way to get the composition right before you automate it.
Option 2: Request PDF output directly.
If the chart needs to be the document — a single-chart export, for instance — you can set format: "pdf" and get a print-ready PDF back directly, no intermediate image-to-PDF step. This is a Business-plan feature, since PDF rendering has different resource cost than raster image output.
Why this matters for report pipelines specifically
Reports are usually generated on a schedule — nightly, weekly, end-of-month — often for many accounts at once. That's exactly the workload where a headless-browser approach falls over: launching Chromium per report, per customer, in a batch job is slow and memory-hungry, and if one render hangs it can stall the whole batch.
For high-volume batch report generation, async rendering is the better fit than synchronous requests: submit each chart to POST /api/v1/jobs, get a job ID back immediately (202), and receive a render.complete webhook (or poll GET /api/v1/jobs/:id) when the CDN URL is ready. Your report pipeline doesn't sit there waiting on each chart — it moves on and picks the images up when they're ready.
// submit an async render job
const jobResponse = await fetch("https://www.chart-output.com/api/v1/jobs", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.CHART_OUTPUT_API_KEY}`,
"Idempotency-Key": "report-2026-08-acme-line-chart"
},
body: JSON.stringify({
type: "line",
format: "png",
returnUrl: true,
data: {
labels: ["Jan", "Feb", "Mar"],
datasets: [{ data: [12000, 15000, 18000] }]
}
})
});
const { jobId, pollUrl } = await jobResponse.json();
// render.complete webhook delivers { jobId, resultUrl, ... } when donePass an Idempotency-Key header so retries from your side won't enqueue duplicate jobs — useful if your job queue has its own retry logic. Register webhook endpoints in your dashboard to receive render.complete events.
Deterministic output matters for reports specifically
Reports often get archived, compared month over month, or sent to clients who expect consistency. Chart-Output pins rendering dependencies and runs a regression suite against golden snapshots on every deploy, so the same spec produces the same image regardless of when it's rendered — no surprise visual drift between January's report and February's because a dependency updated underneath you.
For a full walkthrough including brand-kit theming so your reports carry consistent colors and fonts across every chart, see the Charts in PDFs guide.