---
title: Templates
description: Save an HTML document once, then render it per customer by sending an ID and a few variables.
sidebar:
  label: Templates
  order: 4
---

A template is an HTML document stored on your account with `{{ placeholders }}` where the values change. Your app then sends an ID and a small JSON object instead of a full document on every request — which keeps the markup out of your codebase and the payload small.

Reach for a template when the same document is produced over and over — invoices, receipts, tickets, certificates, order confirmations. Send `html` directly when each document is genuinely one-off.

## Create one

Templates are managed in the [dashboard](https://snhtml.com/dashboard) under **Templates**. Write or paste the HTML, give it a name (up to 120 characters), and save.

```html
<!doctype html>
<html>
  <head>
    <style>
      body { font-family: system-ui, sans-serif; padding: 48px; }
      .total { font-size: 32px; font-weight: 600; }
    </style>
  </head>
  <body>
    <h1>Invoice {{ invoiceNumber }}</h1>
    <p>Billed to {{ customerName }}</p>
    <p class="total">{{ total }}</p>
  </body>
</html>
```

Saving parses the HTML and records the placeholder names it finds, so the dashboard can list a template's variables for you. Editing the HTML re-parses them.

Creating, editing, and deleting templates are free — they don't count as renders. **Preview** in the dashboard draws the HTML in your browser and is free too; only **Render** calls the API and counts.

## Placeholder syntax

A placeholder is a name in double braces: `{{ customerName }}`. Surrounding whitespace is ignored, so `{{customerName}}` and `{{ customerName }}` are the same placeholder.

Names follow JavaScript identifier rules — letters, digits, `_`, and `$`, not starting with a digit. `{{ line_total }}` and `{{ $ref }}` work; `{{ line-total }}`, `{{ items[0] }}`, and `{{ user.name }}` do not, and are left in the output verbatim.

There are no loops, conditionals, or expressions. If a document needs a variable-length table, build that HTML in your app and send it as `html`.

## Render one

`POST /v1/render/template` with the template's ID and the values to substitute:

```bash cURL
curl -X POST https://api.snhtml.com/v1/render/template \
  -H "authorization: Bearer snhtml_..." \
  -H "content-type: application/json" \
  -d '{
    "templateId": "k57d2...",
    "variables": {
      "invoiceNumber": 1842,
      "customerName": "Ada Lovelace",
      "total": "$249.00"
    },
    "format": "pdf",
    "pdfFormat": "A4"
  }' \
  --output invoice-1842.pdf
```

Values may be strings, numbers, booleans, or `null`. Each is converted to text: `1842` becomes `1842`, `true` becomes `true`, and `null` — like a placeholder you didn't pass at all — becomes an empty string. A missing variable is never an error, so a typo shows up as a gap in the document rather than a failed request.

The ID is a per-account secret in practice: a template only renders for the account that owns it, and a key from another account gets `404 Template not found`.

Every [render option](/render-options) works here too — the template supplies the markup, the request supplies the format, viewport, and PDF settings. The output filename defaults to the template's name when you don't send `fileName`.

## Values are inserted as raw HTML

Substitution is plain text replacement, so a value containing `<` or `&` becomes markup in the rendered document. Escape anything that comes from your users before you send it:

```ts TypeScript
const escapeHtml = (value: string) =>
  value.replace(
    /[&<>"']/g,
    (character) =>
      ({
        "&": "&amp;",
        "<": "&lt;",
        ">": "&gt;",
        '"': "&quot;",
        "'": "&#39;",
      })[character] as string,
  );

await fetch("https://api.snhtml.com/v1/render/template", {
  method: "POST",
  headers: {
    authorization: `Bearer ${process.env.SNAPHTML_API_KEY}`,
    "content-type": "application/json",
  },
  body: JSON.stringify({
    templateId,
    variables: { customerName: escapeHtml(customer.name) },
    format: "pdf",
  }),
});
```

That behavior is occasionally what you want — a `{{ rowsHtml }}` placeholder filled with markup you generated. Just make sure the string was built by your code, not typed by a customer.
