PDF mail merge from a CSV, one PDF or image per row

A PDF mail merge takes one design and a spreadsheet, and gives you one finished file for each row. In thirds.ai, you save a design once, such as a certificate, an invoice, or a social post, with your logo, colours, and fonts. Then you paste your rows into Batches and click one button. You get a ZIP with one PDF or image for each row, and each file that works costs one credit. This page covers making new files from your own design. It doesn't fill in a PDF form that someone else made.

Use with your agent

Give this prompt to your AI agent. The agent does the task for you.

Read https://thirds.ai/docs/batch-renders and help me turn a spreadsheet of rows into many PDFs or images from one thirds.ai template, with a clear credit cost.

You stop copying an old file and retyping each name by hand, and every file keeps the same look. A verified free account gets 50 free credits each month, as the credits and billing page says, so you can try a 10-row batch before you pay for anything.

Make it without code

You can run a PDF mail merge in the thirds.ai editor and the Batches page without writing any code. It takes three steps, and you need a free thirds.ai account and a spreadsheet with one row for each file. This example uses the Portrait course certificate, so each learner on your list gets their own certificate.

The thirds.ai editor with boxes for the learner, the course, and the issuer beside the finished Alexandra Morgan certificate

  1. Open the Portrait course certificate and click Use this template. Sign in first if you need to. If you want to change the words, colours, or layout, do it on the canvas and click Save.
  2. Click Batch at the top of the editor. The Batches page opens with a table that has one column for each detail on the certificate. You can also open Batches from the studio or from a template.
  3. Paste your rows, check the cost, and click the button that shows your file count, such as Make 2 files. When the files are ready, click Download ZIP.

How do I set up the spreadsheet for a PDF mail merge?

To set up a spreadsheet for a thirds.ai PDF mail merge, give it one column for each detail on your design and one row for each file you want. The table on the Batches page already has those columns, so you only add the rows. You can type in the cells, paste cells from a spreadsheet, or load a CSV file.

When the first row you paste or load holds the column names, each column goes to the detail it names. Any other paste fills the cells in order. For a detail that holds a list or an object, put JSON in that cell. The product spec sheet recipe shows a CSV with list cells that makes one sheet for each SKU. Download a starter CSV gives you a file with the right first row.

This sample fills the four details of the Portrait course certificate, the same ones the certificate recipe walks through for a single learner:

csv
issuer,learner,course,date
Northgate Design School,Alex Rivera,Workshop,2026-09-04
Northgate Design School,Theo Lee,Workshop,2026-09-04

Paste or load the CSV. Its first row names issuer, learner, course, and date, so each column fills the right spot on the certificate. Check the rows, then click Make 2 files. To check one certificate before you make the batch, use the free certificate generator.

What does a CSV batch cost, and how many rows can I send?

A CSV batch in thirds.ai costs one credit for each file that works. Failed and cancelled rows cost nothing, and there's no bulk discount. The Batches page shows the cost before you click, and a refresh never spends a credit. 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.

For a billing example, see how to make an invoice PDF for each client. The credits and billing page lists what each plan includes.

Download, cancel, or retry a batch

On the Batches page, you download a thirds.ai batch with Download ZIP, stop it with Cancel pending, and try failed rows again with Retry failed rows. The ZIP is ready as soon as any file in the batch works. Retry doesn't charge you again for files that already worked.

The files in the ZIP follow row order, such as 001.pdf. The ZIP also holds a manifest.json file with each row's state, but not the details you typed. Cancel pending stops only the rows that haven't started.

API and MCP

You can also run a CSV batch from your own code with the thirds.ai API or an MCP agent. The API takes each row as a JSON object instead of CSV text, so turn each spreadsheet row into one object. Pin the template version, and reuse an idempotency key only for the same body. To prepare a design to reuse across rows, follow how to make a template you can reuse.

To have an AI agent send a new list each morning, connect Hermes Agent to thirds.ai and try the Hermes Agent use cases. To make a poster for each new event every week, connect OpenClaw to thirds.ai. To make one report a week from a Power Automate flow or a Make scenario, follow create weekly report PDFs in Power Automate or Make. To ask Kimi Code for a certificate for each name, connect Kimi Code to thirds.ai and start from the prompts on the Kimi Code page.

POST /v1/batches
curl --fail-with-body --silent --show-error https://thirds.ai/v1/batches \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: YOUR_UNIQUE_BATCH_REQUEST' \
  --data '{"template_id":"tpl_00000000000000000000000000000000","version":1,"format":"pdf","rows":[{"issuer":"Northgate Design School","learner":"Alex Rivera","course":"Workshop","date":"2026-09-04"},{"issuer":"Northgate Design School","learner":"Theo Lee","course":"Workshop","date":"2026-09-04"}]}'

Poll GET /v1/batches/{id} until state is complete, partial, failed, or cancelled. Then download archive_url. POST /v1/batches/{id}/cancel stops pending rows. POST /v1/batches/{id}/retry admits failed, cancelled, and pending rows.

Node 24:

JavaScript
const response = await fetch("https://thirds.ai/v1/batches", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.THIRDS_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "YOUR_UNIQUE_BATCH_REQUEST",
  },
  body: JSON.stringify({
    template_id: "tpl_00000000000000000000000000000000",
    version: 1,
    format: "pdf",
    rows: [
      {
        issuer: "Northgate Design School",
        learner: "Alex Rivera",
        course: "Workshop",
        date: "2026-09-04",
      },
      {
        issuer: "Northgate Design School",
        learner: "Theo Lee",
        course: "Workshop",
        date: "2026-09-04",
      },
    ],
  }),
});
const batch = await response.json();

Python 3:

Python
import os
import urllib.request

request = urllib.request.Request(
    "https://thirds.ai/v1/batches",
    data=b'{"template_id":"tpl_00000000000000000000000000000000","version":1,"format":"pdf","rows":[{"issuer":"Northgate Design School","learner":"Alex Rivera","course":"Workshop","date":"2026-09-04"}]}',
    headers={
        "Authorization": f"Bearer {os.environ['THIRDS_API_KEY']}",
        "Content-Type": "application/json",
        "Idempotency-Key": "YOUR_UNIQUE_BATCH_REQUEST",
    },
    method="POST",
)
with urllib.request.urlopen(request) as response:
    print(response.read().decode())

An MCP agent, such as OpenCode with the thirds.ai server or Claude Code with the thirds.ai MCP server, can call create_batch with the same JSON rows, then get_status with the batch_ id:

MCP request
{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "tools/call",
  "params": {
    "name": "create_batch",
    "arguments": {
      "idempotency_key": "YOUR_UNIQUE_BATCH_REQUEST",
      "template_id": "tpl_00000000000000000000000000000000",
      "format": "pdf",
      "rows": [
        {
          "issuer": "Northgate Design School",
          "learner": "Alex Rivera",
          "course": "Workshop",
          "date": "2026-09-04"
        }
      ]
    }
  }
}

See the batches reference and credits and billing.

More batch recipes

Ready to make your first file?

Start in the playground with a gallery template, or get an API key and send your first request from your own terminal.