Guide
HTML to PDF in FastAPI
FastAPI is async-first, and the common Python PDF libraries are not. WeasyPrint blocks the event loop; running a headless browser inside an async app introduces process management complexity. PDFPipe is a single async HTTP call that fits the FastAPI model cleanly: your endpoint awaits the render, the event loop never blocks, and there is nothing extra to install.
Basic endpoint
Install httpx for async HTTP (it is already a FastAPI dependency), set your API key as an environment variable, and add a route:
# pip install fastapi httpx uvicorn
import os
import httpx
from fastapi import FastAPI, Response, HTTPException
app = FastAPI()
PDFPIPE_KEY = os.environ["PDFPIPE_API_KEY"]
@app.get("/invoice/{order_id}.pdf")
async def invoice(order_id: str):
html = f"""
<html>
<body style="font-family: system-ui; padding: 40px">
<h1>Invoice #{order_id}</h1>
<p>Thank you for your order.</p>
</body>
</html>
"""
async with httpx.AsyncClient(timeout=60) as client:
r = await client.post(
"https://api.pdfpipe.xyz/v1/pdf",
headers={"Authorization": f"Bearer {PDFPIPE_KEY}"},
json={"html": html, "options": {"format": "A4"}},
)
if not r.is_success:
raise HTTPException(502, r.json().get("detail", "render failed"))
return Response(
content=r.content,
media_type="application/pdf",
headers={"Content-Disposition": f'attachment; filename="invoice-{order_id}.pdf"'},
)With Jinja2 templates
Render the template on your server to fill in order data, customer details, and line items, then pass the complete HTML string to PDFPipe:
# Render a Jinja2 template, then pass the HTML to PDFPipe.
# pip install jinja2
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader("templates"))
@app.get("/report/{report_id}.pdf")
async def report(report_id: str):
data = await get_report_data(report_id) # your data source
template = env.get_template("report.html")
html = template.render(data=data)
async with httpx.AsyncClient(timeout=60) as client:
r = await client.post(
"https://api.pdfpipe.xyz/v1/pdf",
headers={"Authorization": f"Bearer {PDFPIPE_KEY}"},
json={"html": html, "options": {"format": "A4"}},
)
if not r.is_success:
raise HTTPException(502, "render failed")
return Response(
content=r.content,
media_type="application/pdf",
)Background task with stored result
For high-traffic or slow-loading documents, generate the PDF in a background task. Pass store: true to keep it on the CDN and get a URL back that you can email or store in your database:
# Generate in a background task and store the result for later retrieval.
from fastapi import BackgroundTasks
from pydantic import BaseModel
class InvoiceRequest(BaseModel):
order_id: str
customer_email: str
async def render_and_store(order_id: str, html: str):
async with httpx.AsyncClient(timeout=60) as client:
r = await client.post(
"https://api.pdfpipe.xyz/v1/pdf",
headers={"Authorization": f"Bearer {PDFPIPE_KEY}"},
json={
"html": html,
"options": {"format": "A4"},
"store": True,
"filename": f"invoice-{order_id}.pdf",
},
)
if r.is_success:
doc_url = r.headers.get("X-PDFPipe-Document-Url")
# Save doc_url to your database, send by email, etc.
await save_document_url(order_id, doc_url)
@app.post("/invoices", status_code=202)
async def create_invoice(body: InvoiceRequest, background_tasks: BackgroundTasks):
html = build_invoice_html(body.order_id) # your template
background_tasks.add_task(render_and_store, body.order_id, html)
return {"status": "rendering"}Getting an API key
The Hobby plan gives 500 free documents a month with no credit card. Paid plans start at $19 and include longer document retention, higher limits, and email support.
Full API reference on the docs page. Try it now in the live playground with no signup.
Get an API key →