Building an AI-Powered PDF Generator: Claude API + FormaTeX LaTeX

Discover how to create a robust AI PDF generator using Claude API and FormaTeX, featuring practical examples like invoices and certificates.

Building an AI-Powered PDF Generator: Claude API + FormaTeX LaTeX

By Elhassane Mehdioui, AI Consultant — June 2026


Generating professional PDFs programmatically has always been a friction-heavy problem. HTML-to-PDF tools like Puppeteer or WeasyPrint are convenient but produce output that looks like a webpage forced into paper dimensions. For documents where layout precision matters — invoices, certificates, academic papers, legal contracts — LaTeX remains the gold standard. The challenge: writing correct LaTeX by hand is tedious, and getting an LLM to generate valid LaTeX reliably has historically required heavy post-processing.

This post walks through a production-ready architecture that solves both problems: Claude API generates the LaTeX, and FormaTeX's smart compile endpoint automatically fixes common LLM errors before rendering the PDF. The result is an AI PDF generator pipeline that is fast, reliable, and produces publication-quality output.


Why LaTeX Beats HTML-to-PDF for Serious Documents

Before diving into code, it is worth understanding why this architecture uses LaTeX rather than generating HTML and converting it with a headless browser.

Typography and layout control. LaTeX was designed for typesetting. It handles kerning, hyphenation, widow/orphan control, and multi-column layouts with a precision that CSS cannot match without significant custom work. For an invoice with perfectly aligned columns or an academic paper with correctly formatted equations, LaTeX is the correct tool.

Deterministic pagination. HTML-to-PDF tools make layout decisions that vary across library versions and system fonts. LaTeX compilation is deterministic: the same source always produces the same PDF, on any machine, at any time.

Structured semantic markup. LaTeX documents are structured documents, not visual descriptions. This makes them easier for an LLM to reason about. When Claude writes \section{Introduction} it is expressing document structure, not visual styling — which aligns well with how language models represent knowledge.

Native math and scientific notation. For academic papers or technical reports, LaTeX's math rendering is unmatched. No HTML-to-PDF pipeline comes close.


Architecture Overview

The pipeline has three stages:

User Input / Business Data
        |
        v
  Claude API (claude-sonnet-4-5)
  Prompt: "Generate LaTeX for this invoice"
        |
        v
  FormaTeX Smart Compile
  POST https://api.formatex.io/api/v1/compile/smart
  Auto-fix pipeline patches LLM errors
        |
        v
  PDF binary (returned in response body)
  X-Auto-Fixes-Applied header for observability

Claude handles the creative and contextual reasoning — understanding what the document should contain, formatting currency correctly, structuring sections logically. FormaTeX handles the brittle part: actually compiling LaTeX into a PDF, with an auto-fix pipeline that catches the common mistakes LLMs make when generating LaTeX source.


Setting Up the Project

mkdir ai-pdf-generator && cd ai-pdf-generator
npm init -y
npm install @anthropic-ai/sdk axios dotenv
npm install -D typescript @types/node ts-node

Create a tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}

Create a .env file:

ANTHROPIC_API_KEY=your_anthropic_api_key
FORMATEX_API_KEY=your_formatex_api_key

Core Pipeline: Claude Generates LaTeX, FormaTeX Compiles It

Create src/pdfPipeline.ts:

import Anthropic from "@anthropic-ai/sdk";
import axios from "axios";
import * as fs from "fs";

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
});

const FORMATEX_BASE_URL = "https://api.formatex.io/api/v1";

interface CompileResult {
  pdf: Buffer;
  autoFixesApplied: string[];
  compilationTimeMs: number;
}

async function generateLatexWithClaude(
  documentType: string,
  data: Record<string, unknown>
): Promise<string> {
  const systemPrompt = `You are a LaTeX document generation expert. Generate complete, compilable LaTeX documents.
Always use standard packages only: geometry, fontenc, inputenc, booktabs, array, xcolor, hyperref, graphicx.
Never use custom or obscure packages. Always include \\documentclass and \\begin{document}...\\end{document}.
Return only the raw LaTeX source, no markdown fences, no explanation.`;

  const userPrompt = `Generate a professional ${documentType} in LaTeX with the following data:
${JSON.stringify(data, null, 2)}

Requirements:
- Clean, professional layout
- All monetary values formatted with proper currency symbols
- Proper table alignment for line items
- Company branding section at the top
- Return ONLY the LaTeX source code`;

  const message = await anthropic.messages.create({
    model: "claude-sonnet-4-5",
    max_tokens: 4096,
    messages: [{ role: "user", content: userPrompt }],
    system: systemPrompt,
  });

  const content = message.content[0];
  if (content.type !== "text") {
    throw new Error("Unexpected response type from Claude");
  }

  // Strip markdown code fences if Claude included them despite instructions
  return content.text
    .replace(/^```latex\n?/i, "")
    .replace(/^```\n?/, "")
    .replace(/\n?```$/, "")
    .trim();
}

async function compileWithFormaTeX(latexSource: string): Promise<CompileResult> {
  const startTime = Date.now();

  const response = await axios.post(
    `${FORMATEX_BASE_URL}/compile/smart`,
    { source: latexSource },
    {
      headers: {
        Authorization: `Bearer ${process.env.FORMATEX_API_KEY}`,
        "Content-Type": "application/json",
        Accept: "application/pdf",
      },
      responseType: "arraybuffer",
    }
  );

  const autoFixesHeader = response.headers["x-auto-fixes-applied"] as string;
  const autoFixesApplied = autoFixesHeader
    ? autoFixesHeader.split(",").map((s: string) => s.trim())
    : [];

  return {
    pdf: Buffer.from(response.data),
    autoFixesApplied,
    compilationTimeMs: Date.now() - startTime,
  };
}

export async function generatePDF(
  documentType: string,
  data: Record<string, unknown>,
  outputPath: string
): Promise<void> {
  console.log(`Generating ${documentType} with Claude...`);
  const latex = await generateLatexWithClaude(documentType, data);

  console.log("Compiling with FormaTeX smart compile...");
  const result = await compileWithFormaTeX(latex);

  if (result.autoFixesApplied.length > 0) {
    console.log(`Auto-fixes applied: ${result.autoFixesApplied.join(", ")}`);
  }

  console.log(`Compiled in ${result.compilationTimeMs}ms`);
  fs.writeFileSync(outputPath, result.pdf);
  console.log(`PDF saved to ${outputPath}`);
}

The Smart Compile Auto-Fix Pipeline

The X-Auto-Fixes-Applied response header is one of FormaTeX's most valuable features for AI-generated LaTeX. When Claude produces LaTeX with common errors, the smart compile endpoint applies a pipeline of automatic patches before the final pdflatex (or xelatex) pass.

Typical fixes the pipeline handles for LLM-generated output include:

  • Unescaped special characters&, %, $, # appearing in text nodes where they need to be escaped as \&, \%, \$, \#
  • Missing package imports — when generated code uses \toprule from booktabs without importing it
  • Malformed table column specs|l|c|r| style when the content requires different sizing
  • Smart quote substitution — LLMs often emit Unicode curly quotes (", ") that need conversion to LaTeX '' and ````
  • Overfull hbox warnings promoted to errors — relaxing \hfuzz when content overflows a column

Logging these fixes gives you an observability signal: if a particular document type consistently triggers the same auto-fix, you should update your Claude prompt to avoid that error class. Over time, this feedback loop tightens your prompts and reduces compilation time.


Template System for Variable Substitution

For high-volume use cases like invoice generation, you want to separate the structural LaTeX template from the variable data. Create src/templateEngine.ts:

export interface TemplateVariable {
  key: string;
  value: string | number;
}

export function applyTemplate(
  template: string,
  variables: TemplateVariable[]
): string {
  let result = template;
  for (const variable of variables) {
    const placeholder = `{{${variable.key}}}`;
    result = result.replaceAll(
      placeholder,
      escapeLatex(String(variable.value))
    );
  }
  return result;
}

function escapeLatex(text: string): string {
  return text
    .replace(/\\/g, "\\textbackslash{}")
    .replace(/&/g, "\\&")
    .replace(/%/g, "\\%")
    .replace(/\$/g, "\\$")
    .replace(/#/g, "\\#")
    .replace(/_/g, "\\_")
    .replace(/\{/g, "\\{")
    .replace(/\}/g, "\\}")
    .replace(/~/g, "\\textasciitilde{}")
    .replace(/\^/g, "\\textasciicircum{}");
}

Then store a template LaTeX file (e.g., templates/invoice.tex) with {{client_name}}, {{invoice_number}}, {{line_items}} placeholders. For bulk generation — thousands of invoices — this is significantly faster than calling Claude for every document, since Claude is only called once to create or refine the base template.


Use Case 1: AI Invoice Generator

// src/examples/invoiceGenerator.ts
import { generatePDF } from "../pdfPipeline";

const invoiceData = {
  invoiceNumber: "INV-2026-0042",
  issueDate: "June 15, 2026",
  dueDate: "July 15, 2026",
  vendor: { name: "Intelligentb2b", address: "Casablanca, Morocco" },
  client: { name: "Acme Corp", address: "New York, NY, USA" },
  lineItems: [
    { description: "AI Consulting - Strategy Workshop", qty: 2, unit: 1800, total: 3600 },
    { description: "LLM Integration Development", qty: 40, unit: 150, total: 6000 },
    { description: "Deployment & Documentation", qty: 1, unit: 1200, total: 1200 },
  ],
  subtotal: 10800,
  tax: 0,
  total: 10800,
  currency: "USD",
};

generatePDF("professional invoice", invoiceData, "./output/invoice-0042.pdf");

Use Case 2: Certificate Generator

const certificateData = {
  recipientName: "Sarah Johnson",
  courseName: "Advanced Prompt Engineering",
  completionDate: "June 15, 2026",
  instructorName: "Elhassane Mehdioui",
  organizationName: "Intelligentb2b Academy",
  certificateId: "CERT-2026-PE-1087",
};

generatePDF(
  "completion certificate with decorative border and formal typography",
  certificateData,
  "./output/certificate-sarah-johnson.pdf"
);

Use Case 3: Academic Paper Formatter

For academic output, extend the system prompt to specify formatting requirements:

const paperData = {
  title: "Retrieval-Augmented Generation in Enterprise Knowledge Bases",
  authors: ["Elhassane Mehdioui", "Co-Author Name"],
  abstract: "This paper examines...",
  sections: ["Introduction", "Related Work", "Methodology", "Results", "Conclusion"],
  citations: [...],
};

generatePDF(
  "academic paper formatted for IEEE conference proceedings with two-column layout",
  paperData,
  "./output/paper-rag-enterprise.pdf"
);

Error Handling and Observability

Robust error handling distinguishes a production pipeline from a prototype:

async function generatePDFWithRetry(
  documentType: string,
  data: Record<string, unknown>,
  outputPath: string,
  maxRetries = 2
): Promise<void> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      await generatePDF(documentType, data, outputPath);
      return;
    } catch (error) {
      if (axios.isAxiosError(error) && error.response?.status === 422) {
        // Unprocessable Entity — LaTeX error the auto-fix pipeline could not resolve
        console.error(`LaTeX compilation failed on attempt ${attempt + 1}`);
        if (attempt === maxRetries) throw error;
        // On retry, ask Claude to simplify the document structure
        console.log("Retrying with simplified layout instructions...");
      } else {
        throw error;
      }
    }
  }
}

Track these metrics in production: auto-fix rate per document type, Claude token usage per document, FormaTeX compilation latency, and retry rate. A rising auto-fix rate for a specific document type signals prompt drift — usually caused by changes in the underlying model's LaTeX generation style.


Performance Considerations

For high-throughput scenarios, batch Claude calls and compile in parallel:

const pdfPromises = invoiceBatch.map((invoice) =>
  generatePDF("invoice", invoice, `./output/invoice-${invoice.id}.pdf`)
);
await Promise.all(pdfPromises);

For very high volume (thousands of documents per day), use the template approach: call Claude once to generate or refine the base template, then use applyTemplate() for substitution and call FormaTeX directly — bypassing Claude entirely for routine batch generation.


Conclusion

The combination of Claude API for LaTeX generation and FormaTeX's smart compile endpoint for auto-fixed PDF compilation creates a robust, production-ready AI PDF generator. The architecture respects the strengths of each component: Claude excels at reasoning about document structure and content; FormaTeX handles the error-prone mechanics of LaTeX compilation with a purpose-built auto-fix pipeline.

This stack powers invoice generators, certificate generators, and academic formatters that produce output indistinguishable from hand-authored LaTeX — with none of the manual effort. The X-Auto-Fixes-Applied observability hook gives you a continuous feedback loop to improve your prompts over time.

Start with the invoice generator example, instrument the auto-fix header, and let the data tell you where your prompts need refinement. Within a few iterations, you will have a pipeline that handles your document types reliably with minimal manual intervention.


Elhassane Mehdioui is an AI consultant specialising in LLM integration and document automation. He works with enterprises to build reliable, production-grade AI pipelines.

Need a React or Next.js interface?

Modern, responsive front-ends built to perform.

I build fast, accessible UIs with React, Next.js, TypeScript and Tailwind CSS.

HassanOSSYS-00
SECURE CHANNEL · ACTIVE
INIT://