Make a CSV batch of PDFs or images
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.
Keep one saved template and make one PDF or image for each data row. Open Batches, choose the template, upload or paste a CSV, map the columns, and click the button that shows the credit cost. A refresh never spends a credit.
Each successful file costs one credit. Failed and cancelled rows cost nothing. There is no bulk discount. The page shows the quoted cost before you start.
Prepare the CSV
The first row is the column names. Your plan sets the most data rows in one batch: 10 on Free and on credit packs with no plan, 50 on Starter, 100 on Growth, and 200 on Scale. Map each required template field to a column. For a list or object field, put JSON in that cell.
This sample fills the attendance certificate fields, the same three fields the certificate recipe walks through for a single learner:
learner,course,date
Alex Rivera,Workshop,2026-09-04
Theo Lee,Workshop,2026-09-04
Open Batches from the studio, from a template, or from the editor. Map learner, course, and date, check the sample rows, then click Make 2 files for 2 credits.
Cancel pending rows if you need to stop. Retry failed rows does not charge files that already succeeded. When any file succeeds, download the ZIP. Names follow row order, such as 001.pdf. The ZIP also holds a manifest.json with row states, not the values you sent.
API and MCP
The API accepts JSON objects, not CSV text. Pin the template version. Reuse an idempotency key only for the same body.
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":[{"learner":"Alex Rivera","course":"Workshop","date":"2026-09-04"},{"learner":"Theo Lee","course":"Workshop","date":"2026-09-04"}]}'const response = await fetch("https://thirds.ai/v1/batches", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
"Idempotency-Key": "YOUR_UNIQUE_BATCH_REQUEST"
},
body: JSON.stringify({
"template_id": "tpl_00000000000000000000000000000000",
"version": 1,
"format": "pdf",
"rows": [
{
"learner": "Alex Rivera",
"course": "Workshop",
"date": "2026-09-04"
},
{
"learner": "Theo Lee",
"course": "Workshop",
"date": "2026-09-04"
}
]
}),
redirect: "error",
signal: AbortSignal.timeout(30000),
});
if (!response.ok) throw new Error(`HTTP ${response.status}; Retry-After: ${response.headers.get("retry-after") ?? "none"}`);
console.log(await response.text());import json
from urllib.error import HTTPError
from urllib.request import HTTPRedirectHandler, Request, build_opener
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
request = Request("https://thirds.ai/v1/batches",
method="POST",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
"Idempotency-Key": "YOUR_UNIQUE_BATCH_REQUEST"
},
data=json.dumps({
"template_id": "tpl_00000000000000000000000000000000",
"version": 1,
"format": "pdf",
"rows": [
{
"learner": "Alex Rivera",
"course": "Workshop",
"date": "2026-09-04"
},
{
"learner": "Theo Lee",
"course": "Workshop",
"date": "2026-09-04"
}
]
}).encode("utf-8"),
)
try:
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(response.read().decode("utf-8"))
except HTTPError as error:
retry = error.headers.get("Retry-After", "none")
raise RuntimeError(f"HTTP {error.code}; Retry-After: {retry}") from NonePoll 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:
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: [
{ learner: "Alex Rivera", course: "Workshop", date: "2026-09-04" },
{ learner: "Theo Lee", course: "Workshop", date: "2026-09-04" },
],
}),
});
const batch = await response.json();
Python 3:
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":[{"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 can call create_batch with the same JSON rows, then get_status with the batch_ id:
{
"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": [
{
"learner": "Alex Rivera",
"course": "Workshop",
"date": "2026-09-04"
}
]
}
}
}
See the batches reference and credits and billing.