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.
Related
- Invoices and receipts, with table breaks and framework jobs: invoice PDFs.
- Doing it yourself in Python with WeasyPrint, pdfkit or Playwright: HTML to PDF in Python. In Node.js: HTML to PDF in Node.js.
- Wait options in detail: wait parameters.
Get a free API key and point it at one of your reports.