PDF4.dev
Learn

Templates

Templates are reusable HTML documents with dynamic variables. Design them once in the dashboard, then render them via API with different data each time.

Creating a template

From the dashboard

Click New template on the dashboard. Choose a starter template (Invoice, Payslip, Certificate, Receipt, Letter) or start blank.

Via API

curl -X POST https://pdf4.dev/api/v1/templates \
  -H "Authorization: Bearer p4_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Monthly invoice",
    "html": "<h1>Invoice #{{invoice_number}}</h1><p>Total: {{total}}</p>",
    "sample_data": {
      "invoice_number": "INV-001",
      "total": "$1,000.00"
    }
  }'

A URL-safe slug is auto-generated from the name (e.g., monthly-invoice). You can use either the id or the slug to reference a template in render requests.

Template structure

Each template has:

FieldDescription
idUnique identifier (tmpl_xxx format)
nameDisplay name
slugURL-safe identifier, unique per user
htmlHTML content with {{variables}}
plain_textAuto-generated plain text version
pdf_formatPage size, margins, and global styles
sample_dataDefault values for preview (not used during API rendering)

Using variables

Insert {{variable_name}} anywhere in your HTML. When rendering, pass a data object to replace them:

Template HTML
<h1>Hello {{customer_name}}</h1>
<p>Your order #{{order_id}} has been confirmed.</p>
<p>Total: {{total}}</p>
Render request data
{
  "data": {
    "customer_name": "Jane Smith",
    "order_id": "ORD-2025-042",
    "total": "$299.00"
  }
}

See Variables for detailed syntax rules.

Sample data

The sample_data field stores default values used in the dashboard preview. These are not used during API rendering: you must always pass data explicitly.

This lets you see a realistic preview while editing without affecting production renders.

Referencing templates

When calling POST /api/v1/render, you can reference a template by:

  • ID: "template_id": "tmpl_a1b2c3d4e5f6"
  • Slug: "template_id": "monthly-invoice"

Both resolve to the same template. Slugs are human-readable and stable unless you rename the template.

Components

Templates can include reusable header, footer, and block components. In the code editor, they appear as custom HTML tags:

<pdf4-header component-id="comp_abc">Company Header</pdf4-header>
<pdf4-block component-id="comp_xyz">Signature Block</pdf4-block>
<pdf4-footer component-id="comp_def">Page Footer</pdf4-footer>

Multi-page repetition

Header and footer components repeat on every page of the rendered PDF. Under the hood, the renderer restructures the document into a table with <thead> (header) and <tfoot> (footer) elements, which Chromium's print engine repeats automatically on each page.

Style inheritance

Components are injected into the same document as the template, so they inherit all global styles set via the pdf_format object: font_family, font_size, color, line_height, and any font loaded via google_fonts_url.

Custom fonts

To use a Google Font (or any @import-compatible CSS URL) across your template and all its components, set google_fonts_url in the format:

{
  "format": {
    "font_family": "Roboto, sans-serif",
    "google_fonts_url": "https://fonts.googleapis.com/css2?family=Roboto&display=swap"
  }
}

The font stylesheet is loaded in <head> so it is available before rendering.

Component gap

Use component_gap to control the spacing between header/footer components and the page content:

{
  "format": {
    "component_gap": "5mm"
  }
}

Use footer_position to control whether the footer sticks to content or pins to the page bottom:

{
  "format": {
    "footer_position": "page-bottom"
  }
}

Two modes: "after-content" (default) places the footer right after the last content row, "page-bottom" pins it to the bottom of every page.

See PDF format for all available format options.

Duplicating templates

Duplicate a template via the dashboard (3-dot menu) or API:

curl -X POST https://pdf4.dev/api/v1/templates/tmpl_xxx/duplicate \
  -H "Authorization: Bearer p4_live_xxx"

The copy gets "(copy)" appended to its name.