Invalid Date

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.

  1. Separate your service layer -- keep PDF generation logic out of views and routers
  2. Use async processing -- offload heavy PDF jobs to background tasks
  3. Write tests -- mock the API with respx and cover edge cases
  4. 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.

Powered by FUNBREW PDF