MainSDK
Search⌘K
Interfaces

Files, sharing and media

PDF documents

pdf

Render PDFs from layouts and templates in the reader's language with the built-in renderer, Gotenberg or DocRaptor; the rendering half of documents.

Previewships as a beta APIPhase 1 · Core, public betaSpec documentsFree in your app, Apache-2.0

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
Updated 2026-10-28

pdf renders invoices, receipts, certificates and contracts from templates, with text, money and dates in the reader's language through i18n.

The real call is documents.render; pdf.render(template, data) is an alias of it, kept for the short name.

At a glance

pdf:
  driver: builtin

Calls

  • pdf.render(template, data)

Guarantees

Whatever the driver.

  • Localised through i18n
  • Rendered without a browser by the built-in renderer
  • Output stored through files in the tenant's region

Drivers

4 drivers. Select one to see its line in main.yaml.

main.yamlBuilt in (default)
pdf:
  driver: builtin

Change this one line to swap providers; your calls stay the same.Built-in and local drivers integration

Codes

CodeWhere it appearsMeaning
MAIN-DOC-0011Checkmainsdk check, before deployA locale with a complex script renders with the builtin renderer, which doesn't shape it
MAIN-DOC-0007RuntimeError responses and logsA render layout or template is invalid (400)

Features

Nothing here has shipped yet. Each feature lands in its phase; the spec ticket builds it.

FeaturePhaseMilestoneSpec ticket
Render from a templatePhase 10.4documents#render
HTML to PDF through Gotenberg or DocRaptorPhase 10.4documents#render-drivers-checks
Sign a documentPhase 31.2signed-approvals

Quick start

main.yaml
documents:
  render: { driver: builtin }
permissions:
  disk:
    read: [templates]
lib/invoice.ts
import { documents } from "mainsdk";

export async function invoiceFor(order: { number: string }, userId: string, locale: string) {
  return documents.render("invoice", order, { owner: userId, locale });
}

templates/invoice.pdf.yaml holds the layout. The call returns a FileRef: the PDF is already stored through files, owned by owner, in the tenant's region.

Templates and layouts

documents.render takes either a template name or a layout object.

templates/invoice.pdf.yaml
page: A4                       # A4 | Letter
margin: 20mm
body:
  - text: "Invoice {{number}}"
    style: h1                  # h1 | h2 | body | small
  - table:
      head: ["Item", "Qty", "Price"]
      rows:
        each: items            # repeats the row for every item; the header repeats on each page
        cells: ["{{name}}", "{{qty}}", "{{price | money:currency}}"]
  - text: "Total {{total}}"
    style: h2
  • Elements are text, table, image (a FileRef), spacer and pageBreak.
  • {{path}} reads from your data. A missing value fails with MAIN-DOC-0007; nothing is evaluated.
  • A value written as { money: 125000, currency: "INR" }, { number: 4 } or { date: "2026-10-27" } is formatted for the locale. The filter {{price | money:currency}} does the same for a plain number.
  • The template is found along the locale chain: templates/<locale>/<name>.pdf.yaml, then the fallbacks, then templates/<name>.pdf.yaml. Declare the folder under permissions.disk.read.
  • Page numbers ("Page 1 of 2") are added for you.

Configuration

main.yaml
documents:
  render:
    driver: builtin                       # builtin | gotenberg | docraptor
    fonts: [assets/fonts/Inter.ttf]       # embedded and subset per document
    url: https://gotenberg.example.com    # gotenberg only
    key: secret:DOCRAPTOR_API_KEY         # docraptor only
permissions:
  disk:
    read: [templates, assets/fonts]
KeyDefaultWhat it does
documents.render.driverbuiltinThe renderer
documents.render.fontsnoneTrueType fonts to embed. The first is the default; set font: in a layout to pick another
documents.render.urlnoneWhere your Gotenberg runs
documents.render.keynoneThe DocRaptor key, as a secret: reference

Without a font the built-in renderer uses a standard PDF font, which has no rupee sign, for example. Embed a TrueType font that has the characters you print.

What you get

  • PDFs from a layout or a template, rendered by the built-in renderer, Gotenberg or DocRaptor.
  • Text, money and dates in the reader's language, with a missing translation caught by the i18n check.
  • Output stored through files with an owner, ready to share with an expiring link.
  • Template values sanitized before an HTML driver sees them, and no fetches outside network.allow.

Examples

Render an invoice for a signed-in customer

lib/customer-invoice.ts
import { auth, documents, models, s } from "mainsdk";

const { orders } = models({
  orders: s.object({ item: s.string(), total: s.int(), locale: s.string() }),
}); // declared once in lib/orders.ts, shown inline so the example stands alone

export async function customerInvoice(req: Request, orderId: string) {
  const session = await auth.require(req);
  const order = await orders.for(session.user).get(orderId);
  return documents.render("invoice", { ...order }, { owner: session.user, locale: order.locale });
}

orders.for(session.user) means a customer can only render invoices for their own orders.

Render from a layout in code

lib/receipt-pdf.ts
import { documents } from "mainsdk";

export function receipt(userId: string, lines: string[][]) {
  return documents.render(
    { page: "A4", body: [{ text: "Receipt", style: "h1" }, { table: { head: ["Item", "Qty"], rows: lines } }] },
    {},
    { owner: userId, title: "Receipt" },
  );
}

Store and share a certificate

lib/certificates.ts
import { documents, share } from "mainsdk";

export async function issueCertificate(user: { id: string; locale: string }, course: { name: string }) {
  const ref = await documents.render("certificate", { course }, { owner: user.id, locale: user.locale });
  return share(ref, { expiresIn: "30d" });
}

Files and links go through getqr.id; see File uploads and Share links and QR codes.

Sign a contract (planned)

Signing a rendered PDF is planned for phase 3 and has no code. A signing driver (Documenso) is named in the catalog.

Errors and checks

CodeWhenFix
MAIN-DOC-0007The layout, template, font or a value is missing or invalidAdd templates/<name>.pdf.yaml, declare the folder under permissions.disk.read, or give the value the template names
MAIN-FILES-0002No owner was passed and no person is signed inPass { owner: user } to documents.render
MAIN-PERM-0002The templates or fonts folder is not under permissions.disk.readAdd it in main.yaml
MAIN-DOC-0011In check: a complex-script locale (Devanagari, Tamil, Arabic) on the built-in rendererUse gotenberg or docraptor

A missing translation in a template fails the i18n check, not the reader.

Swapping drivers

main.yaml
documents:
  render: { driver: docraptor, key: secret:DOCRAPTOR_API_KEY }   # was builtin

Your documents.render calls stay the same. The HTML drivers render an HTML template (templates/<name>.pdf.html) after sanitize, and fetch nothing outside network.allow. See Swap a driver.

Testing

The built-in renderer runs offline. Render, then read the result back with documents.parse:

lib/invoice.test.ts
import { it } from "node:test";
import assert from "node:assert/strict";
import { createTestApp } from "@mainsdk/testing";
import { documents } from "mainsdk";

it("documents AC11 renders an invoice and reads it back", async () => {
  createTestApp({ main: "./main.yaml" });
  const ref = await documents.render(
    { body: [{ text: "Invoice INV-7", style: "h1" }] },
    {},
    { owner: "u1" },
  );
  const doc = await documents.parse(ref, { as: "u1" });
  assert.match(doc.pages[0]?.text ?? "", /Invoice INV-7/);
});

Security and privacy

  • Generated documents often hold personal data. They are stored through files with an owner, and you share them with expiring links.
  • The HTML drivers receive the document data. Check their regions before you choose one; see Data and regions.
  • Template values are sanitized before an HTML driver sees them.

FAQ

Do I need a hosted service? No. The built-in renderer is the default.

Which languages render? The built-in renderer handles scripts that need no shaping, such as Latin, Greek, Cyrillic and CJK, with an embedded font that has the glyphs. For Devanagari, Tamil, Arabic and other scripts that need shaping, use gotenberg or docraptor.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet