MintRender
All docs pages

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

FieldAbout
templateRequired. The template id.
versionA version number. Default: the newest published version.
formatpng (default), jpg, webp, svg or pdf.
sizesSize names, default ["main"], 1 to 20. Each row makes one file per size.
scale, qualityAs in POST /v1/render.
rowsA list of data objects, at most 10,000. Each row at most 32 KB of JSON.
csv_assetAn uploaded CSV's asset id (csv_…).
csv_urlA public CSV link, at most 5 MB. It must answer within 10 seconds, with at most 3 redirects.
ignore_unknownSkip columns or keys that match no param. Use it for sheets with extra columns.
ziptrue to build one ZIP of every file at the end. Default false.
webhook_urlA 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"}
StatusMeans
runningRows are being drawn.
zippingEvery row has ended and the ZIP is being built.
completedEvery row has ended and at least one succeeded. The ZIP, when asked for, is ready or failed.
failedEvery row has ended and none succeeded.
  • done counts rows that succeeded, failed rows that failed or were canceled.
  • renders lists each row's render in row order. row starts at 1, after the CSV header. Page through big batches with offset and limit (1 to 1,000, default 1,000).
  • Each row's render has "metadata": {"row": n}. Read its files and error with GET /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.completed event, 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, like 00001-main.pdf and 00002-square.png.
  • zip_url is a path on the API. Get it with your key: GET https://mintrender.com/v1/batch/{id}/zip. The answer is application/zip.
  • A ZIP may hold at most 65,535 files (rows × sizes). A batch with zip: true and 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_url is null again and the route answers 404.
curl "https://mintrender.com/v1/batch/bat_2Lk9mQ8xP0aB4nC7vR2s/zip" \  -H "Authorization: Bearer $MINTRENDER_KEY" \  -o batch.zip

CSV 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 name page2.title to set one page.
  • At most 10,000 rows after the header.

Google Sheets

  1. Put the param names in the first row of the sheet, one per column.

  2. Share the sheet so anyone with the link can view it.

  3. 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

StatusCodeWhen
400invalid_requestNot exactly one of rows, csv_asset and csv_url, or no rows to render.
400invalid_paramA 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.
400invalid_csvThe CSV cannot be read, or it has more than 10,000 rows. The message gives the line or the row.
400url_refused, url_failedThe CSV link cannot be fetched.
402no_creditsThe credits for all rows do not fit.
404not_foundNo such template, version or CSV asset.
409not_publishedThe template has no published version.
409in_progressA request with the same Idempotency-Key is still running.
413too_largeThe body is over 12 MB.
422idempotency_mismatchThe Idempotency-Key was used with a different body.
429rate_limited, busyToo many calls, the plan's running jobs are all in use, or too many renders at the same moment. See Retry-After.
451frozen, expiredThe 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.