Skip to main content

Rendering

Tag: Templates · Version: v1 · Stability: 🟒 Stable

The render endpoints turn template + data into a PDF. They are the only endpoints that accept either auth lane β€” a JWT (for previews from the platform) or an X-Api-Key (for your backend).

EndpointUse when
POST /api/templates/{id}/renderYou have a stored template; render it with fresh data.
POST /api/renderYou want a one-off render without persisting a template.
Performance baseline

~9.5 renders/sec sustained on a single box, p50 ~625 ms, p95 ~1.1 s, p99 ~1.3 s (measured on a laptop). Plenty of headroom for typical workloads.

Render a stored template​

POST /api/templates/{id}/render Β· JWT or API key Β· {id} is a UUID.

Request β€” RenderTemplateBody
{
"data": {
"invoiceNumber": "INV-1024",
"total": "$4,200.00",
"lineItems": [{ "name": "Design", "qty": 1, "price": "$4,200.00" }]
}
}
FieldTypeRequiredNotes
dataJSON objectβœ…Free-form; its keys resolve the template's {{tokens}}.
themeobject | nullβ€”A tenant's brand colours for this render. See Brand colours.

Returns the rendered PDF.

curl -X POST https://api.paperwright.dev/api/templates/<id>/render \
-H "X-Api-Key: <key>" \
-H "Content-Type: application/json" \
-d '{ "data": { "invoiceNumber": "INV-1024", "total": "$4,200.00" } }' \
--output invoice.pdf

Render inline (no stored template)​

POST /api/render Β· JWT or API key

Request β€” RenderInlineBody
{
"content": "<h1>Hello {{name}}</h1>",
"data": { "name": "World" },
"baseTemplateId": null
}
FieldTypeRequiredNotes
contentstringβœ…The template body with {{tokens}}.
dataJSON objectβœ…Values to resolve tokens against.
baseTemplateIdUUID | nullβ€”Reuse a stored template's base PDF as the background of this inline render.
themeobject | nullβ€”A tenant's brand colours for this render. See Brand colours.

Brand colours (theme)​

Both render endpoints take an optional theme that fills the template's brand colours for that one render β€” one stored template, rendered in each of your tenants' colours. The theme is never stored; it applies to the render it is sent with.

Request β€” with a theme
{
"data": { "invoiceNumber": "INV-1024", "total": "$4,200.00" },
"theme": { "primary": "#E11D48", "accent": "#0EA5E9" }
}
KeyTypeNotes
primarystring | null#rgb or #rrggbb. Replaces the template's primary colour.
accentstring | null#rgb or #rrggbb. Replaces the template's accent colour.
  • Per key. null, an empty string, or an omitted key means "keep the template's own colour" β€” a tenant with no brand colour still renders.
  • Readable text follows. Elements that use the template's "on primary" / "on accent" colours get black or white, whichever reads better on the colour you sent.
  • No brand slots, no effect. A template whose elements don't use the brand colours ignores theme and renders as designed. See Templates β†’ Brand colours for how a template opts in.
  • Validated first. A bad theme is rejected before the render starts, and a rejected request is not metered.
ProblemResult
A key other than primary / accent400 β€” theme has an unknown key "secondary"; allowed: primary, accent
A colour that isn't #rgb / #rrggbb400 β€” theme.primary must be a hex colour like #E11D48 (or theme.accent …)
curl -X POST https://api.paperwright.dev/api/templates/<id>/render \
-H "X-Api-Key: <key>" \
-H "Content-Type: application/json" \
-d '{ "data": { "invoiceNumber": "INV-1024" },
"theme": { "primary": "#E11D48" } }' \
--output invoice.pdf

Logos and other images​

An image element's source can be a token. Bind it to a data field such as {{logo}} and send the picture in data, either as a data URI or as bare base64 (the image type is detected for PNG, JPEG, GIF, WebP, BMP and SVG):

{
"data": {
"logo": "iVBORw0KGgoAAAANSUhEUgAA…"
}
}

A field that is missing or empty renders no image. Remote URLs are never fetched β€” the renderer has no network access β€” so send the file's bytes rather than a link.

Page size​

The page comes from the template, not the request: a template stores its paper, and a render uses it. There are 22 presets (ISO A3–A6 and B5, US Letter, Legal, Tabloid, Executive and Half Letter, labels, envelopes, cards, a ticket and two receipt rolls), each in portrait or landscape, plus a custom size. Anything unknown falls back to A4. See Templates β†’ Page size for the stored shape.

A receipt roll (80 mm or 58 mm) has a fixed width and renders as one page as tall as its content, rather than being split into A4-style pages. (A roll longer than 14,400 pt, the PDF readers' page limit, continues on a new page.)

Tables grow to fit their data​

A table row's height is a minimum. When a bound row's data wraps onto more lines than the designed height allows β€” a long description, or Arabic text β€” the row grows in the PDF instead of clipping, and the rows below move down with it. Header rows and rows that only hold fixed text keep their designed height.

Token grammar​

Tokens resolve against the data object at render time:

{{field}} plain substitution
{{total | number}} number, default pattern "#,##0.##"
{{total | number:"#,##0.00"}} number, custom pattern
{{rate | number:"percent"}} number preset
{{issuedAt | date:"medium"}} date preset
{{issuedAt | date:"dd/MM/yyyy"}} custom .NET-style format
{{$today | date:"long"}} current date at render time
{{$now | datetime:"short"}} current time at render time
{{$page}} / {{$pages}} page number / total

Date presets are medium, short, long and iso; datetime also accepts time and 24h. A custom date format uses .NET-style specifiers (yyyy, MMMM, dd, HH, mm, tt, and 'literal' text).

Numbers use the number filter with a preset (integer, decimal, percent) or a custom pattern: 0 and # for digits, , for grouping, . for the decimal point, %, 'literal' text, and ; to give negative numbers their own pattern. Rounding is half away from zero, applied to the value as written, so 1.005 formats as 1.01. A value that isn't a number prints unchanged. A table column can apply the same format to every cell.

Security model​

The renderer runs with JavaScript disabled and default-deny network egress (no SSRF) β€” markdown and icon SVGs are sanitised. AI-authored or user-authored content renders safely through the same hardened pipeline.

Metering​

Every render is metered (best-effort β€” a metering failure never fails a render). Renders are tagged by source (api vs preview); see Usage.