API reference
One base URL, JSON in, application/pdf out. Every endpoint below is
live on https://renderpaper.com.
Authentication
Create a key under API keys in your account. Send it as either header — both work identically:
X-API-Key: rs_live_… — or — Authorization: Bearer rs_live_…
A key is shown once, when it is created. Store it somewhere your application can read it and treat it like a password — anyone holding it can render documents billed to your account. Revoking a key takes effect immediately.
Your first render
The shortest path: post HTML and data together, get a PDF back in the response body. Nothing is stored.
$ curl -X POST https://renderpaper.com/v1/render \ -H "X-API-Key: $RENDERPAPER_KEY" \ -H "Content-Type: application/json" \ -d '{"template":"<h1>Invoice {{.Number}}</h1>","data":{"Number":"2026-0142"}}' \ -o invoice.pdf # invoice.pdf — 1 page, A4
Template syntax
Templates are Go html/template.
Every field of data is available by name, and values are HTML-escaped as they are
injected, so customer-supplied text cannot break your layout or inject markup.
| Syntax | Does |
|---|---|
| {{.Field}} | Insert a value by name. |
| {{.A.B}} | Reach into nested objects. |
| {{range .Lines}}…{{end}} | Repeat a block over an array. Inside, . is the current element. |
| {{if .Paid}}…{{else}}…{{end}} | Conditional block. |
| {{printf "%.2f" .Total}} | Format a number. |
A template that fails to parse or execute returns 400 with
the actual error message, so it is debuggable from the response rather than by guesswork.
Render inline HTML
Returns application/pdf. Use this when the template lives in your codebase.
| Field | Type | Notes |
|---|---|---|
| template | string | required HTML, with optional placeholders. |
| data | any JSON | Values injected into the template — object, array or scalar. |
| options | object | See options. |
Render a stored template
Returns application/pdf. Use this when someone other than a developer owns the
design — you send only the data, and edits made in the editor apply to the next render with no
code change.
{
"data": { "Number": "2026-0142", "Customer": "Northwind Ltd" },
"options": { "paper": "a4" }
}
Options
All optional, on every render endpoint.
| Field | Type | Default | Notes |
|---|---|---|---|
| format | "pdf" | "png" | "jpeg" | "webp" | "pdf" | What comes back. See image output. |
| paper | "a4" | "letter" | "a4" | PDF only. Case-insensitive. |
| landscape | boolean | false | |
| print_background | boolean | true | Set false to drop background colours and images. |
| margin_in | number | 0.4 | Inches, applied to all four sides. Maximum 5. |
Your CSS wins. An @page rule in the template takes precedence over
paper, landscape and margin_in. That is deliberate —
a template carrying its own page geometry renders identically wherever it is sent from. If
the paper option appears to be ignored, look for @page in your
CSS.
Image output
Set format to png, jpeg or webp on any
render endpoint and the response is that image instead of a PDF. Same template, same data,
same metering — an image counts as one document, however tall it is.
$ curl -X POST https://renderpaper.com/v1/render \ -H "X-API-Key: $RENDERPAPER_KEY" \ -H "Content-Type: application/json" \ -d '{"template":"<h1>{{.Title}}</h1>","data":{"Title":"Launch"}, "options":{"format":"png","width":1200,"height":630}}' \ -o card.png # card.png — 1200x630 CSS pixels at 2x, ready for an og:image tag
| Field | Type | Default | Notes |
|---|---|---|---|
| width | number | 800 | CSS pixels the page is laid out at. Max 4000. |
| height | number | full page | Omit it and the image extends to the laid-out height of the page. Max 12000. |
| scale | 1 | 2 | 3 | 2 | Device pixels per CSS pixel. 2 is crisp on ordinary screens. |
| quality | 1–100 | 90 | jpeg and webp only; png is lossless. |
A screenshot has a viewport, not a page. The paper,
landscape and margin_in options apply to PDFs only, and an
@page rule in your CSS is ignored for images. One hard bound: width or height
multiplied by scale may not exceed 16,000 device pixels — a request past
that is refused with an explanation, and a measured full-page height beyond it is truncated
rather than failed.
Templates are self-contained
The renderer fetches nothing over the network — not images, not stylesheets, not fonts. That is a deliberate property: a render cannot leak data to a third host, cannot slow down on someone else's CDN, and cannot change because a remote file did.
For images, upload them in the editor — references to uploaded assets
(/a/…) are inlined into the document automatically at render time — or embed
a data: URI directly. For type, use the built-in font set (Liberation and
Noto cover ordinary documents) or embed a font as @font-face with a
data: source.
List templates
[
{
"id": "inv_8f2c1a",
"name": "Invoice",
"updated_at": "2026-08-04T09:12:44Z",
"render_url": "https://renderpaper.com/v1/templates/inv_8f2c1a/render"
}
]
Create a template
| Field | Type | Notes |
|---|---|---|
| name | string | Truncated at 80 characters. |
| html | string | required Up to 512 KiB. |
| sample_data | string | JSON, as a string. Drives the editor preview. Up to 64 KiB. |
Returns 201 and the same object shape as the list endpoint.
Templates created through the API are editable in the browser editor, and vice versa — there
is one set of templates, not two.
Update a template
Same body as create. Renders that begin after the update use the new HTML; documents already accepted in a batch are unaffected, because a batch snapshots the template when it is accepted.
Delete a template
Returns 204. Deleting a template does not affect PDFs already rendered from it.
Submit a batch
For invoice and statement runs: one request instead of hundreds, with retries handled for
you and no HTTP timeout hanging over the whole job. Returns 202 Accepted — the
documents are queued, not finished.
Paid plans only. A free account gets 402 here and should render
documents one at a time with POST /v1/render instead. Queue position also
follows the plan: higher tiers are claimed first, and within a tier the queue is
first-in-first-out, so a large run is never starved by a trickle behind it.
| Field | Type | Notes |
|---|---|---|
| template_id | string | Use a stored template. One of this or html is required. |
| html | string | Inline HTML instead of a stored template. |
| options | object | Applied to every document in the batch, including format — a batch can produce images. |
| items | array | required Up to 500 objects, each {"data": …}. |
| webhook_url | string | Optional. https URL that receives a signed POST on completion. See webhooks. |
{
"batch_id": "bat_7d1e04",
"count": 3,
"status_url": "https://renderpaper.com/v1/batches/bat_7d1e04",
"items": [
{ "id": "job_a1", "position": 0,
"pdf_url": "https://renderpaper.com/v1/jobs/job_a1/pdf" }
]
}
The template is snapshotted at acceptance. Editing it while the batch runs cannot
change documents already accepted, so a long run stays internally consistent. A template
that fails to parse fails the whole batch immediately with 400, rather than
recording hundreds of identical failures.
Batch status
{
"batch_id": "bat_7d1e04",
"complete": false,
"counts": { "total": 3, "queued": 1, "running": 1, "done": 1, "failed": 0 },
"created_at": "2026-08-04T09:14:02Z",
"items": [
{ "id": "job_a1", "position": 0, "status": "done",
"pdf_bytes": 24518,
"pdf_url": "https://renderpaper.com/v1/jobs/job_a1/pdf" }
]
}
Poll this until complete is true. An item that failed every
attempt carries an error field explaining why.
Fetch a document
Returns application/pdf once the document is rendered.
While the job is still queued or running this returns 409 with a
Retry-After header — not 404. The job exists; it simply has
no result yet. Treat 409 as "poll again" and 404 as "wrong id".
Batch results are deleted automatically 24 hours after they are created, so fetch what you need within that window.
Completion webhooks
Add webhook_url to a batch and we POST the outcome to it once every
document has settled — no polling. The URL must be https on a public host.
{
"event": "batch.completed",
"batch_id": "bat_7d1e04",
"counts": { "total": 3, "done": 3, "failed": 0 },
"items": [ { "id": "job_a1", "status": "done", "pdf_url": "…" } ],
"timestamp": "2026-08-05T18:40:12Z"
}
Verifying the signature
Every delivery carries X-Renderpaper-Signature:
hex(HMAC-SHA256(body, key)) where the key is
hex(SHA256(your API key)) — derived from something you already hold, so
there is no extra secret to store.
const key = crypto.createHash('sha256').update(process.env.RENDERPAPER_KEY).digest('hex'); const sig = crypto.createHmac('sha256', key).update(rawBody).digest('hex'); // compare sig with the X-Renderpaper-Signature header, constant-time
Delivery is at-least-once. Two documents finishing simultaneously can both
observe a completed batch, so deduplicate on batch_id. Three attempts
over ~15 seconds; after that the failure is logged on our side and
batch status remains the source of truth.
Zapier, Make and n8n
There is no marketplace app to install — the API is plain HTTP, which every automation tool already speaks. Three working recipes:
Zapier
Add a Webhooks by Zapier → Custom Request step: method
POST, URL https://renderpaper.com/v1/render, headers
X-API-Key and Content-Type: application/json, and the JSON body
from your first render with fields mapped from earlier steps. The
response is the PDF; feed it to any step that accepts a file.
Make
Use the HTTP → Make a request module with the same
method, URL and headers. Body type Raw / JSON. Tick Parse response
off — the response is binary — and map the output to Google Drive, email, or wherever
the document goes.
n8n
An HTTP Request node, response format File.
For batches, pair it with a Webhook node and pass that node's URL as
webhook_url — n8n resumes when the batch completes, no polling loop.
These are recipes against the generic HTTP modules, not published marketplace apps; the field names above are exact.
Errors
Every error is JSON: {"error": "…"}.
| Status | Meaning |
|---|---|
| 400 | Invalid template, data or options. The parse or execution error is returned verbatim. |
| 401 | Missing, malformed or revoked API key. |
| 404 | No such template, batch or job — or it belongs to another account. |
| 409 | The document is not rendered yet. Retry after the interval in Retry-After. |
| 413 | Request body over the size cap. |
| 429 | Per-minute rate limit, or the monthly document allowance on a free plan. Retry-After is set — for an allowance it points at the start of next month. |
| 503 | Renderer at capacity. Retry shortly; Retry-After is set. |
| 500 | Render failure on our side. |
Limits
| Limit | Value |
|---|---|
| Request body, render endpoints | 1 MiB |
| Request body, batch endpoint | 8 MiB |
| Documents per batch | 500 |
| Template HTML | 512 KiB |
| Sample data per template | 64 KiB |
| Templates per account | 200 |
| Batch result retention | 24 hours |
Requests are also rate limited per account, and each plan includes a monthly document allowance — see pricing. On a paid plan, going over the allowance is billed rather than blocked, because a hard stop mid-run is the worse failure. On the free plan rendering stops at the allowance, since there is no card on file to bill the extra to. A failed render is never counted against the allowance.