Building a PDF Report Endpoint with FastAPI

Rendex Team··6 min read
fastapipythonpdftutorial
FastAPI endpoint code for fastapi pdf generation: _generate_pdf function calling Rendex Python SDK with pdf_format A4 and run_in_executor for async support

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)
  • rendex SDK: 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.

templates.py
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.

main.py
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=html passes raw HTML to the renderer. The SDK sends it as the html field in the POST body to POST /v1/screenshot. No public URL needed.
  • pdf_format="A4" sets the page size. Options: A4, Letter, Legal, Tabloid, A3.
  • pdf_margin takes a dict with top, right, bottom, left as CSS values ("20mm", "0.5in", "0", etc.).
  • run_in_executor hands 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:

test.sh
# 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.4

Or call the Rendex REST API directly without the SDK:

direct-api.sh
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/login

Step 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:

landscape_report.py
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.

Try Rendex Free

100 screenshots/month. No credit card required.

Get API Key