If you're building web applications in Python, you're likely using Django, FastAPI, or Flask. All three frameworks integrate cleanly with a PDF generation API to dynamically create invoices, certificates, reports, and more.
This guide walks you through integrating FUNBREW PDF with Django, FastAPI, and Flask, covering synchronous and async processing, template-based PDF generation, Blueprint-based project structure, security best practices, and testing with pytest.
For basic Python API usage, see the language quickstart guide. For error handling patterns, check the error handling guide.
Prerequisites
Get Your API Key
Create a free account and generate an API key from the dashboard.
# Add to your .env file
FUNBREW_PDF_API_KEY="sk-your-api-key"
For secure key management, see the security guide.
Install Common Dependencies
pip install httpx python-dotenv
httpx supports both sync and async HTTP calls, making it ideal for both Django and FastAPI.
Django Integration
Project Structure
myproject/
├── myproject/
│ ├── settings.py
│ └── urls.py
├── pdf_app/
│ ├── views.py
│ ├── urls.py
│ ├── services.py
│ └── templates/
│ └── pdf_app/
│ └── invoice.html
└── manage.py
settings.py — API Configuration
Load the API key from environment variables. Never hardcode credentials.
# myproject/settings.py
import os
from dotenv import load_dotenv
load_dotenv()
FUNBREW_PDF_API_KEY = os.getenv("FUNBREW_PDF_API_KEY")
FUNBREW_PDF_API_URL = "https://api.pdf.funbrew.cloud/v1/pdf/from-html"
services.py — PDF Generation Service
Separate business logic from views for reusability and testability.
# pdf_app/services.py
import httpx
from django.conf import settings
class PDFGenerationError(Exception):
"""Raised when PDF generation fails."""
pass
def generate_pdf(html: str, options: dict | None = None) -> bytes:
"""Generate a PDF from HTML and return the binary content."""
payload = {
"html": html,
"format": "A4",
"engine": "quality",
}
if options:
payload.update(options)
response = httpx.post(
settings.FUNBREW_PDF_API_URL,
json=payload,
headers={
"Authorization": f"Bearer {settings.FUNBREW_PDF_API_KEY}",
"Content-Type": "application/json",
},
timeout=60.0,
)
if response.status_code != 200:
raise PDFGenerationError(
f"PDF generation failed: {response.status_code} - {response.text}"
)
return response.content
views.py — PDF Endpoint
# pdf_app/views.py
from django.http import HttpResponse, JsonResponse
from django.template.loader import render_to_string
from django.views.decorators.http import require_POST
from .services import generate_pdf, PDFGenerationError
@require_POST
def generate_invoice_pdf(request, invoice_id):
"""Generate an invoice PDF and return it as an HTTP response."""
# In production, fetch invoice data from your database
invoice_data = {
"invoice_id": invoice_id,
"company": "Acme Corp",
"items": [
{"name": "PDF API Pro Plan", "quantity": 1, "price": 49.80},
{"name": "Extra API Calls (500)", "quantity": 1, "price": 20.00},
],
"total": 69.80,
}
# Render HTML using Django's template engine
html = render_to_string("pdf_app/invoice.html", invoice_data)
try:
pdf_bytes = generate_pdf(html)
except PDFGenerationError as e:
return JsonResponse({"error": str(e)}, status=502)
response = HttpResponse(pdf_bytes, content_type="application/pdf")
response["Content-Disposition"] = f'attachment; filename="invoice-{invoice_id}.pdf"'
return response
urls.py — Routing
# pdf_app/urls.py
from django.urls import path
from . import views
urlpatterns = [
path(
"invoices/<int:invoice_id>/pdf/",
views.generate_invoice_pdf,
name="invoice-pdf",
),
]
# myproject/urls.py
from django.urls import path, include
urlpatterns = [
path("api/", include("pdf_app.urls")),
]
Template-Based PDF Generation
Django's template engine lets you inject data into HTML templates before generating PDFs. For template design tips, see the template engine guide.
<!-- pdf_app/templates/pdf_app/invoice.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body {
font-family: Arial, sans-serif;
padding: 40px;
color: #1a1a1a;
}
.header {
display: flex;
justify-content: space-between;
border-bottom: 3px solid #1a56db;
padding-bottom: 16px;
margin-bottom: 32px;
}
h1 { font-size: 24px; margin: 0; }
table {
width: 100%;
border-collapse: collapse;
margin-top: 24px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e5e7eb;
}
th { background: #f9fafb; font-weight: 600; }
.total {
text-align: right;
font-size: 20px;
font-weight: 700;
margin-top: 24px;
}
</style>
</head>
<body>
<div class="header">
<h1>Invoice</h1>
<div>
<p>Invoice #{{ invoice_id }}</p>
<p>{{ company }}</p>
</div>
</div>
<table>
<thead>
<tr>
<th>Item</th>
<th>Quantity</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.name }}</td>
<td>{{ item.quantity }}</td>
<td>${{ item.price }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="total">Total: ${{ total }}</div>
</body>
</html>
Async PDF Generation with Celery (Django)
For bulk PDF generation or long-running jobs, Celery is the go-to solution. For batch processing patterns, see the batch processing guide.
# pdf_app/tasks.py
from celery import shared_task
from django.core.files.storage import default_storage
from .services import generate_pdf, PDFGenerationError
@shared_task(
bind=True,
max_retries=3,
default_retry_delay=10,
)
def generate_pdf_async(self, html: str, filename: str, options: dict | None = None):
"""Generate a PDF asynchronously and save to storage."""
try:
pdf_bytes = generate_pdf(html, options)
path = f"generated_pdfs/{filename}"
default_storage.save(path, pdf_bytes)
return {"status": "success", "path": path}
except PDFGenerationError as exc:
# Retry with exponential backoff
raise self.retry(exc=exc, countdown=2 ** self.request.retries * 10)
# pdf_app/views.py (add async endpoint)
from django.http import JsonResponse
from django.template.loader import render_to_string
from django.views.decorators.http import require_POST
from .tasks import generate_pdf_async
@require_POST
def generate_invoice_pdf_async(request, invoice_id):
"""Queue an async PDF generation job."""
invoice_data = get_invoice_data(invoice_id) # Fetch from DB
html = render_to_string("pdf_app/invoice.html", invoice_data)
task = generate_pdf_async.delay(
html=html,
filename=f"invoice-{invoice_id}.pdf",
)
return JsonResponse({
"task_id": task.id,
"status": "processing",
"poll_url": f"/api/tasks/{task.id}/status/",
})
FastAPI Integration
Project Structure
myproject/
├── app/
│ ├── main.py
│ ├── config.py
│ ├── services/
│ │ └── pdf_service.py
│ ├── routers/
│ │ └── pdf.py
│ └── templates/
│ └── report.html
├── tests/
│ └── test_pdf.py
└── requirements.txt
config.py — Settings Management
Use Pydantic Settings for type-safe environment variable management.
# app/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
funbrew_pdf_api_key: str
funbrew_pdf_api_url: str = "https://api.pdf.funbrew.cloud/v1/pdf/from-html"
class Config:
env_file = ".env"
settings = Settings()
pdf_service.py — Async PDF Service
Leverage FastAPI's async/await for non-blocking PDF generation.
# app/services/pdf_service.py
import httpx
from app.config import settings
class PDFGenerationError(Exception):
def __init__(self, status_code: int, detail: str):
self.status_code = status_code
self.detail = detail
async def generate_pdf(html: str, options: dict | None = None) -> bytes:
"""Generate a PDF from HTML asynchronously."""
payload = {
"html": html,
"format": "A4",
"engine": "quality",
}
if options:
payload.update(options)
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post(
settings.funbrew_pdf_api_url,
json=payload,
headers={
"Authorization": f"Bearer {settings.funbrew_pdf_api_key}",
"Content-Type": "application/json",
},
)
if response.status_code != 200:
raise PDFGenerationError(
status_code=response.status_code,
detail=response.text,
)
return response.content
routers/pdf.py — Endpoint Definitions
# app/routers/pdf.py
from fastapi import APIRouter, HTTPException
from fastapi.responses import Response
from pydantic import BaseModel
from app.services.pdf_service import generate_pdf, PDFGenerationError
router = APIRouter(prefix="/pdf", tags=["PDF"])
class PDFRequest(BaseModel):
html: str
filename: str = "document.pdf"
format: str = "A4"
engine: str = "quality"
@router.post("/generate")
async def generate_pdf_endpoint(req: PDFRequest):
"""Generate a PDF from HTML and return the binary."""
try:
pdf_bytes = await generate_pdf(
html=req.html,
options={"format": req.format, "engine": req.engine},
)
except PDFGenerationError as e:
raise HTTPException(status_code=e.status_code, detail=e.detail)
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={
"Content-Disposition": f'attachment; filename="{req.filename}"',
},
)
Template-Based PDF Generation with Jinja2
FastAPI natively supports Jinja2 templates for rendering HTML before PDF conversion.
# app/routers/pdf.py (add template-based endpoint)
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import Response
from fastapi.templating import Jinja2Templates
from app.services.pdf_service import generate_pdf, PDFGenerationError
router = APIRouter(prefix="/pdf", tags=["PDF"])
templates = Jinja2Templates(directory="app/templates")
@router.post("/reports/{report_id}")
async def generate_report_pdf(report_id: int, request: Request):
"""Generate a report PDF from a Jinja2 template."""
# In production, fetch data from your database
report_data = {
"request": request,
"report_id": report_id,
"title": "Monthly Report",
"metrics": [
{"name": "PDFs Generated", "value": "1,234"},
{"name": "Avg Response Time", "value": "0.8s"},
{"name": "Success Rate", "value": "99.7%"},
],
}
html = templates.get_template("report.html").render(report_data)
try:
pdf_bytes = await generate_pdf(html)
except PDFGenerationError as e:
raise HTTPException(status_code=502, detail="PDF generation failed")
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={
"Content-Disposition": f'attachment; filename="report-{report_id}.pdf"',
},
)
main.py — Application Setup
# app/main.py
from fastapi import FastAPI
from app.routers import pdf
app = FastAPI(
title="PDF Generation Service",
version="1.0.0",
)
app.include_router(pdf.router, prefix="/api/v1")
Async PDF Generation with BackgroundTasks (FastAPI)
FastAPI's BackgroundTasks provides lightweight async processing without external task queues.
# app/routers/pdf.py (BackgroundTasks version)
import uuid
from pathlib import Path
from fastapi import APIRouter, BackgroundTasks, HTTPException
from pydantic import BaseModel
from app.services.pdf_service import generate_pdf
router = APIRouter(prefix="/pdf", tags=["PDF"])
# In-memory task store (use Redis in production)
task_store: dict[str, dict] = {}
class AsyncPDFRequest(BaseModel):
html: str
filename: str = "document.pdf"
async def _generate_and_save(task_id: str, html: str, filename: str):
"""Generate a PDF in the background and save to disk."""
try:
pdf_bytes = await generate_pdf(html)
output_dir = Path("storage/generated_pdfs")
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / filename
output_path.write_bytes(pdf_bytes)
task_store[task_id] = {"status": "completed", "path": str(output_path)}
except Exception as e:
task_store[task_id] = {"status": "failed", "error": str(e)}
@router.post("/generate/async")
async def generate_pdf_async(req: AsyncPDFRequest, background_tasks: BackgroundTasks):
"""Start an async PDF generation job."""
task_id = str(uuid.uuid4())
task_store[task_id] = {"status": "processing"}
background_tasks.add_task(_generate_and_save, task_id, req.html, req.filename)
return {"task_id": task_id, "status": "processing"}
@router.get("/tasks/{task_id}")
async def get_task_status(task_id: str):
"""Check the status of an async PDF generation task."""
task = task_store.get(task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return task
For webhook-based completion notifications, see the webhook integration guide.
Flask Integration
Project Structure
myproject/
├── app/
│ ├── __init__.py
│ ├── config.py
│ ├── services/
│ │ └── pdf_service.py
│ └── pdf/
│ ├── __init__.py
│ ├── routes.py
│ └── templates/
│ └── invoice.html
├── tests/
│ └── test_pdf.py
└── requirements.txt
config.py — Settings
# app/config.py
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
FUNBREW_PDF_API_KEY = os.getenv("FUNBREW_PDF_API_KEY")
FUNBREW_PDF_API_URL = "https://api.pdf.funbrew.cloud/v1/pdf/from-html"
services/pdf_service.py — PDF Generation Service
Flask works naturally with httpx's synchronous client.
# app/services/pdf_service.py
import httpx
from flask import current_app
class PDFGenerationError(Exception):
"""Raised when PDF generation fails."""
pass
def generate_pdf(html: str, options: dict | None = None) -> bytes:
"""Generate a PDF from HTML and return the binary content."""
payload = {
"html": html,
"format": "A4",
"engine": "quality",
}
if options:
payload.update(options)
response = httpx.post(
current_app.config["FUNBREW_PDF_API_URL"],
json=payload,
headers={
"Authorization": f"Bearer {current_app.config['FUNBREW_PDF_API_KEY']}",
"Content-Type": "application/json",
},
timeout=60.0,
)
if response.status_code != 200:
raise PDFGenerationError(
f"PDF generation failed: {response.status_code} - {response.text}"
)
return response.content
Basic Flask Route for PDF Generation
# app/pdf/routes.py
from flask import Blueprint, render_template, make_response, jsonify
from app.services.pdf_service import generate_pdf, PDFGenerationError
pdf_bp = Blueprint("pdf", __name__, template_folder="templates")
@pdf_bp.route("/invoices/<int:invoice_id>/pdf", methods=["POST"])
def generate_invoice_pdf(invoice_id):
"""Generate an invoice PDF and return it as an HTTP response."""
# In production, fetch invoice data from your database
invoice_data = {
"invoice_id": invoice_id,
"company": "Acme Corp",
"items": [
{"name": "PDF API Pro Plan", "quantity": 1, "price": 49.80},
{"name": "Extra API Calls (500)", "quantity": 1, "price": 20.00},
],
"total": 69.80,
}
# Render HTML using Jinja2 templates (built into Flask)
html = render_template("invoice.html", **invoice_data)
try:
pdf_bytes = generate_pdf(html)
except PDFGenerationError as e:
return jsonify({"error": str(e)}), 502
response = make_response(pdf_bytes)
response.headers["Content-Type"] = "application/pdf"
response.headers["Content-Disposition"] = (
f'attachment; filename="invoice-{invoice_id}.pdf"'
)
return response
Flask-RESTful API Implementation
Flask-RESTful provides a cleaner REST API structure with resource classes.
# app/pdf/resources.py
from flask import request, make_response
from flask_restful import Resource
from app.services.pdf_service import generate_pdf, PDFGenerationError
class PDFResource(Resource):
def post(self):
"""Generate a PDF from HTML and return the binary."""
data = request.get_json()
if not data or "html" not in data:
return {"error": "html field is required"}, 400
html = data["html"]
filename = data.get("filename", "document.pdf")
options = {
"format": data.get("format", "A4"),
"engine": data.get("engine", "quality"),
}
try:
pdf_bytes = generate_pdf(html, options)
except PDFGenerationError as e:
return {"error": str(e)}, 502
response = make_response(pdf_bytes)
response.headers["Content-Type"] = "application/pdf"
response.headers["Content-Disposition"] = (
f'attachment; filename="{filename}"'
)
return response
# app/__init__.py (Flask-RESTful version)
from flask import Flask
from flask_restful import Api
from app.config import Config
from app.pdf.resources import PDFResource
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
api = Api(app)
api.add_resource(PDFResource, "/api/v1/pdf/generate")
return app
Blueprint-Based Project Structure
For larger applications, Blueprints let you split PDF-related routes into self-contained modules.
# app/pdf/__init__.py
from flask import Blueprint
pdf_bp = Blueprint(
"pdf",
__name__,
url_prefix="/api/v1/pdf",
template_folder="templates",
)
from app.pdf import routes # noqa: E402, F401
# app/__init__.py (Blueprint version)
from flask import Flask
from app.config import Config
from app.pdf import pdf_bp
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
app.register_blueprint(pdf_bp)
return app
With this structure, PDF routes are accessible at /api/v1/pdf/invoices/<id>/pdf. You can create separate Blueprints for invoices, certificates, and reports, keeping each concern neatly isolated. For template design tips, see the template engine guide.
Security Considerations
API key management is the most critical security aspect of PDF API integration. For a full treatment, see the security guide. At minimum, follow these rules.
Never Hardcode API Keys
# Bad: hardcoded credentials
API_KEY = "sk-abc123..."
# Good: loaded from environment
API_KEY = os.getenv("FUNBREW_PDF_API_KEY")
Exclude .env from Version Control
# .gitignore
.env
.env.local
.env.production
Keep API Calls Server-Side Only
Always call the PDF API from the server. Never expose your API key in frontend JavaScript. Both Django and FastAPI are server-side frameworks, so this is naturally enforced.
Testing with pytest
Use respx (an httpx mock library) to avoid real API calls in tests.
Django Tests
# tests/test_pdf_service.py
import pytest
import respx
from httpx import Response
from pdf_app.services import generate_pdf, PDFGenerationError
@respx.mock
def test_generate_pdf_success():
"""PDF generation returns binary content on success."""
fake_pdf = b"%PDF-1.4 fake content"
respx.post("https://api.pdf.funbrew.cloud/v1/pdf/from-html").mock(
return_value=Response(200, content=fake_pdf)
)
result = generate_pdf("<h1>Test</h1>")
assert result == fake_pdf
@respx.mock
def test_generate_pdf_api_error():
"""PDFGenerationError is raised on API failure."""
respx.post("https://api.pdf.funbrew.cloud/v1/pdf/from-html").mock(
return_value=Response(500, text="Internal Server Error")
)
with pytest.raises(PDFGenerationError):
generate_pdf("<h1>Test</h1>")
@respx.mock
def test_generate_invoice_pdf_view(client):
"""Invoice PDF endpoint returns a PDF response."""
fake_pdf = b"%PDF-1.4 fake content"
respx.post("https://api.pdf.funbrew.cloud/v1/pdf/from-html").mock(
return_value=Response(200, content=fake_pdf)
)
response = client.post("/api/invoices/1/pdf/")
assert response.status_code == 200
assert response["Content-Type"] == "application/pdf"
FastAPI Tests
# tests/test_pdf.py
import pytest
import respx
from httpx import AsyncClient, Response
from app.main import app
@pytest.mark.anyio
@respx.mock
async def test_generate_pdf_endpoint():
"""PDF generation endpoint returns PDF binary."""
fake_pdf = b"%PDF-1.4 fake content"
respx.post("https://api.pdf.funbrew.cloud/v1/pdf/from-html").mock(
return_value=Response(200, content=fake_pdf)
)
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post(
"/api/v1/pdf/generate",
json={"html": "<h1>Test</h1>", "filename": "test.pdf"},
)
assert response.status_code == 200
assert response.headers["content-type"] == "application/pdf"
assert response.content == fake_pdf
@pytest.mark.anyio
@respx.mock
async def test_generate_pdf_api_failure():
"""Returns error status when external API fails."""
respx.post("https://api.pdf.funbrew.cloud/v1/pdf/from-html").mock(
return_value=Response(500, text="Server Error")
)
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post(
"/api/v1/pdf/generate",
json={"html": "<h1>Test</h1>"},
)
assert response.status_code == 500
@pytest.mark.anyio
@respx.mock
async def test_async_pdf_generation():
"""Async endpoint returns a task ID."""
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post(
"/api/v1/pdf/generate/async",
json={"html": "<h1>Test</h1>", "filename": "async-test.pdf"},
)
assert response.status_code == 200
data = response.json()
assert "task_id" in data
assert data["status"] == "processing"
Django vs FastAPI vs Flask: Which Should You Choose?
| Criteria | Django | FastAPI | Flask |
|---|---|---|---|
| Best for | Admin-heavy apps, existing Django projects | Microservices, high-throughput APIs, real-time | Lightweight APIs, prototypes, simple web apps |
| PDF pattern | Template engine + Celery async | async/await + BackgroundTasks | Blueprint + sync (Celery optional) |
| Performance | Sync by default (async since Django 4.1+) | Native async/await, excels at I/O-bound tasks | Sync by default, simple and predictable |
| Ecosystem | Django REST Framework, admin panel, ORM | Pydantic validation, auto-generated OpenAPI docs | Flask-RESTful, Blueprints, extensive extensions |
| Async processing | Celery + Redis/RabbitMQ | BackgroundTasks (lightweight) or Celery (heavy) | Celery + Redis/RabbitMQ |
| Learning curve | Full-stack framework to learn | API-focused, lightweight | Minimal concepts to start, highly flexible |
Decision guide:
- You have an existing Django app -- integrate into Django views naturally
- Building a new API service -- leverage FastAPI's async/await
- Need a lightweight service or quick prototype -- Flask with Blueprints gets you there fast
- Bulk PDF generation -- use Celery regardless of framework
- Need an admin panel -- Django's built-in admin is hard to beat
For PDF generation use cases like invoices and certificates, see the invoice automation guide and use case gallery. For production deployment tips, check the production guide.
Summary
Integrating FUNBREW PDF API is straightforward with Django, FastAPI, and Flask alike. Send HTML via an HTTP request, get a PDF binary back -- this core pattern stays the same regardless of framework.
- Separate your service layer -- keep PDF generation logic out of views and routers
- Use async processing -- offload heavy PDF jobs to background tasks
- Write tests -- mock the API with respx and cover edge cases
- Secure your keys -- environment variables only, server-side calls only
Try your HTML in the Playground first, then check the documentation for the full API reference when you're ready to build.