Mail merge to PDF from a spreadsheet or your code
A mail merge to PDF fills one design with a new name, amount, or list for each person and saves a separate PDF for each one. Keep one saved design in thirds.ai and send it new values each time you make a file. Upload a spreadsheet to get one PDF or image per row in a ZIP, or send each row from your own code. Each finished file costs one credit. Templates use HTML with Jinja fields, and the format is the same whether you start in the chat, editor, gallery, or API.
Give this prompt to your AI agent. The agent does the task for you.
Read https://thirds.ai/docs/templates-with-data and help me turn my HTML and data into a reusable template with supported fields, a data schema, and versioned renders, so I can make one PDF per row of data.
Follow How to make a template you can reuse to make one in the studio, or create a branded template for PDFs and images from the Client invoice design without code. For a working request, see Generate your first PDF with the API. For a billing example, see how to make an invoice PDF for each client. To see every detail the Client invoice template needs and the request that fills it, read Generate invoice PDFs from template data. To send the same invoice data from your own app, follow Generate invoice PDFs from JSON data in Node.js. To try an invoice before you save a template, use the PDF invoice generator. For a price quote, use the free quote generator. To pitch a client on one page, fill in the free proposal template. For a UK business card, use the free business card generator. To send a plan before a meeting, fill in the free meeting agenda template. To test sale ads, make static ad variations from one design with a new headline, offer, or product photo in each one. The free tools page lists every PDF maker and picture tool in one place.
Make one PDF for each row of a spreadsheet
You get a ZIP with one PDF or image for each row of your spreadsheet, all made from one saved design. It takes four steps. Open Batches, choose the design, upload or paste a CSV, and click the button that makes the files. You need a saved design first, and a CSV file where the first row names each value in the design, so each column fills the matching part.
The page shows the most the batch can cost before anything starts, and the button names the file count, such as Make 2 files. A row that fails costs nothing. Your plan sets the most rows in one batch: 10 on Free and on credit packs with no plan, 50 on Starter, 100 on Growth, and 200 on Scale. The files in the ZIP follow row order, so the first row becomes 001.pdf. Read PDF mail merge from a CSV for the full steps, the retry and cancel buttons, and the API request that sends the same rows as JSON.
Send each row from your own code
Send html to render the HTML in the request, html with data to fill fields without saving a template, or template_id with data to fill a saved version. PDF and image requests accept all three inputs. A saved version keeps the source and an optional schema. Send the data with every render. Sample values in the schema only fill the studio preview.
| Input | What it does |
|---|---|
html | Renders the HTML you send. |
html with data | Fills template fields in the HTML, then renders it. |
template_id with data | Loads a saved version, checks its schema if present, and fills its fields. |
See the API reference for request fields, saved templates for the endpoints that create and publish a version, and PDF and image options for layout settings. To send each row from a workflow instead, add the thirds.ai Zapier app, Make app, or n8n node, or build the request yourself with the HTTP request guide.
Write fields, conditions, and loops
Put a field name inside {{ }} to insert its value. Use a condition to show an optional section and a loop to repeat rows. Send every value the template reads, including optional flags and empty lists.
This source makes a short project brief:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Project brief</title>
</head>
<body>
<h1>{{ project }}</h1>
<p>Client: {{ client }}</p>
<p>Due: {{ due_date | date }}</p>
{% if show_tasks %}
<ul>
{% for task in tasks %}
<li>{{ task }}</li>
{% endfor %}
</ul>
{% endif %}
</body>
</html>
Use this data object:
{
"project": "Autumn catalogue",
"client": "Example Studio",
"due_date": "2026-10-01",
"show_tasks": true,
"tasks": ["Check product names", "Approve the cover"]
}
The result has the project heading, client, date, and two task rows. Set show_tasks to false to hide the list. Reading a missing value fails with template_missing_data. To have the AI chat build a brief like this with your brand kit, follow how to prompt the AI for a reusable project brief.
Use if, elif, else, and for with their closing blocks. You can also use Jinja comments and raw blocks. Calculate values in your app and pass the results as JSON data. The template does not support +, *, ~, in, or not in, except for in as the separator in a for loop.
Format values
Five filters format a field value: default, date, currency, number, and safe. Only default and currency take an extra value. Other filters are not supported.
| Filter | Input and result |
|---|---|
default | Use the supplied fallback only when the field is missing. {{ photo | default('image.png') }} keeps empty strings, zero, false, and null. |
date | A YYYY-MM-DD date, or an RFC 3339 timestamp converted to a UTC date. |
currency | A number with a currency sign, comma groups, and two decimal places. 1234.5 becomes $1,234.50, and currency("GBP") gives £1,234.50. |
number | A number with comma groups and up to three decimal places. 1234.5 becomes 1,234.5. |
safe | A string inserted as HTML without escaping. |
currency uses dollars unless you add "GBP" for pounds or "EUR" for euros. For another currency or locale, format the text in your app and send it as a normal string.
Normal fields display HTML tags as text. For example, <b>Example</b> keeps its tags visible in the output. Use safe only for HTML you have checked and want to insert as markup. Keep plain customer text in normal fields.
Partials and reusable sections
Keep the complete source in one template. Templates cannot load partial files or other templates. They do not support includes, imports, inheritance, macros, assignments, or function calls. If your app manages shared sections, combine them before you save or send the source.
Check data with a schema
Add a JSON Schema to check data before rendering a saved template. thirds.ai supports draft 2020-12 and references within the schema. It does not load remote or file references. It checks formats and rejects a schema that uses an unknown format.
For the example above, a schema can require each field and reject values of the wrong type:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["project", "client", "due_date", "show_tasks", "tasks"],
"properties": {
"project": { "type": "string" },
"client": { "type": "string" },
"due_date": { "type": "string", "format": "date" },
"show_tasks": { "type": "boolean" },
"tasks": { "type": "array", "items": { "type": "string" } }
},
"additionalProperties": false
}
In the studio, open Data, then Advanced, to edit the schema. With the API, send schema beside source when you create a template or version. A schema belongs to that exact version.
The studio preview uses the sample values in the schema's examples[0]. Add "x-thirds-kind": "image" to a picture field, or "x-thirds-kind": "colour" to a colour field, to get a picture or colour picker in the studio.
If the data fails the schema check, the API returns template_data_invalid with up to 16 field paths and fixed reasons. A nested field uses a JSON Pointer, such as data/items/0/count. Each pointer is cut at 256 UTF-8 bytes. The details never repeat the value you sent. If the template itself fails, look up its code in the template error codes to find the cause. See retries, downloads, and limits for common errors and safe retries.
An AI draft may have no schema. thirds.ai drops an AI schema if it is invalid or does not match the sample data. Add and test your own schema when you need to enforce which values a template accepts.
Save and use a version
In the editor, click Save to save the current source and schema as a new version. With the API, use POST /v1/templates to create version 1 or POST /v1/templates/{template_id}/versions to add the next version. You choose when to save a new version. A preview or a finished AI message doesn't save a new version.
Send the template ID, data, and an integer version to render a specific saved design. Copy the template ID from the Template menu in the editor, or from the card on your Templates page. If you omit version, the request selects the newest version when thirds.ai prepares it. Do not send the string "latest". Once thirds.ai accepts the job, that job keeps its selected version even if you save a new version.
Saved versions never change. In the editor, open the design's menu and choose Versions and settings to see every version. Click Open to edit from that version. Saving adds a version after the current latest version and keeps the old one intact.
To reuse an older design, render its version number. To make it the newest design again, click Restore on that version in Versions and settings.
You can still read an archived template's versions, but you cannot add versions or start new renders from it. Archiving does not cancel a render that thirds.ai already accepted. Your account can only read and render its own templates.
Keep renders consistent
Use a fixed template version and the same data when you need the same layout. Use stable asset URLs or include permitted assets in the source. A remote image or font can change even when your template version stays the same. Changes to assets or the renderer can produce files with different bytes.
Check the full PDF or image before you connect the template to a live workflow. To fill a saved template every week from a flow, follow create weekly report PDFs in Power Automate or Make. Read retries, downloads, and limits for job status, polling, and file retention.
Select a brand kit
Send brand_kit_id with a PDF, image, or template preview request. The editor also saves your default kit in the source. An API request can select another kit. Use brand.colours.primary, brand.colours.secondary, and brand.colours.accent for the first three palette colours, and brand.logo for the first logo. Use brand.fonts[0].family and brand.fonts[0].src in a font-face rule. The files stay private and each render captures their bytes.
Do not put brand in your data when you select a kit. The backend supplies it. Your schema describes your own fields. Add enough colours, a logo, or a font to the kit before you use that value. Missing values fail clearly. To set up the kit itself, follow how to create a brand kit. To find a kit's ID from your code, use the brand kits API.
A default applies only to an undefined field. For team.photo | default('image.png'), send team: {} if no photo is set. A missing parent object still fails the strict data check.
Apply the data to a business task
The CRM quote recipe maps approved quantities and prices to the Price quote template. The Stripe receipt recipe maps a successful payment and approved order to a receipt, and the free receipt generator makes one receipt PDF without code. The certificate recipe maps a learner's name, course, and date to a saved certificate design, and the free certificate generator makes one certificate PDF without code. The event ticket recipe maps a registration record to the Event ticket template and its QR code. The travel itinerary recipe maps a confirmed booking to the Travel itinerary template, one day per leg. The product spec sheet recipe maps one product record to the Product spec sheet template and checks its row limits. Each guide includes sample input, a Python mapper, and a checked download step.