Payment processors are good at collecting money. They are not always good at producing the exact invoice a business needs to issue.
That difference sounds small until the invoice becomes a legal record. The invoice number, tax treatment, customer address, line items, payment reference, and PDF all need to describe the same business event. If the payment processor cannot express one of those rules, the invoice cannot be treated as a skin on top of the payment provider.
That is where we ended up with IGIC, the Canary Islands indirect tax. Stripe still handles the payment lifecycle for us. It charges the customer, tracks subscription state, handles failed payments, and emits the webhook that tells us a payment succeeded. But Stripe does not support the Canary Islands tax model we need for customer-facing invoices.
So we draw a hard boundary. Stripe owns payment state. Our billing system owns invoice state. The Iteration Layer Document Generation API turns that invoice state into the PDF customers download.
The Stripe Boundary
Stripe invoices are useful when Stripe can model the invoice you need. In our case, the payment object and the legal invoice are related, but they are not the same artifact.
The specific blocker is IGIC. The Canary Islands are part of Spain, but they do not use mainland IVA in the same way. For our customer-facing invoices, the tax rule we need today is deliberately narrow:
- Canary Islands customers: 7% IGIC
- B2C customers outside the EU: 7% IGIC
- everything else: 0% IGIC
Stripe still creates internal billing objects because subscriptions, renewals, failed payments, and invoice.paid webhooks depend on them. We use those objects as payment evidence, not as the final invoice document.
That split keeps responsibilities clear:
- Stripe tells us what was paid.
- Our billing system records what we issued.
- Document generation renders that issued record.
This matters for retries too. If Stripe sends the same webhook twice, the Stripe invoice ID points back to the invoice we already issued. If PDF rendering fails, a background job can retry without charging the customer again or creating a second invoice number.
The Invoice Record Comes First
The mistake in many PDF systems is starting with the template.
For invoices, the source of truth should be the business record. Our record contains the invoice number, issue date, payment reference, customer billing snapshot, issuer snapshot, line items, subtotal, tax rate, tax amount, and total. The PDF is one rendering of that record, not the record itself.
This is the same reason HTML-to-PDF templates break down for business documents. HTML encourages teams to mix data, layout, and rendering behavior in one file. That works for simple pages. It gets brittle when the document needs tax correctness, stable numbering, page-aware tables, and deterministic output.
Line items are especially important. Stripe remains the source of truth for paid line totals, including subscription prices, usage charges, prorations, and credits. Our renderer does not recalculate graduated pricing or reinterpret a negative line item. It stores the paid values and renders them exactly, including values such as -42,00.
That design keeps the invoice generator boring in the right places. Tax classification happens once. Invoice numbering happens once. The document layer receives a complete invoice and focuses on layout.
Rendering the Invoice as Blocks
The invoice template is a normal document-generation document. It is not HTML. It is built from typed blocks that describe the page and the content on it.
The rendered invoice uses:
- an image block for the Iteration Layer wordmark
- a grid block for the top header
- another grid block for customer details and invoice metadata
- paragraph blocks for addresses, labels, and payment references
- a table block for line items
- a second table block for subtotal, IGIC, and total
Here is a sample invoice generated from synthetic data:
The request below reflects the same synthetic invoice and the same document structure our invoice template builds: A4 page setup, global styles, a brand grid, an invoice metadata grid, an issuer grid, the line-item table, and the totals table. The raw JSON request uses a base64-encoded wordmark image buffer. The SDK examples pass image bytes inline because the SDKs serialize binary fields before sending the request.
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 IL-10001",
"author": "Iteration Layer"
},
"page": {
"size": {
"preset": "A4"
},
"margins": {
"top_in_pt": 56.0,
"right_in_pt": 44.0,
"bottom_in_pt": 56.0,
"left_in_pt": 44.0
}
},
"styles": {
"text": {
"font_family": "PlusJakartaSans",
"font_size_in_pt": 11.0,
"line_height": 1.28,
"color": "#111111"
},
"headline": {
"font_family": "Outfit",
"font_size_in_pt": 18.0,
"font_weight": "bold",
"color": "#111111",
"spacing_before_in_pt": 0.0,
"spacing_after_in_pt": 4.0
},
"table": {
"header": {
"background_color": "#FFFFFF",
"text_color": "#111111",
"font_size_in_pt": 11.0,
"font_weight": "bold"
},
"body": {
"background_color": "#FFFFFF",
"text_color": "#111111",
"font_size_in_pt": 11.0
},
"border": {
"outer": {
"top": {
"color": "#DDDDDD",
"width_in_pt": 0.5
},
"right": {
"color": "#FFFFFF",
"width_in_pt": 0.0
},
"bottom": {
"color": "#DDDDDD",
"width_in_pt": 0.5
},
"left": {
"color": "#FFFFFF",
"width_in_pt": 0.0
}
},
"inner": {
"horizontal": {
"color": "#DDDDDD",
"width_in_pt": 0.5
},
"vertical": {
"color": "#FFFFFF",
"width_in_pt": 0.0
}
}
}
},
"grid": {
"background_color": "#FFFFFF",
"border_color": "#FFFFFF",
"border_width_in_pt": 0.0,
"gap_in_pt": 22.0
},
"image": {
"border_color": "#FFFFFF",
"border_width_in_pt": 0.0
}
},
"content": [
{
"type": "grid",
"columns": [
{
"column_span": 8,
"blocks": [
{
"type": "paragraph",
"runs": [
{
"text": "."
}
],
"styles": {
"font_size_in_pt": 1.0,
"color": "#FFFFFF"
}
}
]
},
{
"column_span": 4,
"horizontal_alignment": "right",
"blocks": [
{
"type": "image",
"buffer": "<base64-wordmark-png>",
"width_in_pt": 133.5,
"height_in_pt": 25.5,
"fit": "contain"
}
]
}
],
"styles": {
"gap_in_pt": 0.0
}
},
{
"type": "paragraph",
"runs": [
{
"text": "."
}
],
"styles": {
"font_size_in_pt": 45.0,
"color": "#FFFFFF"
}
},
{
"type": "grid",
"columns": [
{
"column_span": 5,
"blocks": [
{
"type": "paragraph",
"text_alignment": "left",
"runs": [
{
"text": "Invoice to",
"font_weight": "bold",
"is_italic": false
}
],
"styles": {
"font_size_in_pt": 11.0
}
},
{
"type": "paragraph",
"text_alignment": "left",
"runs": [
{
"text": "Acme Automation GmbH\nExample Street 42\n10115 Berlin\nGermany\n\nVAT: DE123456789",
"font_weight": null,
"is_italic": false
}
],
"styles": {
"font_size_in_pt": 11.0
}
}
]
},
{
"column_span": 3,
"blocks": [
{
"type": "paragraph",
"runs": [
{
"text": "."
}
],
"styles": {
"font_size_in_pt": 1.0,
"color": "#FFFFFF"
}
}
]
},
{
"column_span": 3,
"blocks": [
{
"type": "paragraph",
"text_alignment": "left",
"runs": [
{
"text": "Invoice No.\nDate\nStripe payment\nPaid via",
"font_weight": "bold",
"is_italic": false
}
],
"styles": {
"font_size_in_pt": 11.0
}
}
]
},
{
"column_span": 1,
"blocks": [
{
"type": "paragraph",
"text_alignment": "right",
"runs": [
{
"text": "IL-10001\n30.04.2026\npi_sample_123456\nCard",
"font_weight": null,
"is_italic": false
}
],
"styles": {
"font_size_in_pt": 11.0
}
}
]
}
],
"styles": {
"gap_in_pt": 0.0
}
},
{
"type": "paragraph",
"runs": [
{
"text": "."
}
],
"styles": {
"font_size_in_pt": 10.0,
"color": "#FFFFFF"
}
},
{
"type": "grid",
"columns": [
{
"column_span": 5,
"blocks": [
{
"type": "paragraph",
"runs": [
{
"text": "."
}
],
"styles": {
"font_size_in_pt": 1.0,
"color": "#FFFFFF"
}
}
]
},
{
"column_span": 7,
"horizontal_alignment": "right",
"blocks": [
{
"type": "paragraph",
"text_alignment": "right",
"runs": [
{
"text": "Iteration Layer",
"font_weight": "bold",
"is_italic": false
}
],
"styles": {
"font_size_in_pt": 11.0
}
},
{
"type": "paragraph",
"text_alignment": "right",
"runs": [
{
"text": "Example Software Studio Ltd.",
"font_weight": null,
"is_italic": false
}
],
"styles": {
"font_size_in_pt": 11.0
}
},
{
"type": "paragraph",
"text_alignment": "right",
"runs": [
{
"text": "1 Example Avenue\n10001 Example City\nSpain\n\nNIF: ESX0000000X",
"font_weight": null,
"is_italic": false
}
],
"styles": {
"font_size_in_pt": 11.0
}
}
]
}
],
"styles": {
"gap_in_pt": 0.0
}
},
{
"type": "paragraph",
"runs": [
{
"text": "."
}
],
"styles": {
"font_size_in_pt": 28.0,
"color": "#FFFFFF"
}
},
{
"type": "table",
"column_widths_in_percent": [
10,
56,
17,
17
],
"header": {
"cells": [
{
"text": "Quantity"
},
{
"text": "Description",
"horizontal_alignment": "center"
},
{
"text": "Price (EUR)",
"horizontal_alignment": "right"
},
{
"text": "Total (EUR)",
"horizontal_alignment": "right"
}
]
},
"rows": [
{
"cells": [
{
"text": "1"
},
{
"text": "Iteration Layer Pro subscription (April 2026)"
},
{
"text": "99,00",
"horizontal_alignment": "right"
},
{
"text": "99,00",
"horizontal_alignment": "right"
}
]
},
{
"cells": [
{
"text": "1250"
},
{
"text": "Pay-as-you-go document generation credits\n1,250 generated document pages beyond included plan usage."
},
{
"text": "0,50",
"horizontal_alignment": "right"
},
{
"text": "625,00",
"horizontal_alignment": "right"
}
]
},
{
"cells": [
{
"text": "1"
},
{
"text": "Prorated plan adjustment credit"
},
{
"text": "-42,00",
"horizontal_alignment": "right"
},
{
"text": "-42,00",
"horizontal_alignment": "right"
}
]
}
]
},
{
"type": "paragraph",
"runs": [
{
"text": "."
}
],
"styles": {
"font_size_in_pt": 18.0,
"color": "#FFFFFF"
}
},
{
"type": "grid",
"columns": [
{
"column_span": 7,
"blocks": [
{
"type": "paragraph",
"runs": [
{
"text": "."
}
],
"styles": {
"font_size_in_pt": 1.0,
"color": "#FFFFFF"
}
}
]
},
{
"column_span": 5,
"blocks": [
{
"type": "table",
"column_widths_in_percent": [
64,
36
],
"rows": [
{
"cells": [
{
"text": "Subtotal (EUR)",
"horizontal_alignment": "right"
},
{
"text": "682,00",
"horizontal_alignment": "right"
}
]
},
{
"cells": [
{
"text": "IGIC (0%)",
"horizontal_alignment": "right"
},
{
"text": "0,00",
"horizontal_alignment": "right"
}
]
},
{
"cells": [
{
"text": "Total (EUR)",
"horizontal_alignment": "right"
},
{
"text": "682,00",
"horizontal_alignment": "right"
}
]
}
]
}
]
}
],
"styles": {
"gap_in_pt": 0.0
}
}
]
}
}'import { writeFile } from "node:fs/promises";
import { IterationLayer, type GenerateDocumentRequest } from "iterationlayer";
const apiKey = process.env.ITERATION_LAYER_API_KEY;
if (!apiKey) {
throw new Error("ITERATION_LAYER_API_KEY is required");
}
const client = new IterationLayer({ apiKey });
const result = await client.generateDocument({
"format": "pdf",
"document": {
"metadata": {
"title": "Invoice IL-10001",
"author": "Iteration Layer",
},
"page": {
"size": {
"preset": "A4",
},
"margins": {
"top_in_pt": 56.0,
"right_in_pt": 44.0,
"bottom_in_pt": 56.0,
"left_in_pt": 44.0,
},
},
"styles": {
"text": {
"font_family": "PlusJakartaSans",
"font_size_in_pt": 11.0,
"line_height": 1.28,
"color": "#111111",
},
"headline": {
"font_family": "Outfit",
"font_size_in_pt": 18.0,
"font_weight": "bold",
"color": "#111111",
"spacing_before_in_pt": 0.0,
"spacing_after_in_pt": 4.0,
},
"table": {
"header": {
"background_color": "#FFFFFF",
"text_color": "#111111",
"font_size_in_pt": 11.0,
"font_weight": "bold",
},
"body": {
"background_color": "#FFFFFF",
"text_color": "#111111",
"font_size_in_pt": 11.0,
},
"border": {
"outer": {
"top": {
"color": "#DDDDDD",
"width_in_pt": 0.5,
},
"right": {
"color": "#FFFFFF",
"width_in_pt": 0.0,
},
"bottom": {
"color": "#DDDDDD",
"width_in_pt": 0.5,
},
"left": {
"color": "#FFFFFF",
"width_in_pt": 0.0,
},
},
"inner": {
"horizontal": {
"color": "#DDDDDD",
"width_in_pt": 0.5,
},
"vertical": {
"color": "#FFFFFF",
"width_in_pt": 0.0,
},
},
},
},
"grid": {
"background_color": "#FFFFFF",
"border_color": "#FFFFFF",
"border_width_in_pt": 0.0,
"gap_in_pt": 22.0,
},
"image": {
"border_color": "#FFFFFF",
"border_width_in_pt": 0.0,
},
},
"content": [
{
"type": "grid",
"columns": [
{
"column_span": 8,
"blocks": [
{
"type": "paragraph",
"runs": [
{
"text": ".",
},
],
"styles": {
"font_size_in_pt": 1.0,
"color": "#FFFFFF",
},
},
],
},
{
"column_span": 4,
"horizontal_alignment": "right",
"blocks": [
{
"type": "image",
"buffer": new Uint8Array([/* wordmark PNG bytes */]),
"width_in_pt": 133.5,
"height_in_pt": 25.5,
"fit": "contain",
},
],
},
],
"styles": {
"gap_in_pt": 0.0,
},
},
{
"type": "paragraph",
"runs": [
{
"text": ".",
},
],
"styles": {
"font_size_in_pt": 45.0,
"color": "#FFFFFF",
},
},
{
"type": "grid",
"columns": [
{
"column_span": 5,
"blocks": [
{
"type": "paragraph",
"text_alignment": "left",
"runs": [
{
"text": "Invoice to",
"font_weight": "bold",
"is_italic": false,
},
],
"styles": {
"font_size_in_pt": 11.0,
},
},
{
"type": "paragraph",
"text_alignment": "left",
"runs": [
{
"text": "Acme Automation GmbH\nExample Street 42\n10115 Berlin\nGermany\n\nVAT: DE123456789",
"font_weight": null,
"is_italic": false,
},
],
"styles": {
"font_size_in_pt": 11.0,
},
},
],
},
{
"column_span": 3,
"blocks": [
{
"type": "paragraph",
"runs": [
{
"text": ".",
},
],
"styles": {
"font_size_in_pt": 1.0,
"color": "#FFFFFF",
},
},
],
},
{
"column_span": 3,
"blocks": [
{
"type": "paragraph",
"text_alignment": "left",
"runs": [
{
"text": "Invoice No.\nDate\nStripe payment\nPaid via",
"font_weight": "bold",
"is_italic": false,
},
],
"styles": {
"font_size_in_pt": 11.0,
},
},
],
},
{
"column_span": 1,
"blocks": [
{
"type": "paragraph",
"text_alignment": "right",
"runs": [
{
"text": "IL-10001\n30.04.2026\npi_sample_123456\nCard",
"font_weight": null,
"is_italic": false,
},
],
"styles": {
"font_size_in_pt": 11.0,
},
},
],
},
],
"styles": {
"gap_in_pt": 0.0,
},
},
{
"type": "paragraph",
"runs": [
{
"text": ".",
},
],
"styles": {
"font_size_in_pt": 10.0,
"color": "#FFFFFF",
},
},
{
"type": "grid",
"columns": [
{
"column_span": 5,
"blocks": [
{
"type": "paragraph",
"runs": [
{
"text": ".",
},
],
"styles": {
"font_size_in_pt": 1.0,
"color": "#FFFFFF",
},
},
],
},
{
"column_span": 7,
"horizontal_alignment": "right",
"blocks": [
{
"type": "paragraph",
"text_alignment": "right",
"runs": [
{
"text": "Iteration Layer",
"font_weight": "bold",
"is_italic": false,
},
],
"styles": {
"font_size_in_pt": 11.0,
},
},
{
"type": "paragraph",
"text_alignment": "right",
"runs": [
{
"text": "Example Software Studio Ltd.",
"font_weight": null,
"is_italic": false,
},
],
"styles": {
"font_size_in_pt": 11.0,
},
},
{
"type": "paragraph",
"text_alignment": "right",
"runs": [
{
"text": "1 Example Avenue\n10001 Example City\nSpain\n\nNIF: ESX0000000X",
"font_weight": null,
"is_italic": false,
},
],
"styles": {
"font_size_in_pt": 11.0,
},
},
],
},
],
"styles": {
"gap_in_pt": 0.0,
},
},
{
"type": "paragraph",
"runs": [
{
"text": ".",
},
],
"styles": {
"font_size_in_pt": 28.0,
"color": "#FFFFFF",
},
},
{
"type": "table",
"column_widths_in_percent": [
10,
56,
17,
17,
],
"header": {
"cells": [
{
"text": "Quantity",
},
{
"text": "Description",
"horizontal_alignment": "center",
},
{
"text": "Price (EUR)",
"horizontal_alignment": "right",
},
{
"text": "Total (EUR)",
"horizontal_alignment": "right",
},
],
},
"rows": [
{
"cells": [
{
"text": "1",
},
{
"text": "Iteration Layer Pro subscription (April 2026)",
},
{
"text": "99,00",
"horizontal_alignment": "right",
},
{
"text": "99,00",
"horizontal_alignment": "right",
},
],
},
{
"cells": [
{
"text": "1250",
},
{
"text": "Pay-as-you-go document generation credits\n1,250 generated document pages beyond included plan usage.",
},
{
"text": "0,50",
"horizontal_alignment": "right",
},
{
"text": "625,00",
"horizontal_alignment": "right",
},
],
},
{
"cells": [
{
"text": "1",
},
{
"text": "Prorated plan adjustment credit",
},
{
"text": "-42,00",
"horizontal_alignment": "right",
},
{
"text": "-42,00",
"horizontal_alignment": "right",
},
],
},
],
},
{
"type": "paragraph",
"runs": [
{
"text": ".",
},
],
"styles": {
"font_size_in_pt": 18.0,
"color": "#FFFFFF",
},
},
{
"type": "grid",
"columns": [
{
"column_span": 7,
"blocks": [
{
"type": "paragraph",
"runs": [
{
"text": ".",
},
],
"styles": {
"font_size_in_pt": 1.0,
"color": "#FFFFFF",
},
},
],
},
{
"column_span": 5,
"blocks": [
{
"type": "table",
"column_widths_in_percent": [
64,
36,
],
"rows": [
{
"cells": [
{
"text": "Subtotal (EUR)",
"horizontal_alignment": "right",
},
{
"text": "682,00",
"horizontal_alignment": "right",
},
],
},
{
"cells": [
{
"text": "IGIC (0%)",
"horizontal_alignment": "right",
},
{
"text": "0,00",
"horizontal_alignment": "right",
},
],
},
{
"cells": [
{
"text": "Total (EUR)",
"horizontal_alignment": "right",
},
{
"text": "682,00",
"horizontal_alignment": "right",
},
],
},
],
},
],
},
],
"styles": {
"gap_in_pt": 0.0,
},
},
],
},
} satisfies GenerateDocumentRequest);
await writeFile("IL-10001.pdf", result.buffer);from pathlib import Path
from iterationlayer import IterationLayer
client = IterationLayer(api_key="YOUR_API_KEY")
result = client.generate_document(
format="pdf",
document={
"metadata": {
"title": "Invoice IL-10001",
"author": "Iteration Layer",
},
"page": {
"size": {
"preset": "A4",
},
"margins": {
"top_in_pt": 56.0,
"right_in_pt": 44.0,
"bottom_in_pt": 56.0,
"left_in_pt": 44.0,
},
},
"styles": {
"text": {
"font_family": "PlusJakartaSans",
"font_size_in_pt": 11.0,
"line_height": 1.28,
"color": "#111111",
},
"headline": {
"font_family": "Outfit",
"font_size_in_pt": 18.0,
"font_weight": "bold",
"color": "#111111",
"spacing_before_in_pt": 0.0,
"spacing_after_in_pt": 4.0,
},
"table": {
"header": {
"background_color": "#FFFFFF",
"text_color": "#111111",
"font_size_in_pt": 11.0,
"font_weight": "bold",
},
"body": {
"background_color": "#FFFFFF",
"text_color": "#111111",
"font_size_in_pt": 11.0,
},
"border": {
"outer": {
"top": {
"color": "#DDDDDD",
"width_in_pt": 0.5,
},
"right": {
"color": "#FFFFFF",
"width_in_pt": 0.0,
},
"bottom": {
"color": "#DDDDDD",
"width_in_pt": 0.5,
},
"left": {
"color": "#FFFFFF",
"width_in_pt": 0.0,
},
},
"inner": {
"horizontal": {
"color": "#DDDDDD",
"width_in_pt": 0.5,
},
"vertical": {
"color": "#FFFFFF",
"width_in_pt": 0.0,
},
},
},
},
"grid": {
"background_color": "#FFFFFF",
"border_color": "#FFFFFF",
"border_width_in_pt": 0.0,
"gap_in_pt": 22.0,
},
"image": {
"border_color": "#FFFFFF",
"border_width_in_pt": 0.0,
},
},
"content": [
{
"type": "grid",
"columns": [
{
"column_span": 8,
"blocks": [
{
"type": "paragraph",
"runs": [
{
"text": ".",
},
],
"styles": {
"font_size_in_pt": 1.0,
"color": "#FFFFFF",
},
},
],
},
{
"column_span": 4,
"horizontal_alignment": "right",
"blocks": [
{
"type": "image",
"buffer": b"<wordmark-png-bytes>",
"width_in_pt": 133.5,
"height_in_pt": 25.5,
"fit": "contain",
},
],
},
],
"styles": {
"gap_in_pt": 0.0,
},
},
{
"type": "paragraph",
"runs": [
{
"text": ".",
},
],
"styles": {
"font_size_in_pt": 45.0,
"color": "#FFFFFF",
},
},
{
"type": "grid",
"columns": [
{
"column_span": 5,
"blocks": [
{
"type": "paragraph",
"text_alignment": "left",
"runs": [
{
"text": "Invoice to",
"font_weight": "bold",
"is_italic": False,
},
],
"styles": {
"font_size_in_pt": 11.0,
},
},
{
"type": "paragraph",
"text_alignment": "left",
"runs": [
{
"text": "Acme Automation GmbH\nExample Street 42\n10115 Berlin\nGermany\n\nVAT: DE123456789",
"font_weight": None,
"is_italic": False,
},
],
"styles": {
"font_size_in_pt": 11.0,
},
},
],
},
{
"column_span": 3,
"blocks": [
{
"type": "paragraph",
"runs": [
{
"text": ".",
},
],
"styles": {
"font_size_in_pt": 1.0,
"color": "#FFFFFF",
},
},
],
},
{
"column_span": 3,
"blocks": [
{
"type": "paragraph",
"text_alignment": "left",
"runs": [
{
"text": "Invoice No.\nDate\nStripe payment\nPaid via",
"font_weight": "bold",
"is_italic": False,
},
],
"styles": {
"font_size_in_pt": 11.0,
},
},
],
},
{
"column_span": 1,
"blocks": [
{
"type": "paragraph",
"text_alignment": "right",
"runs": [
{
"text": "IL-10001\n30.04.2026\npi_sample_123456\nCard",
"font_weight": None,
"is_italic": False,
},
],
"styles": {
"font_size_in_pt": 11.0,
},
},
],
},
],
"styles": {
"gap_in_pt": 0.0,
},
},
{
"type": "paragraph",
"runs": [
{
"text": ".",
},
],
"styles": {
"font_size_in_pt": 10.0,
"color": "#FFFFFF",
},
},
{
"type": "grid",
"columns": [
{
"column_span": 5,
"blocks": [
{
"type": "paragraph",
"runs": [
{
"text": ".",
},
],
"styles": {
"font_size_in_pt": 1.0,
"color": "#FFFFFF",
},
},
],
},
{
"column_span": 7,
"horizontal_alignment": "right",
"blocks": [
{
"type": "paragraph",
"text_alignment": "right",
"runs": [
{
"text": "Iteration Layer",
"font_weight": "bold",
"is_italic": False,
},
],
"styles": {
"font_size_in_pt": 11.0,
},
},
{
"type": "paragraph",
"text_alignment": "right",
"runs": [
{
"text": "Example Software Studio Ltd.",
"font_weight": None,
"is_italic": False,
},
],
"styles": {
"font_size_in_pt": 11.0,
},
},
{
"type": "paragraph",
"text_alignment": "right",
"runs": [
{
"text": "1 Example Avenue\n10001 Example City\nSpain\n\nNIF: ESX0000000X",
"font_weight": None,
"is_italic": False,
},
],
"styles": {
"font_size_in_pt": 11.0,
},
},
],
},
],
"styles": {
"gap_in_pt": 0.0,
},
},
{
"type": "paragraph",
"runs": [
{
"text": ".",
},
],
"styles": {
"font_size_in_pt": 28.0,
"color": "#FFFFFF",
},
},
{
"type": "table",
"column_widths_in_percent": [
10,
56,
17,
17,
],
"header": {
"cells": [
{
"text": "Quantity",
},
{
"text": "Description",
"horizontal_alignment": "center",
},
{
"text": "Price (EUR)",
"horizontal_alignment": "right",
},
{
"text": "Total (EUR)",
"horizontal_alignment": "right",
},
],
},
"rows": [
{
"cells": [
{
"text": "1",
},
{
"text": "Iteration Layer Pro subscription (April 2026)",
},
{
"text": "99,00",
"horizontal_alignment": "right",
},
{
"text": "99,00",
"horizontal_alignment": "right",
},
],
},
{
"cells": [
{
"text": "1250",
},
{
"text": "Pay-as-you-go document generation credits\n1,250 generated document pages beyond included plan usage.",
},
{
"text": "0,50",
"horizontal_alignment": "right",
},
{
"text": "625,00",
"horizontal_alignment": "right",
},
],
},
{
"cells": [
{
"text": "1",
},
{
"text": "Prorated plan adjustment credit",
},
{
"text": "-42,00",
"horizontal_alignment": "right",
},
{
"text": "-42,00",
"horizontal_alignment": "right",
},
],
},
],
},
{
"type": "paragraph",
"runs": [
{
"text": ".",
},
],
"styles": {
"font_size_in_pt": 18.0,
"color": "#FFFFFF",
},
},
{
"type": "grid",
"columns": [
{
"column_span": 7,
"blocks": [
{
"type": "paragraph",
"runs": [
{
"text": ".",
},
],
"styles": {
"font_size_in_pt": 1.0,
"color": "#FFFFFF",
},
},
],
},
{
"column_span": 5,
"blocks": [
{
"type": "table",
"column_widths_in_percent": [
64,
36,
],
"rows": [
{
"cells": [
{
"text": "Subtotal (EUR)",
"horizontal_alignment": "right",
},
{
"text": "682,00",
"horizontal_alignment": "right",
},
],
},
{
"cells": [
{
"text": "IGIC (0%)",
"horizontal_alignment": "right",
},
{
"text": "0,00",
"horizontal_alignment": "right",
},
],
},
{
"cells": [
{
"text": "Total (EUR)",
"horizontal_alignment": "right",
},
{
"text": "682,00",
"horizontal_alignment": "right",
},
],
},
],
},
],
},
],
"styles": {
"gap_in_pt": 0.0,
},
},
],
},
)
Path("IL-10001.pdf").write_bytes(result["buffer"])package main
import (
"log"
"os"
iterationlayer "github.com/iterationlayer/sdk-go"
)
func main() {
client := iterationlayer.NewClient(os.Getenv("ITERATION_LAYER_API_KEY"))
result, err := client.GenerateDocument(iterationlayer.GenerateDocumentRequest{
Format: "pdf",
Document: struct {
Metadata iterationlayer.Metadata `json:"metadata"`
Page iterationlayer.Page `json:"page"`
Styles iterationlayer.DocumentStyles `json:"styles"`
Content []any `json:"content"`
}{
Metadata: iterationlayer.Metadata{
Title: "Invoice IL-10001",
Author: "Iteration Layer",
},
Page: iterationlayer.Page{
Size: iterationlayer.PageSize{Preset: "A4"},
Margins: iterationlayer.Margins{
TopInPt: 56.0,
RightInPt: 44.0,
BottomInPt: 56.0,
LeftInPt: 44.0,
},
},
Styles: iterationlayer.DocumentStyles{
Text: iterationlayer.TextStyle{FontFamily: "PlusJakartaSans", FontSizeInPt: 11.0, LineHeight: 1.28, Color: "#111111"},
Headline: iterationlayer.HeadlineStyle{FontFamily: "Outfit", FontSizeInPt: 18.0, FontWeight: "bold", Color: "#111111", SpacingBeforeInPt: 0.0, SpacingAfterInPt: 4.0},
Table: iterationlayer.TableStyle{
Header: iterationlayer.TableHeaderStyle{BackgroundColor: "#FFFFFF", TextColor: "#111111", FontSizeInPt: 11.0, FontWeight: "bold"},
Body: iterationlayer.TableBodyStyle{BackgroundColor: "#FFFFFF", TextColor: "#111111", FontSizeInPt: 11.0},
Border: iterationlayer.TableBorderStyle{
Outer: iterationlayer.OuterBorderStyle{
Top: iterationlayer.BorderStyle{Color: "#DDDDDD", WidthInPt: 0.5},
Right: iterationlayer.BorderStyle{Color: "#FFFFFF", WidthInPt: 0.0},
Bottom: iterationlayer.BorderStyle{Color: "#DDDDDD", WidthInPt: 0.5},
Left: iterationlayer.BorderStyle{Color: "#FFFFFF", WidthInPt: 0.0},
},
Inner: iterationlayer.InnerBorderStyle{
Horizontal: iterationlayer.BorderStyle{Color: "#DDDDDD", WidthInPt: 0.5},
Vertical: iterationlayer.BorderStyle{Color: "#FFFFFF", WidthInPt: 0.0},
},
},
},
Grid: iterationlayer.GridStyle{BackgroundColor: "#FFFFFF", BorderColor: "#FFFFFF", BorderWidthInPt: 0.0, GapInPt: 22.0},
Image: iterationlayer.ImageStyle{BorderColor: "#FFFFFF", BorderWidthInPt: 0.0},
},
Content: []any{
iterationlayer.GridBlock{
Type: "grid",
Columns: []iterationlayer.GridColumn{
{
ColumnSpan: 8,
Blocks: []any{
iterationlayer.ParagraphBlock{
Type: "paragraph",
Runs: []iterationlayer.ParagraphRun{
{
Text: ".",
},
},
Styles: iterationlayer.TextStyleOverrides{
FontSizeInPt: 1.0,
Color: "#FFFFFF",
},
},
},
},
{
ColumnSpan: 4,
HorizontalAlignment: "right",
Blocks: []any{
iterationlayer.ImageBlock{
Type: "image",
Buffer: []byte("<wordmark-png-bytes>"),
WidthInPt: 133.5,
HeightInPt: 25.5,
Fit: "contain",
},
},
},
},
Styles: iterationlayer.GridStyleOverrides{
GapInPt: 0.0,
},
},
iterationlayer.ParagraphBlock{
Type: "paragraph",
Runs: []iterationlayer.ParagraphRun{
{
Text: ".",
},
},
Styles: iterationlayer.TextStyleOverrides{
FontSizeInPt: 45.0,
Color: "#FFFFFF",
},
},
iterationlayer.GridBlock{
Type: "grid",
Columns: []iterationlayer.GridColumn{
{
ColumnSpan: 5,
Blocks: []any{
iterationlayer.ParagraphBlock{
Type: "paragraph",
TextAlignment: "left",
Runs: []iterationlayer.ParagraphRun{
{
Text: "Invoice to",
FontWeight: "bold",
},
},
Styles: iterationlayer.TextStyleOverrides{
FontSizeInPt: 11.0,
},
},
iterationlayer.ParagraphBlock{
Type: "paragraph",
TextAlignment: "left",
Runs: []iterationlayer.ParagraphRun{
{
Text: "Acme Automation GmbH\nExample Street 42\n10115 Berlin\nGermany\n\nVAT: DE123456789",
},
},
Styles: iterationlayer.TextStyleOverrides{
FontSizeInPt: 11.0,
},
},
},
},
{
ColumnSpan: 3,
Blocks: []any{
iterationlayer.ParagraphBlock{
Type: "paragraph",
Runs: []iterationlayer.ParagraphRun{
{
Text: ".",
},
},
Styles: iterationlayer.TextStyleOverrides{
FontSizeInPt: 1.0,
Color: "#FFFFFF",
},
},
},
},
{
ColumnSpan: 3,
Blocks: []any{
iterationlayer.ParagraphBlock{
Type: "paragraph",
TextAlignment: "left",
Runs: []iterationlayer.ParagraphRun{
{
Text: "Invoice No.\nDate\nStripe payment\nPaid via",
FontWeight: "bold",
},
},
Styles: iterationlayer.TextStyleOverrides{
FontSizeInPt: 11.0,
},
},
},
},
{
ColumnSpan: 1,
Blocks: []any{
iterationlayer.ParagraphBlock{
Type: "paragraph",
TextAlignment: "right",
Runs: []iterationlayer.ParagraphRun{
{
Text: "IL-10001\n30.04.2026\npi_sample_123456\nCard",
},
},
Styles: iterationlayer.TextStyleOverrides{
FontSizeInPt: 11.0,
},
},
},
},
},
Styles: iterationlayer.GridStyleOverrides{
GapInPt: 0.0,
},
},
iterationlayer.ParagraphBlock{
Type: "paragraph",
Runs: []iterationlayer.ParagraphRun{
{
Text: ".",
},
},
Styles: iterationlayer.TextStyleOverrides{
FontSizeInPt: 10.0,
Color: "#FFFFFF",
},
},
iterationlayer.GridBlock{
Type: "grid",
Columns: []iterationlayer.GridColumn{
{
ColumnSpan: 5,
Blocks: []any{
iterationlayer.ParagraphBlock{
Type: "paragraph",
Runs: []iterationlayer.ParagraphRun{
{
Text: ".",
},
},
Styles: iterationlayer.TextStyleOverrides{
FontSizeInPt: 1.0,
Color: "#FFFFFF",
},
},
},
},
{
ColumnSpan: 7,
HorizontalAlignment: "right",
Blocks: []any{
iterationlayer.ParagraphBlock{
Type: "paragraph",
TextAlignment: "right",
Runs: []iterationlayer.ParagraphRun{
{
Text: "Iteration Layer",
FontWeight: "bold",
},
},
Styles: iterationlayer.TextStyleOverrides{
FontSizeInPt: 11.0,
},
},
iterationlayer.ParagraphBlock{
Type: "paragraph",
TextAlignment: "right",
Runs: []iterationlayer.ParagraphRun{
{
Text: "Example Software Studio Ltd.",
},
},
Styles: iterationlayer.TextStyleOverrides{
FontSizeInPt: 11.0,
},
},
iterationlayer.ParagraphBlock{
Type: "paragraph",
TextAlignment: "right",
Runs: []iterationlayer.ParagraphRun{
{
Text: "1 Example Avenue\n10001 Example City\nSpain\n\nNIF: ESX0000000X",
},
},
Styles: iterationlayer.TextStyleOverrides{
FontSizeInPt: 11.0,
},
},
},
},
},
Styles: iterationlayer.GridStyleOverrides{
GapInPt: 0.0,
},
},
iterationlayer.ParagraphBlock{
Type: "paragraph",
Runs: []iterationlayer.ParagraphRun{
{
Text: ".",
},
},
Styles: iterationlayer.TextStyleOverrides{
FontSizeInPt: 28.0,
Color: "#FFFFFF",
},
},
struct {
Type string `json:"type"`
ColumnWidthsInPercent []float64 `json:"column_widths_in_percent,omitempty"`
Header iterationlayer.TableRow `json:"header"`
Rows []iterationlayer.TableRow `json:"rows"`
}{
Type: "table",
ColumnWidthsInPercent: []float64{10, 56, 17, 17},
Header: iterationlayer.TableRow{
Cells: []iterationlayer.TableCell{
{
Text: "Quantity",
},
{
Text: "Description",
HorizontalAlignment: "center",
},
{
Text: "Price (EUR)",
HorizontalAlignment: "right",
},
{
Text: "Total (EUR)",
HorizontalAlignment: "right",
},
},
},
Rows: []iterationlayer.TableRow{
iterationlayer.TableRow{
Cells: []iterationlayer.TableCell{
{
Text: "1",
},
{
Text: "Iteration Layer Pro subscription (April 2026)",
},
{
Text: "99,00",
HorizontalAlignment: "right",
},
{
Text: "99,00",
HorizontalAlignment: "right",
},
},
},
iterationlayer.TableRow{
Cells: []iterationlayer.TableCell{
{
Text: "1250",
},
{
Text: "Pay-as-you-go document generation credits\n1,250 generated document pages beyond included plan usage.",
},
{
Text: "0,50",
HorizontalAlignment: "right",
},
{
Text: "625,00",
HorizontalAlignment: "right",
},
},
},
iterationlayer.TableRow{
Cells: []iterationlayer.TableCell{
{
Text: "1",
},
{
Text: "Prorated plan adjustment credit",
},
{
Text: "-42,00",
HorizontalAlignment: "right",
},
{
Text: "-42,00",
HorizontalAlignment: "right",
},
},
},
},
},
iterationlayer.ParagraphBlock{
Type: "paragraph",
Runs: []iterationlayer.ParagraphRun{
{
Text: ".",
},
},
Styles: iterationlayer.TextStyleOverrides{
FontSizeInPt: 18.0,
Color: "#FFFFFF",
},
},
iterationlayer.GridBlock{
Type: "grid",
Columns: []iterationlayer.GridColumn{
{
ColumnSpan: 7,
Blocks: []any{
iterationlayer.ParagraphBlock{
Type: "paragraph",
Runs: []iterationlayer.ParagraphRun{
{
Text: ".",
},
},
Styles: iterationlayer.TextStyleOverrides{
FontSizeInPt: 1.0,
Color: "#FFFFFF",
},
},
},
},
{
ColumnSpan: 5,
Blocks: []any{
struct {
Type string `json:"type"`
ColumnWidthsInPercent []float64 `json:"column_widths_in_percent,omitempty"`
Rows []iterationlayer.TableRow `json:"rows"`
}{
Type: "table",
ColumnWidthsInPercent: []float64{64, 36},
Rows: []iterationlayer.TableRow{
iterationlayer.TableRow{
Cells: []iterationlayer.TableCell{
{
Text: "Subtotal (EUR)",
HorizontalAlignment: "right",
},
{
Text: "682,00",
HorizontalAlignment: "right",
},
},
},
iterationlayer.TableRow{
Cells: []iterationlayer.TableCell{
{
Text: "IGIC (0%)",
HorizontalAlignment: "right",
},
{
Text: "0,00",
HorizontalAlignment: "right",
},
},
},
iterationlayer.TableRow{
Cells: []iterationlayer.TableCell{
{
Text: "Total (EUR)",
HorizontalAlignment: "right",
},
{
Text: "682,00",
HorizontalAlignment: "right",
},
},
},
},
},
},
},
},
Styles: iterationlayer.GridStyleOverrides{
GapInPt: 0.0,
},
},
},
},
})
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("IL-10001.pdf", result.Buffer, 0o644); err != nil {
log.Fatal(err)
}
}The useful part is the document shape. The header is a grid because the logo, customer address, and invoice metadata need independent alignment without drifting vertically. The line items are a table because quantities, descriptions, unit prices, and totals need consistent columns across short and long rows. The totals are another table because currency amounts should align to the same edge as the line-item totals.
That is where a structured renderer matters. A business document is not just text on a page. It needs layout primitives that understand pages, spacing, tables, images, fonts, alignment, and wrapping.
Details That Make Invoices Hard
Invoice layout looks simple until real data arrives.
Customer addresses can be one line or six lines. Tax IDs are optional for some customers and mandatory for others. Usage descriptions can wrap onto multiple lines. Credits need to render as negative amounts without looking like a formatting bug. Currency belongs in the column header, not repeated in every cell. Totals need to remain visually tied to the line-item table even when the table grows.
Those constraints pushed the renderer in useful directions. The invoice work required nested grid handling, correct text style resolution, explicit newline rendering, and image snapshot tests for the final PDF output. These are not invoice-only problems. They are the same problems that appear in reports, statements, contracts, certificates, and other operational documents.
That feedback loop is valuable. Building a real document against the API exposes rendering edge cases faster than a gallery of perfect demo templates. The API has to handle the uncomfortable parts: long text, nested layout, repeated visual patterns, and values that must be displayed exactly.
Generate After Payment
The PDF is generated after Stripe confirms payment.
The webhook receives invoice.paid and queues a background job. The job creates or reuses the invoice record, calculates the tax result from the billing snapshot, builds the document-generation request, renders the PDF, stores it, and makes it available in the billing dashboard.
That keeps the webhook fast. It also makes the system safe to retry. The invoice number is reserved in the database. The Stripe invoice ID is unique. The generated PDF is stored with the invoice record, so customers download the invoice we issued rather than a Stripe-hosted document.
Google Drive is only an accounting archive for the generated PDF. The invoice record and stored PDF in our system remain the source of truth.
The Pattern to Copy
The useful pattern is not specific to invoices.
If you generate documents that customers, auditors, partners, or internal teams rely on, start with the business record. Then render that record into a document using primitives that match the document domain.
For invoices, that means invoice numbers, tax rules, line items, and payment references before layout. For reports, it might mean measured results and chart data before presentation. For certificates, it might mean recipient identity and issuance rules before typography.
This is also why the best document generation API is not just a template renderer. It needs page-aware layout, tables, images, fonts, deterministic output, and enough structure to fit into real workflows.
We used the same principle when we built our pitch deck with our own API: the document comes from code and structured data, not manual design work. For invoices, the stakes are higher because the document has to be correct.