Building a PDF Report Endpoint with FastAPI

FastAPI is a natural fit for data-heavy Python backends, and sooner or later those backends need to produce PDFs: monthly reports, invoices, audit exports, dashboards printed on demand. The naive approach is to spin up Playwright or wkhtmltopdf in a worker process. That works until the Chrome subprocess leaks memory, blocks the event loop, or inflates your container image by 400 MB.
This tutorial shows a cleaner path: a FastAPI endpoint that renders HTML to PDF via the Rendex Python SDK, runs on your existing async stack, and keeps browser infrastructure off your server entirely.
Prerequisites
- Python 3.10+ with FastAPI installed
- A Rendex API key (free tier gives 100 calls/month, no credit card required)
rendexSDK:pip install rendex fastapi uvicorn
Get your API key at rendex.dev/login.
Step 1: Build an HTML template
The Rendex API renders any HTML string into a PDF. Start with a template function that takes your report data and returns a complete HTML document. Inline CSS keeps things portable.
def render_report_html(title: str, rows: list[dict]) -> str:
row_html = "".join(
f"<tr><td>{r['label']}</td><td>{r['value']}</td></tr>"
for r in rows
)
return f"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: system-ui, sans-serif; margin: 0; padding: 40px; color: #111; }}
h1 {{ font-size: 24px; margin-bottom: 24px; }}
table {{ width: 100%; border-collapse: collapse; }}
th, td {{ padding: 10px 14px; border-bottom: 1px solid #e5e7eb; text-align: left; }}
th {{ background: #f9fafb; font-size: 12px; text-transform: uppercase; color: #6b7280; }}
</style>
</head>
<body>
<h1>{title}</h1>
<table>
<thead><tr><th>Metric</th><th>Value</th></tr></thead>
<tbody>{row_html}</tbody>
</table>
</body>
</html>"""This pattern composes well: swap the template function for Jinja2, pass in a Pydantic model, or pull data from a database query. The rendering step stays the same.
Step 2: Create the FastAPI endpoint
The Rendex SDK is synchronous. Call it from a FastAPI endpoint using run_in_executorso you don't block the async event loop.
import asyncio
import os
from functools import partial
from pathlib import Path
from typing import Annotated
from fastapi import FastAPI, Query
from fastapi.responses import Response
from rendex import Rendex, RendexApiError
from templates import render_report_html
app = FastAPI()
# Get your API key at https://rendex.dev/login
RENDEX_API_KEY = os.environ["RENDEX_API_KEY"]
def _generate_pdf(html: str) -> bytes:
"""Runs synchronously — must be called via run_in_executor."""
with Rendex(RENDEX_API_KEY) as rendex:
result = rendex.screenshot(
"about:blank", # placeholder; html= overrides the URL
html=html,
format="pdf",
pdf_format="A4",
pdf_print_background=True,
pdf_margin={"top": "20mm", "right": "20mm", "bottom": "20mm", "left": "20mm"},
)
return result.image
@app.get("/reports/{report_id}/pdf")
async def export_report_pdf(
report_id: str,
title: Annotated[str, Query()] = "Monthly Report",
) -> Response:
# Replace with your real data fetch
rows = [
{"label": "Total revenue", "value": "$12,400"},
{"label": "New signups", "value": "318"},
{"label": "Churn rate", "value": "2.1%"},
]
html = render_report_html(title, rows)
loop = asyncio.get_event_loop()
pdf_bytes = await loop.run_in_executor(None, partial(_generate_pdf, html))
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={"Content-Disposition": f'attachment; filename="{report_id}-report.pdf"'},
)The key details:
html=htmlpasses raw HTML to the renderer. The SDK sends it as thehtmlfield in the POST body toPOST /v1/screenshot. No public URL needed.pdf_format="A4"sets the page size. Options:A4,Letter,Legal,Tabloid,A3.pdf_margintakes a dict withtop,right,bottom,leftas CSS values ("20mm","0.5in","0", etc.).run_in_executorhands the blocking SDK call off to a thread pool so the FastAPI event loop stays unblocked for other requests.
Step 3: Test with curl
Start the dev server and confirm the endpoint returns a valid PDF:
# start the server
uvicorn main:app --reload
# request a PDF in another terminal
curl "http://localhost:8000/reports/q1/pdf?title=Q1+Results" \
--output q1-report.pdf
# verify it is a valid PDF
file q1-report.pdf
# => q1-report.pdf: PDF document, version 1.4Or call the Rendex REST API directly without the SDK:
curl -X POST https://api.rendex.dev/v1/screenshot \
-H "Authorization: Bearer rdx_your_key" \
-H "Content-Type: application/json" \
-d '{
"html": "<h1>Hello PDF</h1>",
"format": "pdf",
"pdfFormat": "A4",
"pdfPrintBackground": true,
"pdfMargin": {"top": "20mm", "right": "20mm", "bottom": "20mm", "left": "20mm"}
}' --output test.pdf
# Get a key at https://rendex.dev/loginStep 4: Landscape reports and scale control
Wide tables like financial summaries often need landscape orientation or a reduced scale to fit on a single page. Both are single-param changes:
result = rendex.screenshot(
"about:blank",
html=html,
format="pdf",
pdf_format="Letter",
pdf_landscape=True, # rotate to landscape
pdf_scale=0.85, # shrink to 85% to fit wide tables
pdf_print_background=True,
pdf_margin={"top": "15mm", "right": "10mm", "bottom": "15mm", "left": "10mm"},
)pdf_scale accepts values from 0.1 to 2.0. Scaling below 1.0 fits more content per page; above 1.0 enlarges text for printed output.
Troubleshooting
RendexApiError: 401 Unauthorized Your API key is missing or incorrect. Keys start with rdx_. Set RENDEX_API_KEY in your environment before starting the server.
PDF renders blank or cuts off content The HTML template may reference external fonts or images that the renderer cannot reach. Inline all CSS and use base64 data URIs for images, or host assets at a public URL. Avoid relative paths like ./style.css.
TimeoutError or the endpoint hangs The default SDK timeout is 90 seconds. Large HTML documents or very wide tables take longer to render. Pass timeout=120 (seconds) to the Rendex constructor if your reports are complex.
Blocking the event loop in testing If you call _generate_pdf directly in tests, wrap it in asyncio.to_thread() or mock the Rendex client with unittest.mock.MagicMock.
Next steps
This pattern scales to any document type: invoices, compliance exports, printable user profiles. See URL-to-PDF in Python for fetching live URLs instead of rendering HTML strings, and the free URL-to-PDF tool to test captures without any code.
The full API reference documents every PDF parameter including header and footer templates, custom paper dimensions, and async batch capture for generating hundreds of reports in parallel.
Start with 100 free calls/month. Get a free API key and have your first PDF endpoint running in under ten minutes.
Continue Reading
Apr 11, 2026
How to Generate PDFs from URLs in Python (2026)
Aug 25, 2026
How to Convert URLs to PDF in Node.js
Jun 9, 2026
Generate Invoices from an HTML Template and JSON
Solution
Server-Side Report Rendering API — HTML Charts & Tables to PNG at Scale
Solution
URL to PDF API — Convert Any Webpage to PDF
Documentation
Sdks
Documentation
Api Reference
Free Tool
Url To Pdf Tool