\begin{article}
LaTeX API Document Templates That Scale
Build reusable LaTeX document templates for the API — invoices, reports, certificates — with variable substitution and consistent branding.

If you are generating more than one type of PDF — invoices, quarterly reports, client certificates — writing one-off LaTeX strings is a trap. The moment a brand color changes or a legal footer needs updating, you are editing ten files. The right approach is a template system: parameterized .tex files that accept data, produce consistent output, and compile reliably through the FormatEx API. This guide shows you exactly how to build one.
Structuring a LaTeX Template for API Usage
A good API template is a .tex file that makes no assumptions about its own data. Every value that changes between documents — recipient name, date, total amount, company logo path — is expressed as a TeX macro that gets defined before the template is included.
The cleanest pattern is a two-layer file structure:
templates/
invoice/
invoice.cls # Custom document class — layout, fonts, colors
template.tex # Document body — uses \InvoiceNumber, \ClientName, etc.
report/
report.cls
template.tex
shared/
brand.sty # Shared color palette, typography, logo macrosThe template.tex file never hardcodes data. It only calls macros:
% templates/invoice/template.tex
\documentclass{invoice}
\input{brand}
\begin{document}
\MakeHeader{\CompanyName}{\CompanyLogoPath}
\section*{Invoice \#\InvoiceNumber}
\begin{tabular}{ll}
\textbf{Bill To:} & \ClientName \\
\textbf{Date:} & \InvoiceDate \\
\textbf{Due:} & \DueDate \\
\end{tabular}
\LineItems
\vspace{1em}
\begin{flushright}
\textbf{Total: \TotalAmount}
\end{flushright}
\Footer
\end{document}The calling code defines those macros, then includes the template. This separation means the template file never changes when data changes — only the wrapper does.
Variable Injection Patterns
There are three common ways to inject variables into a LaTeX template before sending it to the FormatEx API. Each has trade-offs.
Pattern 1: TeX Macro Prepending
Prepend \newcommand definitions ahead of your template source. The template file stays completely static; your application builds the preamble dynamically.
function buildInvoiceLaTeX(data: InvoiceData, templateSource: string): string {
const macros = [
`\\newcommand{\\InvoiceNumber}{${escapeLaTeX(data.invoiceNumber)}}`,
`\\newcommand{\\ClientName}{${escapeLaTeX(data.clientName)}}`,
`\\newcommand{\\InvoiceDate}{${escapeLaTeX(data.date)}}`,
`\\newcommand{\\DueDate}{${escapeLaTeX(data.dueDate)}}`,
`\\newcommand{\\TotalAmount}{${escapeLaTeX(data.totalAmount)}}`,
`\\newcommand{\\LineItems}{${buildLineItemsTable(data.lineItems)}}`,
].join("\n");
return macros + "\n" + templateSource;
}This is the most LaTeX-native approach. The template compiles identically whether run locally or via the API.
Pattern 2: String Interpolation with Escaping
Replace placeholder tokens (e.g. {{CLIENT_NAME}}) using a simple find-and-replace over the template string. Fast to implement, but you must escape LaTeX special characters in user-supplied data.
const LATEX_SPECIAL_CHARS: [RegExp, string][] = [
[/\\/g, "\\textbackslash{}"],
[/&/g, "\\&"],
[/%/g, "\\%"],
[/\$/g, "\\$"],
[/#/g, "\\#"],
[/_/g, "\\_"],
[/\{/g, "\\{"],
[/\}/g, "\\}"],
[/~/g, "\\textasciitilde{}"],
[/\^/g, "\\textasciicircum{}"],
];
function escapeLaTeX(value: string): string {
return LATEX_SPECIAL_CHARS.reduce(
(str, [pattern, replacement]) => str.replace(pattern, replacement),
value
);
}
function renderTemplate(template: string, vars: Record<string, string>): string {
return Object.entries(vars).reduce(
(src, [key, value]) =>
src.replaceAll(`{{${key}}}`, escapeLaTeX(value)),
template
);
}The escape function above covers all ten LaTeX special characters. Missing even one — a stray & in a company name, for example — causes a compilation error.
Pattern 3: Jinja2 / Nunjucks Templates (Server-Side Rendering)
For Python backends or Node.js with Nunjucks, you can use a full templating engine with loops, conditionals, and filters. This is the most powerful approach for complex documents like multi-section reports.
from jinja2 import Environment, FileSystemLoader
import requests, os
env = Environment(
loader=FileSystemLoader("templates/"),
variable_start_string=r"\VAR{",
variable_end_string="}",
block_start_string=r"\BLOCK{",
block_end_string="}",
comment_start_string=r"\#{",
comment_end_string="}",
)
def compile_invoice(data: dict) -> bytes:
template = env.get_template("invoice/template.tex")
latex_source = template.render(**data)
response = requests.post(
"https://api.formatex.io/api/v1/compile",
headers={
"X-API-Key": os.environ["FORMATEX_API_KEY"],
"Content-Type": "application/json",
},
json={"latex": latex_source, "engine": "pdflatex"},
)
response.raise_for_status()
return response.content # raw PDF bytesUsing non-standard delimiters (\VAR{...} instead of {{ }}), the Jinja2 template remains valid LaTeX that you can open in a local editor without syntax errors.
Building a Shared Style File
Brand consistency across document types is easiest to enforce through a shared .sty package. Define your colors, fonts, and recurring macros once, then \usepackage it in every template.
% shared/brand.sty
\ProvidesPackage{brand}
\usepackage{xcolor}
\usepackage{fontspec} % xelatex/lualatex only
\usepackage{graphicx}
\usepackage{hyperref}
% Brand colors
\definecolor{BrandBlue}{HTML}{1A56DB}
\definecolor{BrandGray}{HTML}{6B7280}
\definecolor{BrandLight}{HTML}{F9FAFB}
% Typography
\setmainfont{Inter}[
BoldFont = Inter Bold,
ItalicFont = Inter Italic
]
% Reusable macros
\newcommand{\MakeHeader}[2]{%
\begin{minipage}{0.5\textwidth}
\includegraphics[height=1.5cm]{#2}
\end{minipage}%
\begin{minipage}{0.5\textwidth}
\raggedleft\large\color{BrandGray}#1
\end{minipage}
\vspace{1em}\hrule\vspace{1em}
}
\newcommand{\Footer}{%
\vfill
\hrule\vspace{0.5em}
\small\color{BrandGray}\centering
\CompanyAddress\ $\cdot$\ \CompanyEmail
}When this file ships with every template directory in your API request (or is embedded inline before the \documentclass), every document shares the same visual language. Updating a brand color means editing one file — brand.sty — not hunting through ten templates.
Note: brand.sty uses fontspec, which requires xelatex or lualatex. On Developer plan and above you have access to all four FormatEx engines. Pass "engine": "xelatex" in your compile request when using custom fonts. For a detailed comparison of when to choose XeLaTeX over pdfLaTeX for font and Unicode support, see XeLaTeX vs pdfLaTeX.
Version-Controlling Templates
Templates are code. They belong in version control alongside the application that calls them. A practical layout for a Node.js and TypeScript LaTeX PDF generation service:
src/
pdf/
templates/
invoice/
template.tex
invoice.cls
report/
template.tex
report.cls
shared/
brand.sty
compiler.ts # FormatEx API client
builder.ts # Per-template data → LaTeX source functions
escape.ts # LaTeX escaping utilitiesKeep the template files as literal .tex files in your repository, not as JavaScript string literals. Your editor, linter, and diff tooling will thank you. Load them at runtime with fs.readFileSync.
import { readFileSync } from "fs";
import { join } from "path";
const TEMPLATES_DIR = join(__dirname, "templates");
export function loadTemplate(name: string): string {
return readFileSync(join(TEMPLATES_DIR, name, "template.tex"), "utf-8");
}For the shared style file, the simplest approach is to inline it at the top of the source string you send to the API — no multi-file upload complexity required. If your templates grow to include bibliography files or custom class files, see multi-file LaTeX documents via API for how to send .tex, .bib, and .cls files together in a single request:
export function buildSource(templateName: string, preamble: string): string {
const brandSty = readFileSync(
join(TEMPLATES_DIR, "shared", "brand.sty"),
"utf-8"
);
const template = loadTemplate(templateName);
// Embed brand.sty so the API compiler finds it
return `\\makeatletter\n${brandSty}\n\\makeatother\n${preamble}\n${template}`;
}Tag template releases. If you change the invoice layout in a way that breaks clients on the old version, having v1.2.0 pinned in git means you can still compile historical invoices against the old template while new invoices use the new one.
Testing Templates via the FormatEx API
Template testing is compilation testing. The fastest feedback loop is a test script that compiles each template with fixture data and asserts the response is a valid PDF.
// tests/templates.test.ts
import { describe, it, expect } from "vitest";
const API_URL = "https://api.formatex.io/api/v1/compile";
const API_KEY = process.env.FORMATEX_API_KEY!;
const INVOICE_FIXTURE = {
invoiceNumber: "INV-0042",
clientName: "Acme Corp",
date: "2026-06-14",
dueDate: "2026-07-14",
totalAmount: "\\$4{,}200.00",
lineItems: [
{ description: "API Integration", qty: 1, unit: 4200 },
],
};
describe("invoice template", () => {
it("compiles to a valid PDF", async () => {
const latex = buildInvoiceLaTeX(INVOICE_FIXTURE, loadTemplate("invoice"));
const res = await fetch(API_URL, {
method: "POST",
headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ latex, engine: "pdflatex" }),
});
expect(res.status).toBe(200);
expect(res.headers.get("content-type")).toBe("application/pdf");
const pdf = await res.arrayBuffer();
// PDF magic bytes: %PDF-
const header = Buffer.from(pdf.slice(0, 5)).toString("ascii");
expect(header).toBe("%PDF-");
});
it("handles special characters in client name", async () => {
const data = { ...INVOICE_FIXTURE, clientName: "Smith & Sons — 100% LLC" };
const latex = buildInvoiceLaTeX(data, loadTemplate("invoice"));
const res = await fetch(API_URL, {
method: "POST",
headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ latex, engine: "pdflatex" }),
});
expect(res.status).toBe(200);
});
});Run these tests in CI on every pull request that touches the templates/ directory. A compilation failure from a template change surfaces immediately — not in production when a customer tries to download their invoice.
Comparison: Template Injection Patterns
| Pattern | Best for | Special char safety | Complexity |
|---|---|---|---|
| TeX macro prepending | Static layout, dynamic data | Yes (TeX handles it) | Low |
| Token replacement | Simple templates, quick setup | Manual (escape function required) | Low |
| Jinja2 / Nunjucks | Complex logic, loops, conditionals | Manual (custom filter needed) | Medium |
Custom .cls file | Multi-document brand system | N/A (layout only) | High |
For most API-driven document generation — invoices, certificates, simple reports — macro prepending is the right default. It keeps your template files valid standalone LaTeX, makes diffs readable, and requires no extra dependencies.
Putting It All Together
A production template pipeline for a billing system looks like this:
- The application receives a webhook (subscription renewed, invoice due).
- It fetches the invoice data from the database.
buildInvoiceLaTeX(data, loadTemplate("invoice"))produces a full LaTeX source string.- The source is POSTed to
https://api.formatex.io/api/v1/compilewithX-API-Keyand"engine": "pdflatex". - The raw PDF response is stored in object storage (S3, R2) and the URL is emailed to the customer.
The FormatEx API handles the TeX Live runtime, engine selection, and file cleanup. Your application owns only the data and the template — which is exactly the right division of responsibility.
If you are generating PDFs at scale — thousands of invoices per month, batch report runs, certificate issuance — the Developer or Pro plan gives you higher monthly compilation limits and access to all four engines, including xelatex for Unicode-heavy documents and latexmk for multi-pass bibliography compilations. For high-volume scenarios, see bulk LaTeX PDF generation via API for concurrency patterns and retry logic.
Start with a free account, build your first template, and POST it to the API today at formatex.io.
Related Articles
- LaTeX PDF Invoice Generator API — Step-by-step guide to building professional branded PDF invoices using LaTeX templates via the API
- LaTeX Certificate Generation at Scale — Batch-generate certificate PDFs from a single template with variable substitution and bulk API patterns
- Multi-File LaTeX Documents via API — Send
.tex,.bib, and.clsfiles together in one request — essential for complex template projects - LaTeX PDF Generation in Node.js and TypeScript — Full TypeScript client with error handling for integrating the FormatEx API into a Node.js service
- Bulk LaTeX PDF Generation via API — Concurrency, retry logic, and queuing strategies for generating thousands of PDFs from templates at scale
\end{article}
\related{posts}




