SaaS Invoice Generator with LaTeX and FormaTeX API: Complete Tutorial

Learn how to build a production-ready SaaS invoice generator using LaTeX and the FormaTeX API in Node.js. No TeX Live required — covers template setup, dynamic data injection, bulk async generation, webhooks, and caching for scale.

SaaS Invoice Generator with LaTeX and FormaTeX API: Complete Tutorial

By Elhassane Mehdioui


If you have ever tried to generate professional PDF invoices in a SaaS application, you have probably gone down the HTML-to-PDF rabbit hole — Puppeteer, wkhtmltopdf, WeasyPrint. They work, but they all share the same fundamental weakness: HTML and CSS were never designed for print-quality document layout. Columns drift, page breaks land in the wrong place, and font rendering is inconsistent across environments.

LaTeX was designed for exactly this job. It has been the gold standard for typeset documents for decades, and the output quality is simply not comparable. The problem has always been the operational overhead: installing and maintaining TeX Live on every server, managing package dependencies, handling compilation errors, and dealing with slow cold-start times at scale.

FormaTeX solves every one of those problems. It is a REST API that compiles LaTeX documents in the cloud — no TeX Live installation, no shell commands, no tex binary on your server. You send a template and variables, and you get back a PDF. This tutorial walks through building a complete SaaS invoice generator on top of it.


Why LaTeX Beats HTML-to-PDF for Invoices

Before touching any code, it is worth being specific about what you gain.

Pixel-perfect typography. LaTeX uses the TeX typesetting engine, which applies the Knuth-Plass line-breaking algorithm. Every word is placed with mathematical precision. The result looks like it came out of a professional design tool, not a browser.

Reliable multi-page layout. Invoice line items can run long. LaTeX handles longtable and page breaks deterministically. With HTML-to-PDF you will spend hours fighting page-break edge cases.

Font control. XeLaTeX and LuaLaTeX support OpenType and TrueType fonts natively. You can embed your brand typeface without fighting CSS @font-face quirks.

Stable output. Given the same inputs, LaTeX produces byte-identical output. This matters for auditing, caching, and deduplication — which FormaTeX's Max+ plan exploits directly.

No headless browser. Puppeteer requires a full Chromium install. It is heavy, slow to cold-start, and a security surface area you do not need.


FormaTeX API Overview

FormaTeX exposes a straightforward REST API with a few key concepts:

  • Template API — Upload a .tex file with named variables (((variable_name))). Call the compile endpoint with a JSON payload of substitutions and receive a PDF.
  • File uploads (Pro) — Upload custom logos and fonts once; reference them by asset ID in your templates.
  • Async compilation — Submit a job and receive a job ID. Poll or use a webhook for the result. Essential for bulk invoice runs.
  • Webhooks — FormaTeX POSTs the compiled PDF URL (or error payload) to your endpoint when compilation finishes.
  • Caching (Max+) — When you submit the same document content twice, FormaTeX returns the cached PDF instantly without consuming quota. Identical invoices (re-downloads, duplicates) become free.

The base URL is https://api.formatex.dev/v1. All requests are authenticated with an Authorization: Bearer YOUR_API_KEY header.


Setting Up Your FormaTeX Account

  1. Sign up at formatex.dev and copy your API key from the dashboard.
  2. If you are on the Pro plan, navigate to Assets and upload your company logo. Note the returned asset_id.
  3. Set your webhook URL under Settings > Webhooks if you plan to use async compilation (recommended for production).

The LaTeX Invoice Template

Save the following as invoice.tex. The ((double-paren)) syntax is FormaTeX's variable substitution marker.

\documentclass[11pt, a4paper]{article}
\usepackage[margin=2.5cm]{geometry}
\usepackage{booktabs}
\usepackage{longtable}
\usepackage{array}
\usepackage{xcolor}
\usepackage{graphicx}
\usepackage{microtype}
\usepackage{fontspec}

\definecolor{brand}{HTML}{((brand_color))}
\setmainfont{Inter}

\begin{document}
\pagestyle{empty}

% Header
\noindent
\begin{minipage}[t]{0.5\textwidth}
  \includegraphics[height=2cm]{((logo_asset_id))}\\[0.4cm]
  \textbf{((company_name))}\\
  ((company_address))\\
  ((company_email))
\end{minipage}
\hfill
\begin{minipage}[t]{0.4\textwidth}
  \raggedleft
  {\Large\color{brand}\textbf{INVOICE}}\\[0.3cm]
  \textbf{Invoice \#:} ((invoice_number))\\
  \textbf{Date:} ((invoice_date))\\
  \textbf{Due:} ((due_date))
\end{minipage}

\vspace{1cm}
\noindent\rule{\linewidth}{0.4pt}
\vspace{0.5cm}

% Bill To
\noindent\textbf{Bill To:}\\
((client_name))\\
((client_address))\\
((client_email))

\vspace{0.8cm}

% Line items
\begin{longtable}{p{7cm} r r r}
\toprule
\textbf{Description} & \textbf{Qty} & \textbf{Unit Price} & \textbf{Amount} \\
\midrule
\endhead
((line_items))
\bottomrule
\end{longtable}

\vspace{0.3cm}

% Totals
\hfill
\begin{minipage}{6cm}
  \begin{tabular}{lr}
    Subtotal & \$((subtotal)) \\
    Tax (((tax_rate))\%) & \$((tax_amount)) \\
    \midrule
    \textbf{Total} & \textbf{\$((total))} \\
  \end{tabular}
\end{minipage}

\vspace{1.5cm}
\noindent\small((payment_terms))

\end{document}

Upload this template to FormaTeX:

curl -X POST https://api.formatex.dev/v1/templates \
  -H "Authorization: Bearer $FORMATEX_API_KEY" \
  -F "file=@invoice.tex" \
  -F "name=saas-invoice-v1"

The response includes a template_id. Store this in your environment config — you will use it for every compilation request.


Dynamic Data Injection

FormaTeX's Template API accepts a variables object. You map your application data to the template's named placeholders.

The ((line_items)) placeholder receives pre-rendered LaTeX rows. Build them server-side from your invoice line items array:

function buildLineItemsLatex(items) {
  return items
    .map(
      ({ description, quantity, unitPrice, amount }) =>
        `${description} & ${quantity} & \\$${unitPrice.toFixed(2)} & \\$${amount.toFixed(2)} \\\\`
    )
    .join('\n');
}

This approach gives you full control over row formatting without complicating the template itself.


Full Node.js Express Route Returning PDF

Install dependencies:

npm install express axios dotenv

Create src/routes/invoices.js:

const express = require('express');
const axios = require('axios');
const router = express.Router();

const FORMATEX_API = 'https://api.formatex.dev/v1';
const FORMATEX_KEY = process.env.FORMATEX_API_KEY;
const TEMPLATE_ID = process.env.FORMATEX_TEMPLATE_ID;

function buildLineItemsLatex(items) {
  return items
    .map(
      ({ description, quantity, unitPrice, amount }) =>
        `${description} & ${quantity} & \\$${unitPrice.toFixed(2)} & \\$${amount.toFixed(2)} \\\\`
    )
    .join('\n');
}

// Synchronous — returns PDF directly (suitable for on-demand single invoices)
router.post('/generate', async (req, res) => {
  const { invoice } = req.body;

  const subtotal = invoice.items.reduce((sum, i) => sum + i.amount, 0);
  const taxAmount = subtotal * (invoice.taxRate / 100);
  const total = subtotal + taxAmount;

  const variables = {
    brand_color: invoice.brandColor || '2563EB',
    logo_asset_id: process.env.FORMATEX_LOGO_ASSET_ID,
    company_name: invoice.company.name,
    company_address: invoice.company.address,
    company_email: invoice.company.email,
    invoice_number: invoice.number,
    invoice_date: invoice.date,
    due_date: invoice.dueDate,
    client_name: invoice.client.name,
    client_address: invoice.client.address,
    client_email: invoice.client.email,
    line_items: buildLineItemsLatex(invoice.items),
    subtotal: subtotal.toFixed(2),
    tax_rate: invoice.taxRate.toString(),
    tax_amount: taxAmount.toFixed(2),
    total: total.toFixed(2),
    payment_terms: invoice.paymentTerms || 'Payment due within 30 days.',
  };

  try {
    const response = await axios.post(
      `${FORMATEX_API}/templates/${TEMPLATE_ID}/compile`,
      { variables },
      {
        headers: {
          Authorization: `Bearer ${FORMATEX_KEY}`,
          'Content-Type': 'application/json',
        },
        responseType: 'arraybuffer',
      }
    );

    res.set({
      'Content-Type': 'application/pdf',
      'Content-Disposition': `attachment; filename="invoice-${invoice.number}.pdf"`,
    });
    res.send(Buffer.from(response.data));
  } catch (err) {
    console.error('FormaTeX compilation failed:', err.response?.data || err.message);
    res.status(500).json({ error: 'PDF generation failed' });
  }
});

module.exports = router;

Register the route in your Express app:

const invoiceRouter = require('./routes/invoices');
app.use('/api/invoices', invoiceRouter);

A POST /api/invoices/generate with a well-formed invoice payload now returns a PDF binary stream.


Bulk Generation with Async Compilation and Webhooks

When a user triggers end-of-month invoice runs for hundreds of clients, synchronous compilation will time out. Use FormaTeX's async mode instead.

// Submit async job — returns immediately with a job_id
router.post('/generate-async', async (req, res) => {
  const { invoice } = req.body;
  const variables = buildVariables(invoice); // same logic as above

  const { data } = await axios.post(
    `${FORMATEX_API}/templates/${TEMPLATE_ID}/compile/async`,
    {
      variables,
      webhook_url: `${process.env.APP_BASE_URL}/api/invoices/webhook`,
      metadata: { invoice_id: invoice.id, user_id: invoice.userId },
    },
    { headers: { Authorization: `Bearer ${FORMATEX_KEY}` } }
  );

  // Store job_id against invoice record in your database
  await db.invoices.update(invoice.id, { formatex_job_id: data.job_id, status: 'compiling' });

  res.json({ job_id: data.job_id, status: 'compiling' });
});

// Webhook handler — FormaTeX calls this when compilation finishes
router.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  const payload = JSON.parse(req.body);

  if (payload.status === 'success') {
    const { invoice_id, user_id } = payload.metadata;
    await db.invoices.update(invoice_id, {
      pdf_url: payload.pdf_url,
      status: 'ready',
    });
    // Optionally trigger a notification to the user
    await notifyUser(user_id, invoice_id, payload.pdf_url);
  } else {
    console.error('Async compilation error:', payload.error);
    await db.invoices.update(payload.metadata.invoice_id, { status: 'failed' });
  }

  res.sendStatus(200);
});

The async pattern decouples invoice generation from the HTTP request lifecycle. Your API responds instantly, FormaTeX does the work, and your webhook handler updates the record when the PDF is ready.

For large batch runs, loop over your invoice records and fire generate-async for each one. FormaTeX queues them and processes them in parallel, calling your webhook for each completion.


Scaling with Caching on Max+

On the FormaTeX Max+ plan, compilation results are cached by document content hash. When you submit the same variables for the same template, FormaTeX returns the cached PDF without recompiling — and without consuming your quota.

This has a direct impact on SaaS economics:

  • Re-downloads: If a customer downloads their invoice three times, only the first compilation costs quota.
  • Duplicate detection: If a bug in your system submits the same invoice twice, the second call is instant and free.
  • Preview mode: Render a draft invoice with placeholder amounts while the user edits — repeated preview renders of identical content are cached.

No code changes are required to benefit from caching. FormaTeX handles it transparently on the server side.


Putting It All Together

You now have a complete PDF invoice generator with the following properties:

  • Professional output — LaTeX typesetting, embedded fonts, brand colours, pixel-perfect layout.
  • No server-side TeX — FormaTeX handles compilation; your Node.js server stays lean.
  • Dynamic data — Variable substitution maps any invoice data model to your template.
  • Scale-ready — Async jobs and webhooks decouple generation from request handling for bulk runs.
  • Efficient — Max+ caching eliminates redundant compilation for identical documents.

The pattern generalises beyond invoices. The same FormaTeX + Node.js architecture works for contracts, payslips, statements of work, and any other document type where output quality and reliability matter.

The full source for the Express routes above is self-contained and production-ready. Add input validation (Zod or Joi), store job IDs in your database, and wire up the webhook to your notification layer — and you have a complete automated invoice PDF pipeline.


Keywords: SaaS invoice generator, LaTeX invoice API, PDF invoice generator Node.js, automated invoice PDF

Need a robust API or back-end?

Scalable back-end systems with Laravel, Node.js or Spring Boot.

REST APIs, authentication, role management, and clean architecture — built to last.

HassanOSSYS-00
SECURE CHANNEL · ACTIVE
INIT://