Batches and CSV
POST /v1/batch makes one render per row of data, up to 10,000 rows. Each row becomes its own render, and one bad row never stops the rest. You can get one ZIP of all files at the end.
Three ways to send rows
Rows in the request
Each row is a data object, as in POST /v1/render.
curl
curl -X POST "https://mintrender.com/v1/batch" \ -H "Authorization: Bearer $MINTRENDER_KEY" \ -H "Content-Type: application/json" \ -d '{"template":"tpl_3k2jQ8xLm0P4nB7vR2sT","format":"pdf","rows":[{"name":"Ada Lovelace","course":"Algebra"},{"name":"Grace Hopper","course":"Compilers"}],"zip":true,"webhook_url":"https://example.com/hooks/mintrender"}'JavaScript
const res = await fetch("https://mintrender.com/v1/batch", { method: "POST", headers: { Authorization: `Bearer ${process.env.MINTRENDER_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ template: "tpl_3k2jQ8xLm0P4nB7vR2sT", format: "pdf", rows: [ { name: "Ada Lovelace", course: "Algebra", }, { name: "Grace Hopper", course: "Compilers", }, ], zip: true, webhook_url: "https://example.com/hooks/mintrender", }),});const body = await res.json();if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message} (${body.error.request_id})`);console.log(body);Python
import os import requests res = requests.post( "https://mintrender.com/v1/batch", headers={"Authorization": f"Bearer {os.environ['MINTRENDER_KEY']}"}, json={ "template": "tpl_3k2jQ8xLm0P4nB7vR2sT", "format": "pdf", "rows": [ { "name": "Ada Lovelace", "course": "Algebra", }, { "name": "Grace Hopper", "course": "Compilers", }, ], "zip": True, "webhook_url": "https://example.com/hooks/mintrender", }, timeout=120,)body = res.json()if not res.ok: raise RuntimeError(f"{body['error']['code']}: {body['error']['message']} ({body['error']['request_id']})")print(body)A CSV link
A public link to a CSV file, like a Google Sheet's export link. The first row names the params.
curl
curl -X POST "https://mintrender.com/v1/batch" \ -H "Authorization: Bearer $MINTRENDER_KEY" \ -H "Content-Type: application/json" \ -d '{"template":"tpl_3k2jQ8xLm0P4nB7vR2sT","format":"png","sizes":["main","square"],"csv_url":"https://docs.google.com/spreadsheets/d/YOUR_SHEET/export?format=csv","ignore_unknown":true,"zip":true}'JavaScript
const res = await fetch("https://mintrender.com/v1/batch", { method: "POST", headers: { Authorization: `Bearer ${process.env.MINTRENDER_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ template: "tpl_3k2jQ8xLm0P4nB7vR2sT", format: "png", sizes: ["main", "square"], csv_url: "https://docs.google.com/spreadsheets/d/YOUR_SHEET/export?format=csv", ignore_unknown: true, zip: true, }),});const body = await res.json();if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message} (${body.error.request_id})`);console.log(body);Python
import os import requests res = requests.post( "https://mintrender.com/v1/batch", headers={"Authorization": f"Bearer {os.environ['MINTRENDER_KEY']}"}, json={ "template": "tpl_3k2jQ8xLm0P4nB7vR2sT", "format": "png", "sizes": ["main", "square"], "csv_url": "https://docs.google.com/spreadsheets/d/YOUR_SHEET/export?format=csv", "ignore_unknown": True, "zip": True, }, timeout=120,)body = res.json()if not res.ok: raise RuntimeError(f"{body['error']['code']}: {body['error']['message']} ({body['error']['request_id']})")print(body)An uploaded CSV
Upload the file to POST /v1/assets first. The answer's id starts with csv_.
curl -X POST "https://mintrender.com/v1/assets" \ -H "Authorization: Bearer $MINTRENDER_KEY" \ -F "file=@people.csv" \ -F "name=People for the March course"curl
curl -X POST "https://mintrender.com/v1/batch" \ -H "Authorization: Bearer $MINTRENDER_KEY" \ -H "Content-Type: application/json" \ -d '{"template":"tpl_3k2jQ8xLm0P4nB7vR2sT","format":"pdf","csv_asset":"csv_8Fk2LmQ9xP0aB4nC7vR2","zip":true}'JavaScript
const res = await fetch("https://mintrender.com/v1/batch", { method: "POST", headers: { Authorization: `Bearer ${process.env.MINTRENDER_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ template: "tpl_3k2jQ8xLm0P4nB7vR2sT", format: "pdf", csv_asset: "csv_8Fk2LmQ9xP0aB4nC7vR2", zip: true, }),});const body = await res.json();if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message} (${body.error.request_id})`);console.log(body);Python
import os import requests res = requests.post( "https://mintrender.com/v1/batch", headers={"Authorization": f"Bearer {os.environ['MINTRENDER_KEY']}"}, json={ "template": "tpl_3k2jQ8xLm0P4nB7vR2sT", "format": "pdf", "csv_asset": "csv_8Fk2LmQ9xP0aB4nC7vR2", "zip": True, }, timeout=120,)body = res.json()if not res.ok: raise RuntimeError(f"{body['error']['code']}: {body['error']['message']} ({body['error']['request_id']})")print(body)Every batch answers 202 at once:
{ "id": "bat_2Lk9mQ8xP0aB4nC7vR2s", "status": "running", "rows": 2, "poll": "/v1/batch/bat_2Lk9mQ8xP0aB4nC7vR2s"}Request fields
| Field | About |
|---|---|
template | Required. The template id. |
version | A version number. Default: the newest published version. |
format | png (default), jpg, webp, svg or pdf. |
sizes | Size names, default ["main"], 1 to 20. Each row makes one file per size. |
scale, quality | As in POST /v1/render. |
rows | A list of data objects, at most 10,000. Each row at most 32 KB of JSON. |
csv_asset | An uploaded CSV's asset id (csv_…). |
csv_url | A public CSV link, at most 5 MB. It must answer within 10 seconds, with at most 3 redirects. |
ignore_unknown | Skip columns or keys that match no param. Use it for sheets with extra columns. |
zip | true to build one ZIP of every file at the end. Default false. |
webhook_url | A URL that gets batch.completed when the batch ends. |
Send exactly one of rows, csv_asset and csv_url. A batch takes the Idempotency-Key header like a render. The body may be at most 12 MB.
Credits
The batch reserves the credits of every row at once: rows × sizes, times pages for a PDF. If they do not all fit, the answer is 402 and no row starts. Rows whose files already exist give their credits back. Rows that fail give their credits back. A test key costs nothing. One batch counts as one running job.
Batch status
curl
curl "https://mintrender.com/v1/batch/bat_2Lk9mQ8xP0aB4nC7vR2s?offset=0&limit=1000" \ -H "Authorization: Bearer $MINTRENDER_KEY"JavaScript
const res = await fetch("https://mintrender.com/v1/batch/bat_2Lk9mQ8xP0aB4nC7vR2s?offset=0&limit=1000", { method: "GET", headers: { Authorization: `Bearer ${process.env.MINTRENDER_KEY}`, },});const body = await res.json();if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message} (${body.error.request_id})`);console.log(body);Python
import os import requests res = requests.get( "https://mintrender.com/v1/batch/bat_2Lk9mQ8xP0aB4nC7vR2s?offset=0&limit=1000", headers={"Authorization": f"Bearer {os.environ['MINTRENDER_KEY']}"}, timeout=120,)body = res.json()if not res.ok: raise RuntimeError(f"{body['error']['code']}: {body['error']['message']} ({body['error']['request_id']})")print(body){ "id": "bat_2Lk9mQ8xP0aB4nC7vR2s", "status": "completed", "rows": 2, "done": 2, "failed": 0, "zip_url": "/v1/batch/bat_2Lk9mQ8xP0aB4nC7vR2s/zip", "renders": [ { "row": 1, "id": "rnd_…", "status": "succeeded" }, { "row": 2, "id": "rnd_…", "status": "succeeded" } ], "created": "2026-11-01T09:30:00.000Z"}| Status | Means |
|---|---|
running | Rows are being drawn. |
zipping | Every row has ended and the ZIP is being built. |
completed | Every row has ended and at least one succeeded. The ZIP, when asked for, is ready or failed. |
failed | Every row has ended and none succeeded. |
donecounts rows that succeeded,failedrows that failed or were canceled.renderslists each row's render in row order.rowstarts at 1, after the CSV header. Page through big batches withoffsetandlimit(1 to 1,000, default 1,000).- Each row's render has
"metadata": {"row": n}. Read its files and error withGET /v1/renders/{id}. - A row that has not started can be canceled with
DELETE /v1/renders/{id}. It counts as failed. - Each row's image URLs are fetched when that row runs. A URL that fails fails only that row.
- Rows send no render events. The batch sends one
batch.completedevent, even when every row failed.
The ZIP
- With
zip: true, the ZIP has every file of the rows that succeeded. Its files are named{row}-{size}.{format}with the row in five digits, like00001-main.pdfand00002-square.png. zip_urlis a path on the API. Get it with your key:GET https://mintrender.com/v1/batch/{id}/zip. The answer isapplication/zip.- A ZIP may hold at most 65,535 files (rows × sizes). A batch with
zip: trueand more files is refused with 400. The files must be under 4 GB together. Over that, the ZIP fails and the batch still completes. - Before the ZIP is ready, the route answers 404 with "the ZIP is not ready yet".
- The ZIP is kept as long as the batch's files: it ends with the last of them. Then
zip_urlisnullagain and the route answers 404.
curl "https://mintrender.com/v1/batch/bat_2Lk9mQ8xP0aB4nC7vR2s/zip" \ -H "Authorization: Bearer $MINTRENDER_KEY" \ -o batch.zipCSV rules
name,course,dateAda Lovelace,Algebra,12 March 2026"Hopper, Grace",Compilers,12 March 2026"Sam ""The Pen"" Lee",Writing,13 March 2026- UTF-8 text. A byte order mark at the start is allowed. An uploaded CSV may be at most 5 MB, a CSV link at most 5 MB.
- The first row names the params. Every column needs a name, and no name may appear twice. Spaces around a name are removed.
- Every row must have as many fields as the header. Empty lines are skipped.
- Fields are split by commas. A field with a comma, a quote or a line break goes in double quotes, and a quote inside it is written twice (RFC 4180). Lines end with LF or CRLF.
- Every CSV value is text. Text layers take it as typed, image layers take it as a URL or
asset:…, shape layers as a colour, QR and barcode layers as their value. A column can namepage2.titleto set one page. - At most 10,000 rows after the header.
Google Sheets
Put the param names in the first row of the sheet, one per column.
Share the sheet so anyone with the link can view it.
Use the export link as
csv_url:https://docs.google.com/spreadsheets/d/YOUR_SHEET/export?format=csv. Add&gid=…for a tab that is not the first.
We fetch the link once, when the batch starts. Changes to the sheet after that are not in the batch.
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | Not exactly one of rows, csv_asset and csv_url, or no rows to render. |
| 400 | invalid_param | A row over 32,768 characters of JSON, an unknown size, a ZIP of more than 65,535 files, or a webhook URL we do not call. |
| 400 | invalid_csv | The CSV cannot be read, or it has more than 10,000 rows. The message gives the line or the row. |
| 400 | url_refused, url_failed | The CSV link cannot be fetched. |
| 402 | no_credits | The credits for all rows do not fit. |
| 404 | not_found | No such template, version or CSV asset. |
| 409 | not_published | The template has no published version. |
| 409 | in_progress | A request with the same Idempotency-Key is still running. |
| 413 | too_large | The body is over 12 MB. |
| 422 | idempotency_mismatch | The Idempotency-Key was used with a different body. |
| 429 | rate_limited, busy | Too many calls, the plan's running jobs are all in use, or too many renders at the same moment. See Retry-After. |
| 451 | frozen, expired | The workspace is frozen, or a reviewer workspace has ended. |
Seen MintRender used for content that breaks our terms? Report it. Questions about the API: support@mintrender.com, with the request id.