Education

Generate Professional PDF Documents from Templates Using an API

Stop writing PDF generation code. Learn how to use the DocForge API to generate invoices, reports, contracts, and certificates from templates in seconds.

Generate Professional PDF Documents from Templates Using an API

Generating PDF documents programmatically sounds simple until you actually try it. You start with a basic PDF library, spend two hours fighting with fonts and table alignment, realize you need pixel-perfect headers and footers, add a charting library, and suddenly you're maintaining 500 lines of layout code that breaks every time the requirements change.

There's a better way. In 2026, document generation is a solved problem if you separate concerns: keep your data in your app, keep your templates in a document engine, and connect them via API.

This guide shows how to use DocForge to generate professional-quality PDFs — invoices, reports, certificates, contracts — without touching PDF internals.

Why Template-Based PDF Generation

The traditional approach (build PDFs in code) has fundamental problems:

  1. Designers can't touch it — layout lives in Python/Node code, not a design tool
  2. Brittle layout math — pixel positions break when content changes length
  3. No preview without running code — slow iteration loop
  4. Hard to maintain — every format change requires a code change and deploy

Template-based generation flips this: a designer maintains the template in a visual editor, you supply data via API, and the engine does the merging. Your code doesn't need to know anything about how the PDF looks.

Setup

pip install requests jinja2
export APIVULT_API_KEY="YOUR_API_KEY"

Approach 1: Using Built-In Templates

DocForge ships with pre-built templates for common document types. The fastest path to a working document:

import os
import requests
 
API_KEY = os.environ["APIVULT_API_KEY"]
BASE_URL = "https://apivult.com/api/v1/docforge"
 
def generate_invoice(data: dict) -> bytes:
    """Generate an invoice PDF using the built-in invoice template."""
    response = requests.post(
        f"{BASE_URL}/generate",
        headers={"X-API-Key": API_KEY},
        json={
            "template": "invoice-standard",  # Built-in template
            "data": data,
            "options": {
                "format": "A4",
                "orientation": "portrait",
                "margin": {"top": "20mm", "bottom": "20mm", "left": "15mm", "right": "15mm"},
            },
        },
    )
    response.raise_for_status()
    return response.content  # Raw PDF bytes
 
# Invoice data
invoice_data = {
    "invoice_number": "INV-2026-0042",
    "issue_date": "2026-03-30",
    "due_date": "2026-04-29",
    "from": {
        "company": "Acme Corp",
        "address": "123 Main St, San Francisco, CA 94105",
        "email": "[email protected]",
        "tax_id": "US-12-3456789",
    },
    "to": {
        "company": "Client Inc.",
        "address": "456 Market St, New York, NY 10001",
        "contact": "Jane Smith",
    },
    "line_items": [
        {
            "description": "API Integration Consulting",
            "quantity": 40,
            "unit": "hours",
            "unit_price": 150.00,
        },
        {
            "description": "Documentation & Testing",
            "quantity": 8,
            "unit": "hours",
            "unit_price": 120.00,
        },
    ],
    "tax_rate": 0.0875,  # 8.75% sales tax
    "currency": "USD",
    "notes": "Payment via wire transfer or ACH. Bank details on file.",
    "logo_url": "https://acme.example.com/logo.png",  # Optional
}
 
pdf_bytes = generate_invoice(invoice_data)
 
with open("invoice_INV-2026-0042.pdf", "wb") as f:
    f.write(pdf_bytes)
 
print(f"Invoice generated: {len(pdf_bytes)} bytes")

Available built-in templates: invoice-standard, invoice-modern, report-executive, certificate-completion, contract-simple, letterhead-formal, nda-basic.

Approach 2: Custom HTML Templates

For full control over design, provide your own HTML/CSS template:

def generate_from_html_template(html_template: str, data: dict) -> bytes:
    """Generate PDF from a custom HTML template with data substitution."""
    response = requests.post(
        f"{BASE_URL}/generate/html",
        headers={"X-API-Key": API_KEY},
        json={
            "html": html_template,
            "data": data,
            "options": {
                "format": "A4",
                "print_background": True,
                "wait_for_fonts": True,
            },
        },
    )
    response.raise_for_status()
    return response.content
 
# Template uses Jinja2-compatible syntax: {{ variable }}, {% for %}, etc.
certificate_template = """
<!DOCTYPE html>
<html>
<head>
  <style>
    @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Open+Sans&display=swap');
    body {
      font-family: 'Open Sans', sans-serif;
      margin: 0;
      padding: 40px 60px;
      background: white;
    }
    .certificate {
      border: 8px double #c9a84c;
      padding: 60px;
      text-align: center;
      min-height: 600px;
      display: flex;
      flex-direction: column;
      justify-content: center;
    }
    h1 { font-family: 'Playfair Display'; font-size: 48px; color: #2c3e50; }
    .recipient { font-size: 36px; color: #c9a84c; font-family: 'Playfair Display'; }
    .course { font-size: 24px; margin: 20px 0; }
    .date { font-size: 16px; color: #666; }
    .signatures { display: flex; justify-content: space-around; margin-top: 60px; }
    .sig-line { border-top: 2px solid #333; width: 200px; padding-top: 8px; font-size: 14px; }
  </style>
</head>
<body>
  <div class="certificate">
    <h1>Certificate of Completion</h1>
    <p style="font-size: 18px;">This certifies that</p>
    <div class="recipient">{{ recipient_name }}</div>
    <p class="course">has successfully completed</p>
    <p style="font-size: 28px; font-weight: bold;">{{ course_name }}</p>
    <p class="date">{{ completion_date }} &nbsp;|&nbsp; {{ hours }} hours</p>
    <div class="signatures">
      <div>
        <div class="sig-line">{{ instructor_name }}</div>
        <div style="font-size: 12px; color: #666;">Instructor</div>
      </div>
      <div>
        <div class="sig-line">{{ director_name }}</div>
        <div style="font-size: 12px; color: #666;">Program Director</div>
      </div>
    </div>
  </div>
</body>
</html>
"""
 
cert_data = {
    "recipient_name": "Alexandra Chen",
    "course_name": "Advanced API Security",
    "completion_date": "March 30, 2026",
    "hours": 32,
    "instructor_name": "Dr. Michael Torres",
    "director_name": "Sarah Johnson",
}
 
pdf = generate_from_html_template(certificate_template, cert_data)
with open("certificate.pdf", "wb") as f:
    f.write(pdf)

Approach 3: Batch Document Generation

Generate hundreds of documents in a single API call:

def generate_batch(template: str, records: list[dict]) -> list[dict]:
    """
    Generate multiple documents in one request.
    Returns a list of {id, url} for downloading each PDF.
    """
    response = requests.post(
        f"{BASE_URL}/batch",
        headers={"X-API-Key": API_KEY},
        json={
            "template": template,
            "records": records,  # Each dict becomes one document
            "output": {
                "naming": "{{invoice_number}}",  # Use field value as filename
                "merge": False,  # True to combine into one PDF
            },
        },
    )
    response.raise_for_status()
    result = response.json()
 
    print(f"Batch complete: {result['generated']} documents in {result['duration_ms']}ms")
    return result["documents"]
 
# Generate 50 invoices at once
invoices = [build_invoice_data(order) for order in orders]
docs = generate_batch("invoice-standard", invoices)
 
# Download each
import urllib.request
for doc in docs:
    urllib.request.urlretrieve(doc["url"], f"invoices/{doc['id']}.pdf")

Approach 4: Integrating with FastAPI

Add document generation to your API:

from fastapi import FastAPI, HTTPException
from fastapi.responses import Response
from pydantic import BaseModel
 
app = FastAPI()
 
class InvoiceRequest(BaseModel):
    invoice_number: str
    customer_id: str
    line_items: list[dict]
    due_date: str
 
@app.post("/documents/invoice", response_class=Response)
async def create_invoice(request: InvoiceRequest):
    # Fetch customer data
    customer = await get_customer(request.customer_id)
 
    # Assemble template data
    data = {
        "invoice_number": request.invoice_number,
        "issue_date": datetime.now().strftime("%Y-%m-%d"),
        "due_date": request.due_date,
        "from": YOUR_COMPANY_INFO,
        "to": {
            "company": customer.company_name,
            "address": customer.billing_address,
            "contact": customer.contact_name,
        },
        "line_items": request.line_items,
        "currency": customer.currency,
    }
 
    # Generate PDF
    try:
        pdf_bytes = generate_invoice(data)
    except requests.HTTPError as e:
        raise HTTPException(status_code=500, detail=f"PDF generation failed: {e}")
 
    # Save to storage
    url = await upload_to_s3(pdf_bytes, f"invoices/{request.invoice_number}.pdf")
 
    return Response(
        content=pdf_bytes,
        media_type="application/pdf",
        headers={"Content-Disposition": f"attachment; filename={request.invoice_number}.pdf"},
    )

Common Template Patterns

Dynamic tables with totals:

{
  "line_items": [
    {"description": "Item A", "qty": 5, "price": 100.00},
    {"description": "Item B", "qty": 2, "price": 250.00}
  ],
  "subtotal": 900.00,
  "tax_rate": 0.08,
  "tax_amount": 72.00,
  "total": 972.00
}

Conditional sections: Use {% if show_payment_instructions %}...{% endif %} in HTML templates to include sections only when needed.

Page breaks: Insert <div style="page-break-after: always;"></div> in HTML templates to force a new page.

Performance

OperationLatencyThroughput
Single document (< 5 pages)~1.2s50/min
Single document (5-20 pages)~2-4s20/min
Batch (100 documents)~15-30s

For high-volume workloads (thousands of documents/day), use the batch endpoint and async callbacks.

Next Steps

Document generation is one of those problems that seems trivial until it isn't. Once you have a working pipeline:

  • Connect to your CRM to auto-generate quotes and proposals
  • Build an automated invoicing workflow triggered on payment events
  • Generate compliance reports on a schedule
  • Let customers self-serve document downloads from your portal

Sign up for APIVult — your first 50 documents are free, no credit card required.