PDF Report Generation API for Charts and Decks

Generate PDF reports, board decks and listing sheets from HTML: wait for charts to draw, landscape and custom page sizes, one-page fit, with a Python job.

By Simon Chiu · Updated

A report is the document people print, forward to the board and read on paper. It is also the hardest kind of PDF to automate, because most of its content is drawn by JavaScript: charts arrive after the page has loaded, and a renderer that prints too soon produces empty boxes. Serve the report as a page in your app, make the page announce when its charts are drawn, and ask RenderScreenshot for a PDF that waits for that signal, on landscape or custom-size paper. This page covers weekly reports, board decks and real estate listing sheets, which share the same three problems: readiness, page size and page fit.

It is part of our use cases. The general PDF options are on the HTML to PDF API page.

Five requests for report-style documents

Each body goes to POST https://api.renderscreenshot.com/v1/screenshot. The url is a report page in your app.

Document Key options Body
Weekly KPI report, A4 portrait, several pages Wait for charts, footer page numbers {"url": "https://app.example.com/reports/weekly/2026-42", "wait": {"for_selector": "body[data-charts-ready]", "timeout": 45}, "output": {"format": "pdf"}, "pdf": {"paper": "a4", "margin": "14mm", "margin_bottom": "20mm", "header": "<span></span>", "footer": "<div style='font:9px Helvetica,Arial,sans-serif;width:100%;text-align:center'>Page <span class='pageNumber'></span> of <span class='totalPages'></span></div>"}}
Wide dashboard export Landscape A4 {"url": "...", "wait": {"for_selector": "body[data-charts-ready]"}, "output": {"format": "pdf"}, "pdf": {"paper": "a4", "landscape": true, "margin": "10mm"}}
Board deck, one slide per page 16:9 custom page, no margin {"url": "https://app.example.com/board/2026-q3/print", "wait": {"for_selector": "body[data-charts-ready]"}, "output": {"format": "pdf"}, "pdf": {"width": "13.333in", "height": "7.5in", "margin": "0"}}
Listing sheet, exactly one page Letter, scaled to fit {"url": "https://homes.example.com/listing/4471/sheet", "wait": {"until": "networkidle"}, "output": {"format": "pdf"}, "pdf": {"paper": "letter", "margin": "0.4in", "fit_one_page": true}}
Cover preview for an email Page 1 only same body as the KPI report, plus "page_ranges": "1" inside pdf

All options are in the PDF parameters and wait options references. We did not call the hosted API for this page; the chart and page-size behavior below comes from our own test in Chromium, described in the next section.

Readiness: wait for the charts, not for a timer

We built a test report with a CSS grid of KPI cards and two bar charts that a script draws 600 ms after the load event, as a chart library does after it fetches data. Printed in Chromium (Playwright 1.64.1) at the load event, the PDF had the KPI cards and two empty chart frames. Waiting for a data-charts-ready attribute that the script sets after drawing gave both charts, every time.

The page has to say when it is done. With most chart libraries that means: turn animation off for print, count the charts, and set a flag when the last one has rendered.

<script>
  // Chart.js example: animation off, flag set when every chart has drawn.
  const charts = document.querySelectorAll('canvas[data-chart]');
  let drawn = 0;
  charts.forEach((canvas) => {
    const config = JSON.parse(canvas.dataset.chart);
    config.options = { ...config.options, animation: false };
    config.plugins = [{ id: 'ready', afterRender: (chart) => {
      if (!chart.$ready) { chart.$ready = true; if (++drawn === charts.length) document.body.dataset.chartsReady = 'true'; }
    } }];
    new Chart(canvas, config);
  });
</script>

We ran this script with Chart.js 4.5.1 and two charts in Chromium; the flag appeared once both had drawn. Then wait.for_selector set to body[data-charts-ready] holds the print until the flag appears. If it never appears, the request fails at wait.timeout (30 seconds by default, 60 at most) instead of returning a half-drawn report. Read the wait strategies guide to choose between a selector, networkidle and a fixed delay.

Page size: landscape, decks and one-page sheets

Landscape for wide tables and dashboards. pdf.landscape: true turns the paper; it does not change the viewport. Lay the report out with relative widths so it uses the wider page.

Custom sizes for decks. A board deck reads best as 16:9 pages. Set pdf.width and pdf.height (they override paper), set the margin to 0, and in the template:

.slide { width: 13.333in; height: 7.5in; overflow: hidden; break-after: page; }
.slide:last-child { break-after: auto; }

Without break-after, slides run together and a chart is cut between two pages. The CSS paged media entry explains @page and the break properties.

One page for listing sheets. A property listing sheet, a one-page summary or a dashboard handout must be exactly one sheet. pdf.fit_one_page: true scales the content to fit. It works well when the content is a little too long; when it is twice too long, the text becomes too small to read, and you should cut content instead. Listing photos are often lazy-loaded, so use loading="eager" in the print template or wait for networkidle.

How it works

flowchart LR
    S[Scheduler: Monday 07:00] --> J[Report job]
    J -->|POST /v1/screenshot: url, wait.for_selector, pdf options, network.auth| R[RenderScreenshot]
    R -->|loads| P[Your report page, charts set data-charts-ready]
    R --> O[PDF bytes + X-Cache-URL]
    O --> J
    J --> E[Email or Slack with the PDF attached]
    J --> B[Your bucket: reports/2026-42.pdf]

The report page usually needs a login. Give the renderer a token that can read one report and expires in minutes. network.auth and network.headers go only to the report's origin, never to the chart CDN or font host (network options). The renderer cannot reach private network addresses, so the page must have a public hostname. The dashboard snapshots use case shows the same access pattern for a Grafana image in Slack.

A weekly report job in Python

The SDKs cover common PDF options, but not fit_one_page, per-side margins or page_ranges. This job sends the JSON body directly, so every option is available.

"""weekly_report.py: render last week's report to PDF. Python 3.9+, pip install requests"""
import os
import sys

import requests

API = "https://api.renderscreenshot.com/v1/screenshot"
FOOTER = ("<div style='font:9px Helvetica,Arial,sans-serif;width:100%;text-align:center'>"
          "Page <span class='pageNumber'></span> of <span class='totalPages'></span></div>")


def render_report(week: str, report_token: str) -> bytes:
    body = {
        "url": f"https://app.example.com/reports/weekly/{week}",
        "network": {"auth": {"type": "bearer", "token": report_token}},
        "wait": {"for_selector": "body[data-charts-ready]", "timeout": 45},
        "output": {"format": "pdf", "filename": f"weekly-report-{week}"},
        "pdf": {"paper": "a4", "landscape": True, "margin": "12mm", "margin_bottom": "18mm",
                "header": "<span></span>", "footer": FOOTER},
    }
    resp = requests.post(API, json=body, timeout=90,
                         headers={"Authorization": f"Bearer {os.environ['RS_API_KEY']}"})
    if resp.status_code != 200:
        error = resp.json().get("error", {})
        raise RuntimeError(f"{resp.status_code} {error.get('code')}: {error.get('message')}")
    return resp.content


if __name__ == "__main__":
    week = sys.argv[1]  # for example 2026-42
    pdf = render_report(week, os.environ["REPORT_READ_TOKEN"])
    with open(f"weekly-report-{week}.pdf", "wb") as f:
        f.write(pdf)

A missing flag returns the timeout error, which is retryable; a wrong selector string never matches and fails the same way, so test it once in your browser's console with document.querySelector.

Settings, and why

Setting Value Reason
wait.for_selector body[data-charts-ready] Print after the charts, not after load
wait.timeout 45 Slow reports fail clearly instead of printing half-drawn
pdf.landscape true for wide content Tables and dashboards keep their columns
pdf.width / pdf.height 13.333in / 7.5in 16:9 deck pages
pdf.fit_one_page true for one-sheet documents Small overflows no longer add a second page
pdf.page_ranges "1" A cover preview without a second render path

What it costs

One report is one credit, whatever the page count. A product that sends a weekly report to 400 customer accounts renders about 1,700 reports a month, which fits the Starter plan (2,500 credits for $10). Failed renders, including timeouts, are not charged. Current numbers are on the pricing page.

Get a free API key and point it at one of your reports.

Frequently asked questions

Why are the charts missing from my PDF report?
The page printed before the chart library drew. In our test, a report whose charts appear 600 ms after load printed with empty chart boxes when we printed at the load event. Make the page set a flag when every chart is drawn and pass that selector in wait.for_selector.
How do I make each slide of a board deck its own PDF page?
Give the PDF a 16:9 page with pdf.width 13.333in and pdf.height 7.5in, set pdf.margin to 0, size each slide to the full page and add break-after: page to every slide but the last.
When should I use fit_one_page?
For documents that must be one sheet and are only a little too long, such as a listing sheet or a one-page summary. It scales everything down, so a long report becomes very small text. Shorten the content instead when the scale would go far below 100%.
Can the renderer open a report that needs a login?
Yes, if the report has a public hostname. Send a short-lived token with network.auth or network.headers; it goes only to the report's origin. Private network addresses are blocked.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.