Iteration Layer

Generating PDFs from JSON Instead of HTML: Why Templates Are a Dead End

16 min read Document Generation

The HTML-to-PDF Pipeline Everyone Regrets

You need to generate PDFs. Invoices, contracts, reports, certificates — the kind of documents businesses send to customers, partners, and regulators. The obvious approach is the one every team reaches for first: write an HTML template, inject data with a templating engine, render it to PDF with a headless browser.

It works. For a while.

Then someone adds a table that spans two pages, and the browser splits a row in half. A customer name is long enough to overflow its container, and the layout collapses. A font loads from a CDN that is down, and the PDF renders in Times New Roman. The CSS you wrote to make the document look right on screen produces different margins on paper. A Puppeteer update changes the rendering engine, and every PDF in production shifts by three pixels.

HTML was designed for screens that scroll. PDFs are fixed-size pages that do not. Bridging that gap with CSS @page rules, page-break-inside: avoid, and browser rendering quirks is fighting the medium. Every team that maintains an HTML-to-PDF pipeline long enough ends up with a collection of CSS hacks, browser flags, and “do not touch” comments that nobody wants to own.

There is a better way: define your document as structured JSON, and let a purpose-built renderer produce the PDF. No browser. No CSS. No template language. Deterministic output from deterministic input.

Why HTML-to-PDF Breaks

The problems with HTML-to-PDF are not bugs in any specific tool. They are fundamental mismatches between what HTML is designed for and what PDF generation requires.

Page Boundaries Are an Afterthought

HTML documents flow. Content starts at the top and continues downward until it ends. There is no concept of a “page” in the document model. CSS added @page rules and page-break-* properties, but they are advisory — the browser tries to honor them but makes no guarantees.

A table with 50 rows will split wherever the page boundary falls. A page-break-inside: avoid on a large element will be ignored if the element does not fit on a single page. A footer that should appear at the bottom of every page requires absolute positioning hacks that interact unpredictably with content length.

In a JSON document model, pages are first-class. You define the page size, margins, header, and footer. Content flows into pages with explicit rules. A table that does not fit splits at row boundaries, never mid-row. The layout engine knows about pages from the start, not as a retrofit.

CSS Is Non-Deterministic for Print

CSS rendering depends on the browser engine, the browser version, the installed fonts, the viewport size at render time, and the order in which stylesheets load. Two machines running the same Puppeteer script with the same HTML can produce different PDFs if their Chrome versions differ.

This is fine for web pages, where “close enough” is acceptable and users resize their windows anyway. It is not fine for invoices sent to customers or contracts submitted to regulators. A one-pixel shift in a table border is cosmetic. A table that renders with three columns instead of four because a percentage width rounds differently — that is a production incident.

JSON-to-PDF rendering is deterministic. The same input produces the same output. Every dimension is specified in points. Every font is embedded. There is no cascade, no inheritance, no viewport. What you define is what you get.

Font Loading Is Fragile

HTML templates typically reference fonts via CSS @font-face rules pointing to URLs or local files. If the URL is down, the browser falls back to a system font. If the system font differs between your development machine and your production server, the PDF looks different. If you bundle fonts in a Docker image, you need to manage font installation in your Dockerfile and hope the headless browser discovers them.

In a JSON document model, fonts are embedded directly in the request as base64-encoded buffers. The font file travels with the document definition. There is no resolution step, no fallback, no network dependency. The renderer uses exactly the font you specified, every time.

Headless Browsers Are Heavy Dependencies

Puppeteer, Playwright, wkhtmltopdf — every HTML-to-PDF tool depends on a browser engine. That means a 200+ MB binary, Chromium security updates, sandbox configuration, and a process that consumes hundreds of megabytes of RAM per render. In a serverless environment, cold starts are slow. In a container, the image is bloated. In CI, the browser needs to be installed and configured.

A JSON-to-PDF API has no browser dependency. You send JSON, you get a PDF back. The rendering happens server-side with a purpose-built engine — not a web browser pretending to be a print driver.

What JSON-to-PDF Looks Like

Instead of writing HTML and CSS, you describe your document as a JSON object. The document has metadata, content blocks, page settings, and styles. Each content block has a type — paragraph, headline, table, list, image, separator — and properties that control its appearance.

Here is a simple invoice:

curl -X POST https://api.iterationlayer.com/document-generation/v1/generate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "format": "pdf",
    "document": {
      "metadata": {
        "title": "Invoice #2026-0042"
      },
      "page": {
        "size": {
          "preset": "A4"
        },
        "margins": {
          "top_in_pt": 72,
          "right_in_pt": 56,
          "bottom_in_pt": 72,
          "left_in_pt": 56
        }
      },
      "content": [
        {
          "type": "headline",
          "level": "h1",
          "text": "Invoice #2026-0042"
        },
        {
          "type": "paragraph",
          "markdown": "**Date:** April 15, 2026\n\n**Due:** May 15, 2026\n\n**Client:** Acme Corporation"
        },
        {
          "type": "table",
          "header": {
            "cells": [
              { "text": "Description" },
              { "text": "Qty", "horizontal_alignment": "right" },
              { "text": "Unit Price", "horizontal_alignment": "right" },
              { "text": "Total", "horizontal_alignment": "right" }
            ]
          },
          "rows": [
            {
              "cells": [
                { "text": "API integration consulting" },
                { "text": "40", "horizontal_alignment": "right" },
                { "text": "$150.00", "horizontal_alignment": "right" },
                { "text": "$6,000.00", "horizontal_alignment": "right" }
              ]
            },
            {
              "cells": [
                { "text": "Document processing setup" },
                { "text": "1", "horizontal_alignment": "right" },
                { "text": "$2,500.00", "horizontal_alignment": "right" },
                { "text": "$2,500.00", "horizontal_alignment": "right" }
              ]
            }
          ]
        },
        {
          "type": "separator"
        },
        {
          "type": "paragraph",
          "markdown": "**Total Due: $8,500.00**"
        }
      ]
    }
  }' --output invoice.pdf
import { IterationLayer } from "iterationlayer";
import { writeFileSync } from "fs";

const client = new IterationLayer({ apiKey: "YOUR_API_KEY" });

const result = await client.generateDocument({
  format: "pdf",
  document: {
    metadata: {
      title: "Invoice #2026-0042",
    },
    page: {
      size: { preset: "A4" },
      margins: {
        top_in_pt: 72,
        right_in_pt: 56,
        bottom_in_pt: 72,
        left_in_pt: 56,
      },
    },
    content: [
      {
        type: "headline",
        level: "h1",
        text: "Invoice #2026-0042",
      },
      {
        type: "paragraph",
        markdown:
          "**Date:** April 15, 2026\n\n**Due:** May 15, 2026\n\n**Client:** Acme Corporation",
      },
      {
        type: "table",
        header: {
          cells: [
            { text: "Description" },
            { text: "Qty", horizontal_alignment: "right" },
            { text: "Unit Price", horizontal_alignment: "right" },
            { text: "Total", horizontal_alignment: "right" },
          ],
        },
        rows: [
          {
            cells: [
              { text: "API integration consulting" },
              { text: "40", horizontal_alignment: "right" },
              { text: "$150.00", horizontal_alignment: "right" },
              { text: "$6,000.00", horizontal_alignment: "right" },
            ],
          },
          {
            cells: [
              { text: "Document processing setup" },
              { text: "1", horizontal_alignment: "right" },
              { text: "$2,500.00", horizontal_alignment: "right" },
              { text: "$2,500.00", horizontal_alignment: "right" },
            ],
          },
        ],
      },
      { type: "separator" },
      {
        type: "paragraph",
        markdown: "**Total Due: $8,500.00**",
      },
    ],
  },
});

writeFileSync("invoice.pdf", Buffer.from(result.buffer, "base64"));
import base64
from iterationlayer import IterationLayer

client = IterationLayer(api_key="YOUR_API_KEY")

result = client.generate_document(
    format="pdf",
    document={
        "metadata": {
            "title": "Invoice #2026-0042",
        },
        "page": {
            "size": {"preset": "A4"},
            "margins": {
                "top_in_pt": 72,
                "right_in_pt": 56,
                "bottom_in_pt": 72,
                "left_in_pt": 56,
            },
        },
        "content": [
            {
                "type": "headline",
                "level": "h1",
                "text": "Invoice #2026-0042",
            },
            {
                "type": "paragraph",
                "markdown": "**Date:** April 15, 2026\n\n**Due:** May 15, 2026\n\n**Client:** Acme Corporation",
            },
            {
                "type": "table",
                "header": {
                    "cells": [
                        {"text": "Description"},
                        {"text": "Qty", "horizontal_alignment": "right"},
                        {"text": "Unit Price", "horizontal_alignment": "right"},
                        {"text": "Total", "horizontal_alignment": "right"},
                    ]
                },
                "rows": [
                    {
                        "cells": [
                            {"text": "API integration consulting"},
                            {"text": "40", "horizontal_alignment": "right"},
                            {"text": "$150.00", "horizontal_alignment": "right"},
                            {"text": "$6,000.00", "horizontal_alignment": "right"},
                        ]
                    },
                    {
                        "cells": [
                            {"text": "Document processing setup"},
                            {"text": "1", "horizontal_alignment": "right"},
                            {"text": "$2,500.00", "horizontal_alignment": "right"},
                            {"text": "$2,500.00", "horizontal_alignment": "right"},
                        ]
                    },
                ],
            },
            {"type": "separator"},
            {
                "type": "paragraph",
                "markdown": "**Total Due: $8,500.00**",
            },
        ],
    },
)

with open("invoice.pdf", "wb") as f:
    f.write(base64.b64decode(result["buffer"]))
package main

import (
    "encoding/base64"
    "os"

    il "github.com/iterationlayer/sdk-go"
)

func main() {
    client := il.NewClient("YOUR_API_KEY")

    result, err := client.GenerateDocument(il.GenerateDocumentRequest{
        Format: "pdf",
        Document: il.DocumentDefinition{
            Metadata: il.DocumentMetadata{
                Title: "Invoice #2026-0042",
            },
            Page: &il.DocumentPage{
                Size: il.DocPageSize{
                    Preset: "A4",
                },
                Margins: il.DocMargins{
                    TopInPt:    72,
                    RightInPt:  56,
                    BottomInPt: 72,
                    LeftInPt:   56,
                },
            },
            Content: []il.ContentBlock{
                il.NewHeadlineBlock("h1", "Invoice #2026-0042"),
                il.ParagraphBlock{
                    Type:     "paragraph",
                    Markdown: "**Date:** April 15, 2026\n\n**Due:** May 15, 2026\n\n**Client:** Acme Corporation",
                },
                il.TableBlock{
                    Type: "table",
                    Header: &il.TableRow{
                        Cells: []il.TableCell{
                            {Text: "Description"},
                            {Text: "Qty", HorizontalAlignment: "right"},
                            {Text: "Unit Price", HorizontalAlignment: "right"},
                            {Text: "Total", HorizontalAlignment: "right"},
                        },
                    },
                    Rows: []il.TableRow{
                        {
                            Cells: []il.TableCell{
                                {Text: "API integration consulting"},
                                {Text: "40", HorizontalAlignment: "right"},
                                {Text: "$150.00", HorizontalAlignment: "right"},
                                {Text: "$6,000.00", HorizontalAlignment: "right"},
                            },
                        },
                        {
                            Cells: []il.TableCell{
                                {Text: "Document processing setup"},
                                {Text: "1", HorizontalAlignment: "right"},
                                {Text: "$2,500.00", HorizontalAlignment: "right"},
                                {Text: "$2,500.00", HorizontalAlignment: "right"},
                            },
                        },
                    },
                },
                il.NewSeparatorBlock(),
                il.ParagraphBlock{
                    Type:     "paragraph",
                    Markdown: "**Total Due: $8,500.00**",
                },
            },
        },
    })
    if err != nil {
        panic(err)
    }

    pdfBytes, err := base64.StdEncoding.DecodeString(result.Buffer)
    if err != nil {
        panic(err)
    }

    os.WriteFile("invoice.pdf", pdfBytes, 0644)
}

No HTML. No CSS. No browser. The document structure is explicit. Every dimension is in points. Every element is typed. The output is a PDF that matches the definition exactly.

Side-by-Side: HTML Template vs. JSON Document

To see the difference concretely, compare how you would build the same invoice with both approaches.

The HTML Template Approach

<!DOCTYPE html>
<html>
<head>
  <style>
    @page {
      size: A4;
      margin: 1in 0.78in;
    }
    body {
      font-family: 'Inter', sans-serif;
      font-size: 11pt;
      color: #1a1a1a;
    }
    h1 { font-size: 24pt; margin-bottom: 24pt; }
    table {
      width: 100%;
      border-collapse: collapse;
      page-break-inside: avoid; /* hint, not guarantee */
    }
    th {
      background: #f5f5f5;
      text-align: left;
      padding: 8pt;
      border-bottom: 1pt solid #e0e0e0;
    }
    td {
      padding: 8pt;
      border-bottom: 1pt solid #f0f0f0;
    }
    .align-right { text-align: right; }
    .total { font-weight: bold; margin-top: 16pt; }
  </style>
</head>
<body>
  <h1>Invoice #{{invoice_number}}</h1>
  <p><strong>Date:</strong> {{date}}</p>
  <p><strong>Due:</strong> {{due_date}}</p>
  <p><strong>Client:</strong> {{client_name}}</p>

  <table>
    <thead>
      <tr>
        <th>Description</th>
        <th class="align-right">Qty</th>
        <th class="align-right">Unit Price</th>
        <th class="align-right">Total</th>
      </tr>
    </thead>
    <tbody>
      {{#each line_items}}
      <tr>
        <td>{{description}}</td>
        <td class="align-right">{{quantity}}</td>
        <td class="align-right">{{unit_price}}</td>
        <td class="align-right">{{total}}</td>
      </tr>
      {{/each}}
    </tbody>
  </table>

  <hr>
  <p class="total">Total Due: {{total_due}}</p>
</body>
</html>

Then you need: a templating engine (Handlebars, Jinja, EJS), a headless browser (Puppeteer, Playwright), font installation, CSS debugging across browser versions, and page-break-inside: avoid that the browser may or may not honor.

The JSON Document Approach

The same invoice, defined as JSON content blocks:

{
  "format": "pdf",
  "document": {
    "metadata": {
      "title": "Invoice #2026-0042"
    },
    "page": {
      "size": {
        "preset": "A4"
      },
      "margins": {
        "top_in_pt": 72,
        "right_in_pt": 56,
        "bottom_in_pt": 72,
        "left_in_pt": 56
      }
    },
    "content": [
      {
        "type": "headline",
        "level": "h1",
        "text": "Invoice #2026-0042"
      },
      {
        "type": "paragraph",
        "markdown": "**Date:** April 15, 2026\n\n**Due:** May 15, 2026\n\n**Client:** Acme Corporation"
      },
      {
        "type": "table",
        "header": {
          "cells": [
            {
              "text": "Description"
            },
            {
              "text": "Qty",
              "horizontal_alignment": "right"
            },
            {
              "text": "Unit Price",
              "horizontal_alignment": "right"
            },
            {
              "text": "Total",
              "horizontal_alignment": "right"
            }
          ]
        },
        "rows": [
          {
            "cells": [
              {
                "text": "API integration consulting"
              },
              {
                "text": "40",
                "horizontal_alignment": "right"
              },
              {
                "text": "$150.00",
                "horizontal_alignment": "right"
              },
              {
                "text": "$6,000.00",
                "horizontal_alignment": "right"
              }
            ]
          }
        ]
      },
      {
        "type": "separator"
      },
      {
        "type": "paragraph",
        "markdown": "**Total Due: $8,500.00**"
      }
    ]
  }
}

No templating engine. No CSS. No browser. The document definition is the source of truth for what the PDF contains and how it looks. You can generate this JSON from any language, store it, version it, diff it, and test it.

The Template Maintenance Problem

HTML templates degrade over time. The degradation follows a predictable pattern.

Month 1: The template works. The invoice looks correct. Ship it.

Month 3: A customer has a company name that wraps to two lines. The layout breaks. Add a CSS fix: white-space: nowrap; overflow: hidden; text-overflow: ellipsis;. Now long names are truncated instead of breaking the layout. Is that better? Maybe. Ship it.

Month 6: The finance team wants a second table for tax breakdowns. Adding it pushes the total to a second page. The page break splits mid-row. Add page-break-inside: avoid to the table. It works in Chrome 120. It does not work in Chrome 122. Pin the browser version. Ship it.

Month 9: The ops team needs the same invoice in German. The German text is 30% longer than English. The layout that worked for English overflows in German. Add language-specific CSS overrides. Ship it.

Month 12: Nobody wants to touch the template. The CSS is 400 lines of interconnected hacks. A junior developer changes the font size and three other elements shift. The template is a liability.

With JSON document blocks, these problems do not exist:

  • Long company names flow naturally within paragraph blocks. No truncation, no overflow.
  • Tables split at row boundaries, never mid-row. The header repeats on the next page.
  • Longer text in a different language fills more vertical space. The page engine handles pagination automatically.
  • Every change is a change to a JSON property. There is no cascade, no inheritance, no side effects.

Styling Without CSS

JSON document generation does not mean unstyled documents. The Document Generation API supports comprehensive styling — but through explicit properties, not a cascade.

Document-level styles set defaults for the entire document:

{
  "format": "pdf",
  "document": {
    "metadata": {
      "title": "Styled Report"
    },
    "styles": {
      "text": {
        "font_family": "Inter",
        "font_size_in_pt": 11,
        "line_height": 1.5,
        "color": "#1a1a1a"
      },
      "headline": {
        "font_family": "Inter",
        "font_size_in_pt": 24,
        "color": "#111111",
        "spacing_before_in_pt": 24,
        "spacing_after_in_pt": 12,
        "font_weight": "bold"
      },
      "table": {
        "header": {
          "background_color": "#f5f5f5",
          "text_color": "#1a1a1a",
          "font_size_in_pt": 10,
          "font_weight": "semibold"
        },
        "body": {
          "background_color": "#ffffff",
          "text_color": "#333333",
          "font_size_in_pt": 10
        },
        "border": {
          "width_in_pt": 0.5,
          "color": "#e0e0e0"
        }
      },
      "separator": {
        "color": "#e0e0e0",
        "thickness_in_pt": 1,
        "spacing_before_in_pt": 16,
        "spacing_after_in_pt": 16
      }
    },
    "content": []
  }
}

Individual blocks can override these defaults with a styles property. A specific headline can use a different color. A specific table can use different borders. The override is local and explicit — it does not cascade to other elements.

This is the key difference from CSS: there is no inheritance chain to debug. The document styles are the defaults. Block-level overrides are exceptions. The interaction between the two is always predictable.

Headers, Footers, and Page Numbers

Headers and footers in HTML-to-PDF are notoriously difficult. Puppeteer supports them via a headerTemplate and footerTemplate option, but they render in a separate context with limited CSS support and no access to document fonts. Many teams give up and use absolute positioning hacks or post-process the PDF with another library.

In the JSON document model, headers and footers are arrays of content blocks — the same block types used in the document body. They render on every page with access to the same fonts and styles.

curl -X POST https://api.iterationlayer.com/document-generation/v1/generate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "format": "pdf",
    "document": {
      "metadata": {
        "title": "Report with Headers"
      },
      "header": [
        {
          "type": "paragraph",
          "markdown": "**Acme Corporation** — Confidential",
          "text_alignment": "right"
        }
      ],
      "footer": [
        {
          "type": "page-number",
          "text_alignment": "center"
        }
      ],
      "content": [
        {
          "type": "headline",
          "level": "h1",
          "text": "Annual Report 2026"
        },
        {
          "type": "paragraph",
          "markdown": "This report contains the financial results for fiscal year 2026."
        }
      ]
    }
  }' --output report.pdf
import { IterationLayer } from "iterationlayer";
import { writeFileSync } from "fs";

const client = new IterationLayer({ apiKey: "YOUR_API_KEY" });

const result = await client.generateDocument({
  format: "pdf",
  document: {
    metadata: { title: "Report with Headers" },
    header: [
      {
        type: "paragraph",
        markdown: "**Acme Corporation** — Confidential",
        text_alignment: "right",
      },
    ],
    footer: [
      {
        type: "page-number",
        text_alignment: "center",
      },
    ],
    content: [
      {
        type: "headline",
        level: "h1",
        text: "Annual Report 2026",
      },
      {
        type: "paragraph",
        markdown:
          "This report contains the financial results for fiscal year 2026.",
      },
    ],
  },
});

writeFileSync("report.pdf", Buffer.from(result.buffer, "base64"));
import base64
from iterationlayer import IterationLayer

client = IterationLayer(api_key="YOUR_API_KEY")

result = client.generate_document(
    format="pdf",
    document={
        "metadata": {"title": "Report with Headers"},
        "header": [
            {
                "type": "paragraph",
                "markdown": "**Acme Corporation** — Confidential",
                "text_alignment": "right",
            }
        ],
        "footer": [
            {
                "type": "page-number",
                "text_alignment": "center",
            }
        ],
        "content": [
            {
                "type": "headline",
                "level": "h1",
                "text": "Annual Report 2026",
            },
            {
                "type": "paragraph",
                "markdown": "This report contains the financial results for fiscal year 2026.",
            },
        ],
    },
)

with open("report.pdf", "wb") as f:
    f.write(base64.b64decode(result["buffer"]))
package main

import (
    "encoding/base64"
    "os"

    il "github.com/iterationlayer/sdk-go"
)

func main() {
    client := il.NewClient("YOUR_API_KEY")

    result, err := client.GenerateDocument(il.GenerateDocumentRequest{
        Format: "pdf",
        Document: il.DocumentDefinition{
            Metadata: il.DocumentMetadata{Title: "Report with Headers"},
            Header: []il.HeaderFooterBlock{
                il.ParagraphBlock{
                    Type:          "paragraph",
                    Markdown:      "**Acme Corporation** — Confidential",
                    TextAlignment: "right",
                },
            },
            Footer: []il.HeaderFooterBlock{
                il.PageNumberBlock{
                    Type:          "page-number",
                    TextAlignment: "center",
                },
            },
            Content: []il.ContentBlock{
                il.NewHeadlineBlock("h1", "Annual Report 2026"),
                il.ParagraphBlock{
                    Type:     "paragraph",
                    Markdown: "This report contains the financial results for fiscal year 2026.",
                },
            },
        },
    })
    if err != nil {
        panic(err)
    }

    pdfBytes, _ := base64.StdEncoding.DecodeString(result.Buffer)
    os.WriteFile("report.pdf", pdfBytes, 0644)
}

The header appears on every page. The footer shows the page number, centered, on every page. No separate template context, no limited CSS subset, no rendering quirks.

Beyond PDF: Same JSON, Multiple Formats

HTML-to-PDF gives you PDFs. If you need the same content as a Word document, you need a different tool. If you need an EPUB, another tool. Each one has its own template format, its own quirks, its own dependencies.

The Document Generation API produces PDF, DOCX, EPUB, and PPTX from the same JSON definition. Change the format field and the same content renders into a different output format.

# PDF
curl -X POST https://api.iterationlayer.com/document-generation/v1/generate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"format": "pdf", "document": {...}}' --output output.pdf

# Word
curl -X POST https://api.iterationlayer.com/document-generation/v1/generate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"format": "docx", "document": {...}}' --output output.docx

# EPUB
curl -X POST https://api.iterationlayer.com/document-generation/v1/generate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"format": "epub", "document": {...}}' --output output.epub
import { IterationLayer } from "iterationlayer";
import { writeFileSync } from "fs";

const client = new IterationLayer({ apiKey: "YOUR_API_KEY" });

const documentDefinition = {
  metadata: { title: "Multi-Format Report" },
  content: [
    { type: "headline" as const, level: "h1" as const, text: "Report Title" },
    { type: "paragraph" as const, markdown: "Report content goes here." },
  ],
};

const pdf = await client.generateDocument({
  format: "pdf",
  document: documentDefinition,
});

const docx = await client.generateDocument({
  format: "docx",
  document: documentDefinition,
});

writeFileSync("output.pdf", Buffer.from(pdf.buffer, "base64"));
writeFileSync("output.docx", Buffer.from(docx.buffer, "base64"));
import base64
from iterationlayer import IterationLayer

client = IterationLayer(api_key="YOUR_API_KEY")

document_definition = {
    "metadata": {"title": "Multi-Format Report"},
    "content": [
        {"type": "headline", "level": "h1", "text": "Report Title"},
        {"type": "paragraph", "markdown": "Report content goes here."},
    ],
}

pdf_result = client.generate_document(
    format="pdf",
    document=document_definition,
)

docx_result = client.generate_document(
    format="docx",
    document=document_definition,
)

with open("output.pdf", "wb") as f:
    f.write(base64.b64decode(pdf_result["buffer"]))

with open("output.docx", "wb") as f:
    f.write(base64.b64decode(docx_result["buffer"]))
package main

import (
    "encoding/base64"
    "os"

    il "github.com/iterationlayer/sdk-go"
)

func main() {
    client := il.NewClient("YOUR_API_KEY")

    documentDefinition := il.DocumentDefinition{
        Metadata: il.DocumentMetadata{Title: "Multi-Format Report"},
        Content: []il.ContentBlock{
            il.NewHeadlineBlock("h1", "Report Title"),
            il.ParagraphBlock{
                Type:     "paragraph",
                Markdown: "Report content goes here.",
            },
        },
    }

    pdfResult, _ := client.GenerateDocument(il.GenerateDocumentRequest{
        Format:   "pdf",
        Document: documentDefinition,
    })

    docxResult, _ := client.GenerateDocument(il.GenerateDocumentRequest{
        Format:   "docx",
        Document: documentDefinition,
    })

    pdfBytes, _ := base64.StdEncoding.DecodeString(pdfResult.Buffer)
    os.WriteFile("output.pdf", pdfBytes, 0644)

    docxBytes, _ := base64.StdEncoding.DecodeString(docxResult.Buffer)
    os.WriteFile("output.docx", docxBytes, 0644)
}

One document definition, multiple output formats. No separate template per format. No format-specific tooling.

When HTML-to-PDF Still Makes Sense

Honesty matters. There are cases where HTML-to-PDF is the right choice.

If you are rendering a web page as a PDF — a “print this page” feature — a headless browser is the correct tool. The content already exists as HTML, and you want the PDF to match what the user sees on screen. Converting that to a JSON document model would be unnecessary work.

If your PDFs need complex visual layouts that are essentially web designs — multi-column magazine layouts with text wrapping around images, gradient backgrounds, CSS grid — HTML and CSS give you more visual flexibility than a block-based document model.

If you have an existing HTML template system that works and you are not experiencing the maintenance problems described above, there is no reason to migrate.

JSON-to-PDF wins when you are generating documents from structured data — invoices, reports, contracts, certificates — where the content is dynamic, the layout follows consistent patterns, and reliability matters more than visual complexity.

Composing Document Generation with Extraction

The strongest case for JSON-to-PDF is when the document content comes from another API. Extract data from a source document, then generate a new document from that data — all within the same platform, same credit pool, same API conventions.

An agency that processes client invoices might extract the line items using the Document Extraction API, then generate a consolidated monthly report as a PDF using the Document Generation API. The extraction output is structured JSON. The generation input is structured JSON. No format conversion, no middleware, no glue code.

This is where the composability of Iteration Layer matters most. Individual document operations are commodities. The ability to chain extraction into generation — parse a document, transform the data, produce a new document — without switching tools, credentials, or billing systems is where the real time savings come from.

Getting Started

The Document Generation API supports PDF, DOCX, EPUB, and PPTX output. See the documentation for the full block reference, style options, and page configuration.

If you are currently maintaining an HTML-to-PDF pipeline and experiencing the fragility described above, start with a single document type — the one that breaks most often. Rebuild it as a JSON document definition. Compare the maintenance burden after a month.

For workflows that combine extraction and generation, the Document Extraction API and Document Generation API share the same auth, credits, and API style. Parse a source document, generate the output document. Two API calls, one pipeline, no glue code.

Try with your own data

Get a free API key and run this in minutes. No credit card required.