Documentation

API reference

Render a single PDF from HTML and data, render a saved template, or run a batch: upload a spreadsheet or POST JSON rows as a dataset, queue a job, poll it, and download the PDFs. JSON in, PDF bytes out. Every endpoint lives under https://sheetrender.com/api/v1.

Authentication

Every request carries an API key as a bearer token. Create one in Settings → API keys. The key is shown once, when it is created; after that only its prefix is visible, so store it in your secret manager rather than in the page you copied it from. Revoking a key takes effect immediately.

Authorization: Bearer sr_live_...

Keys carry the permissions of the account that owns them and are not scoped per project. Treat one as a password: server-side only, never in browser JavaScript or a mobile app.

Endpoints

POST/api/v1/renders

Render one PDF from HTML and a row of data. Responds with the PDF bytes.

  • html is a template; data fills its placeholders. page_settings takes the app page-setup keys: page_size (a4, a5, a3, letter, legal, tabloid), orientation, margins (top/right/bottom/left in mm), page_numbers, scale, print_background. Unknown keys are rejected with 400.
  • Billed as one document against your monthly allowance. On the free plan the PDF carries the SheetRender watermark footer.
  • HTML over 2 MB is rejected with 413 — both the template you send and the document it renders to.

Request body

{
  "html": "<h1>Invoice {{ invoice_no }}</h1><p>{{ client }}</p>",
  "data": { "invoice_no": "1043", "client": "Acme Ltd" },
  "page_settings": { "page_size": "a4", "margins": { "top": 18, "right": 18, "bottom": 18, "left": 18 } }
}

Response

200 application/pdf
Content-Disposition: inline; filename="render.pdf"

<PDF bytes>

Example

curl -X POST https://sheetrender.com/api/v1/renders \
  -H "Authorization: Bearer $SHEETRENDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html":"<h1>Invoice {{ invoice_no }}</h1>","data":{"invoice_no":"1043"}}' \
  --output invoice.pdf

GET/api/v1/templates

List the templates in your account, newest first.

Response

200 application/json

[
  {
    "id": "tpl_8f2c…",
    "name": "Monthly invoice",
    "created_at": "2026-07-02T09:14:31Z",
    "updated_at": "2026-08-11T16:02:08Z"
  }
]

Example

curl https://sheetrender.com/api/v1/templates \
  -H "Authorization: Bearer $SHEETRENDER_API_KEY"

POST/api/v1/designs

Design a new template from your data plus an example document or a written brief.

  • This is the same design step the app runs: Claude reads your columns and the example, writes an HTML template, and maps each placeholder to a column. It takes 30 to 120 seconds, so the call returns 202 straight away and you poll GET /designs/{id}.
  • Give the data one way: dataset_id (an existing dataset), rows (a JSON array), or a multipart data file (.csv, .xlsx, .xlsm, or tab-separated .tsv or .txt). A dataset file or rows creates a new project.
  • Give the design source one way: an example file (.pdf, .png, .jpg, .webp, or .docx, multipart field example) on its own, or a brief (up to 2,000 characters) with an optional style_id from the style gallery.
  • Send JSON when there is no file. Send multipart/form-data when there is; rows then goes in as a JSON string.
  • One design at a time per project. Sending the same request again while it runs returns the same design_id without charging again; a different design for that project answers 409 until the first one finishes.
  • Counts against your plan's template and AI limits, the same as designing in the app.

Request body

{
  "dataset_id": "ds_1a90…",
  "brief": "One-page invoice. Company name top left, line items in a table, total bottom right.",
  "name": "Invoice"
}

Response

202 application/json

{ "design_id": "tpl_9d21…", "status": "running" }

Example

curl -X POST https://sheetrender.com/api/v1/designs \
  -H "Authorization: Bearer $SHEETRENDER_API_KEY" \
  -F "data=@clients.xlsx" \
  -F "example=@last-months-invoice.pdf" \
  -F "name=Invoice"

GET/api/v1/designs/{id}

Poll a design until it has a template.

  • status is running, succeeded, or failed. Poll every few seconds.
  • On succeeded the design_id is also the template_id: pass it to POST /templates/{id}/render or POST /jobs. mapping lists which column each placeholder reads.
  • On failed, error says why in one line. A design that was running when the server restarted reports failed; start a new one.

Response

200 application/json

{
  "design_id": "tpl_9d21…",
  "status": "succeeded",
  "template_id": "tpl_9d21…",
  "dataset_id": "ds_1a90…",
  "name": "Invoice",
  "mapping": [{ "variable": "client_name", "column": "Client" }],
  "preview_url": "/api/v1/templates/tpl_9d21…/preview.pdf"
}

Example

curl https://sheetrender.com/api/v1/designs/tpl_9d21 \
  -H "Authorization: Bearer $SHEETRENDER_API_KEY"

GET/api/v1/templates/{id}/preview.pdf

Render the first row of a template as a preview PDF.

  • Free and not counted as a document. Carries the preview watermark. Use it to check a design before queueing a job.
  • A template with no finished version yet answers 404.

Response

200 application/pdf

<PDF bytes>

Example

curl https://sheetrender.com/api/v1/templates/tpl_9d21/preview.pdf \
  -H "Authorization: Bearer $SHEETRENDER_API_KEY" \
  --output preview.pdf

POST/api/v1/templates/{id}/render

Render one PDF from a saved template and a row of data.

  • Uses the template’s current version and its saved page settings. Pass page_settings to override them for this call only.
  • Billed as one document, same as /renders. The filename comes from the template name.
  • A template with no current version answers 400.

Request body

{
  "data": { "invoice_no": "1043", "client": "Acme Ltd", "total": "1,280.00" },
  "page_settings": { "page_size": "letter" }
}

Response

200 application/pdf
Content-Disposition: inline; filename="Monthly invoice.pdf"

<PDF bytes>

Example

curl -X POST https://sheetrender.com/api/v1/templates/tpl_8f2c/render \
  -H "Authorization: Bearer $SHEETRENDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"data":{"invoice_no":"1043","client":"Acme Ltd"}}' \
  --output invoice.pdf

POST/api/v1/templates/{id}/datasets

Upload a spreadsheet (.csv, .xlsx, .xlsm, or tab-separated .tsv or .txt) as a dataset the given template can run a batch against.

  • The body is multipart/form-data with one part named file. The dataset is stored on the template’s project, so it is immediately usable with POST /jobs — and with any other template in that project.
  • columns[].key is the sanitized column name that POST /jobs group_by and template placeholders use. original is the spreadsheet header as uploaded; inferred_type is "string", "number", or "date". A date column is one whose values read as dates, including real date cells in .xlsx; the date filter formats them.
  • Creating a dataset is free. Only rendering against it counts toward your monthly document allowance. Files over 20 MB answer 413; more than 500,000 cells, or a file that is not a readable spreadsheet, answers 400. An unknown template answers 404.

Request body

multipart/form-data

file: customers.csv

Response

201 application/json

{
  "id": "ds_1a90…",
  "filename": "customers.csv",
  "sheet_name": "csv",
  "columns": [
    { "key": "client", "original": "Client", "inferred_type": "string" },
    { "key": "invoice_no", "original": "Invoice No", "inferred_type": "string" },
    { "key": "total", "original": "Total", "inferred_type": "number" }
  ],
  "row_count": 240,
  "created_at": "2026-08-19T11:18:02Z"
}

Example

curl -X POST https://sheetrender.com/api/v1/templates/tpl_8f2c/datasets \
  -H "Authorization: Bearer $SHEETRENDER_API_KEY" \
  -F "file=@customers.csv"

POST/api/v1/templates/{id}/datasets/rows

Create the same kind of dataset from JSON rows, when you have no file to post.

  • rows is a JSON array of objects. The spreadsheet header is the union of those objects’ keys, in first-seen order; a missing key on a later row becomes a blank cell. name is an optional label stored as the filename.
  • Creating a dataset is free. Only rendering against it counts toward your monthly document allowance.
  • At most 50,000 rows, and at most 500,000 cells including the header row. Over either limit, or an empty rows list, answers 400.

Request body

{
  "rows": [
    { "name": "Ada", "total": 42 },
    { "name": "Ben", "total": 17 }
  ],
  "name": "August customers"
}

Response

201 application/json

{
  "id": "ds_7e22…",
  "filename": "August customers.xlsx",
  "sheet_name": "Data",
  "columns": [
    { "key": "name", "original": "name", "inferred_type": "string" },
    { "key": "total", "original": "total", "inferred_type": "number" }
  ],
  "row_count": 2,
  "created_at": "2026-08-19T11:18:02Z"
}

Example

curl -X POST https://sheetrender.com/api/v1/templates/tpl_8f2c/datasets/rows \
  -H "Authorization: Bearer $SHEETRENDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"rows":[{"name":"Ada","total":42},{"name":"Ben","total":17}],"name":"August customers"}'

GET/api/v1/templates/{id}/datasets

List the datasets this template can run a batch against, newest first.

  • Scoped to the template’s project, so a dataset uploaded against any template there appears here. An unknown template answers 404.

Response

200 application/json

[
  {
    "id": "ds_1a90…",
    "filename": "customers.csv",
    "sheet_name": "csv",
    "columns": [
      { "key": "client", "original": "Client", "inferred_type": "string" },
      { "key": "invoice_no", "original": "Invoice No", "inferred_type": "string" },
      { "key": "total", "original": "Total", "inferred_type": "number" }
    ],
    "row_count": 240,
    "created_at": "2026-08-19T11:18:02Z"
  }
]

Example

curl https://sheetrender.com/api/v1/templates/tpl_8f2c/datasets \
  -H "Authorization: Bearer $SHEETRENDER_API_KEY"

POST/api/v1/jobs

Queue a batch: every row of a dataset rendered with a template.

  • The template and the dataset must belong to the same project. Create the dataset with POST /templates/{id}/datasets or POST /templates/{id}/datasets/rows, or pick one from GET /templates/{id}/datasets.
  • filename_template and group_by are project settings, not per-job arguments: passing them durably reconfigures the project, exactly as editing them in the app would. Later jobs — including scheduled ones — use the new values.
  • group_by must name a column key from the dataset (columns[].key), otherwise 400.
  • Billed per rendered document as the job runs.

Request body

{
  "template_id": "tpl_8f2c…",
  "dataset_id": "ds_1a90…",
  "filename_template": "{{ client }}-{{ invoice_no }}.pdf",
  "group_by": "client"
}

Response

201 application/json

{ "job_id": "job_44b1…" }

Example

curl -X POST https://sheetrender.com/api/v1/jobs \
  -H "Authorization: Bearer $SHEETRENDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"template_id":"tpl_8f2c","dataset_id":"ds_1a90"}'

GET/api/v1/jobs/{id}

Poll a batch job and collect the document ids it produced.

  • status moves through queued and running to succeeded, failed, or cancelled.
  • documents is empty until the job finishes, then lists one entry per rendered PDF in row order.

Response

200 application/json

{
  "id": "job_44b1…",
  "status": "succeeded",
  "rows_total": 240,
  "rows_done": 240,
  "rows_failed": 0,
  "created_at": "2026-08-19T11:20:04Z",
  "finished_at": "2026-08-19T11:22:47Z",
  "documents": [{ "id": "doc_71c3…", "filename": "Acme-1043.pdf" }],
  "merged_available": true,
  "zip_available": true
}

Example

curl https://sheetrender.com/api/v1/jobs/job_44b1 \
  -H "Authorization: Bearer $SHEETRENDER_API_KEY"

GET/api/v1/documents/{id}

Download one rendered PDF from a batch.

  • A document id from another account answers 404, never 403.

Response

200 application/pdf
Content-Disposition: attachment; filename="Acme-1043.pdf"

<PDF bytes>

Example

curl https://sheetrender.com/api/v1/documents/doc_71c3 \
  -H "Authorization: Bearer $SHEETRENDER_API_KEY" \
  --output Acme-1043.pdf

Rate limits and errors

Each key is limited to 120 requests per minute. Over that, requests answer 429 until the window rolls forward. Batch jobs additionally allow at most 12 job starts per 10 minutes per account. They are still the cheaper way to render a lot of documents: one request queues the whole dataset.

Errors come back with the matching HTTP status and a JSON body in FastAPI's shape, so the message is always at detail.

{ "detail": "A maximum of 10 active API keys is allowed" }
StatusWhen you see it
400The request cannot be acted on — a template placeholder that does not resolve, a grouping column that is not in the dataset, a template with no current version, an unreadable spreadsheet, more than 500,000 cells, or more than 50,000 rows on POST /templates/{id}/datasets/rows.
401Missing, malformed, or revoked key. Check the Authorization header.
403The key is good but the plan is not: a monthly document limit, or a feature your plan does not include.
404No such template, job, dataset, or document in your account. Ids belonging to another account also answer 404.
409A different design is already running for that project. Wait for it to finish, then try again.
413The HTML exceeds 2 MB, before or after data substitution, or a dataset file exceeds 20 MB.
429Rate limit exceeded. Back off and retry.

MCP server

The same API is available to AI assistants over the Model Context Protocol, so a chat client can design a template from a spreadsheet and an example, list your templates, and render documents without you writing the HTTP calls. Add this to your MCP client configuration:

{
  "mcpServers": {
    "sheetrender": {
      "command": "npx",
      "args": ["-y", "@sheetrender/mcp"],
      "env": { "SHEETRENDER_API_KEY": "sr_live_..." }
    }
  }
}

The server reads its key from SHEETRENDER_API_KEY and calls the endpoints above, so the same rate limit, plan limits, and document billing apply.