Skip to content

Component templates

A component template describes a document as a tree of JSON components (containers, text, images, repeated lists and tables), each bound to a value in your data. Nothing in it runs: text is never read as a template expression, and data values are always inserted as text.

Choose components over Handlebars templates when a program generates the document, or when you want no code in the template at all. Scripts and helpers.js only run in Handlebars templates.

You build a heading, a table of items and a “Paid” badge, then preview, publish and render it.

You need the API_KEY and PDF_URL variables from the quickstart and an API key with the template:write, template:read, template:publish, render:create and render:read scopes. Studio and the client libraries cannot start a component template, so you create it with curl.

Save your data as order.json:

order.json
{
"order": { "number": "A-1042", "paid": true },
"lines": [
{ "name": "Desk lamp", "price": 39.9 },
{ "name": "Bulb, 2 pack", "price": 6.5 }
]
}

The composition is the template’s body. A root container holds a text bound to the order number, a table with one row per line, and a badge shown only when the order is paid:

composition
{
"root": {
"type": "container",
"style": {
"display": "flex",
"flexDirection": "column",
"gap": 16,
"padding": 24
},
"children": [
{
"type": "text",
"text": { "path": ["order", "number"], "prefix": "Order " },
"style": { "fontSize": 20, "fontWeight": 700 }
},
{
"type": "table",
"source": { "path": ["lines"] },
"columns": [
{ "id": "name", "header": "Item", "value": { "path": ["name"] } },
{
"id": "price",
"header": "Price",
"value": {
"path": ["price"],
"format": { "type": "currency", "currency": "EUR" }
}
}
]
},
{
"type": "text",
"text": "Paid",
"visibleWhen": { "path": ["order", "paid"], "operator": "truthy" }
}
]
}
}

Inside the table, paths are relative to each line, so ["name"] is the line’s name.

Send the composition in the draft’s content, next to its page settings (the same fields as page.json):

Terminal window
curl $PDF_URL/v1/templates \
-H "x-api-key: $API_KEY" \
-H "content-type: application/json" \
-d '{
"name": "Order confirmation",
"content": {
"composition": { "root": { ... } },
"settings": {
"pageSize": { "widthMm": 210, "heightMm": 297 },
"margins": { "topMm": 10, "rightMm": 10, "bottomMm": 10, "leftMm": 10 }
}
}
}'

Replace { ... } with the root from step 1. The response is the new template with draftRevision 1. Save its id:

Terminal window
export TEMPLATE_ID='paste-the-id-here'
Terminal window
curl $PDF_URL/v1/templates/$TEMPLATE_ID/preview \
-H "x-api-key: $API_KEY" \
-H "content-type: application/json" \
-d "{ \"data\": $(cat order.json) }" \
--fail-with-body -o preview.pdf

preview.pdf shows “Order A-1042”, a two-row table with prices such as “€39.90”, and the “Paid” badge. Preview again with "paid": false and the badge disappears.

The template also opens in Studio, where you edit composition.json. To preview there, add order.json as a workspace file and pick it in the Data selector above the preview.

Renders by name use the newest published version:

Terminal window
curl $PDF_URL/v1/templates/$TEMPLATE_ID/versions \
-H "x-api-key: $API_KEY" \
-H "content-type: application/json" \
-d '{ "expectedRevision": 1 }'

Render the template by name with the Reports API and save order.pdf:

Terminal window
curl "$PDF_URL/v1/reports/render" \
-H "x-api-key: $API_KEY" \
-H "content-type: application/json" \
--fail-with-body -o order.pdf \
-d "{
\"format\": \"pdf\",
\"template\": { \"name\": \"Order confirmation\" },
\"data\": $(cat order.json)
}"

To render a specific version in the background, use the Renders API.

Field Type Description
root object Required. The top component, usually a container.
components object Up to 100 reusable components by ID.
stylesheets string[] Up to 20 workspace stylesheet paths from the template’s folder, applied in order.
header string Handlebars page header, up to 100,000 characters. See headers and footers.
footer string Handlebars page footer, up to 100,000 characters.

In Studio, each part has its own file:

  • composition.json holds only root.
  • Page header and Page footer hold header and footer.
  • Template files above the preview lists the folder’s stylesheets; tick the ones to apply.
  • Studio does not show components. Saving there keeps them unchanged; change them through the API (PUT /v1/templates/{id}/draft).

Every component has a type and these optional fields:

Field Type Description
style object CSS properties in camelCase. See styling.
className string Up to 1,000 characters of space-separated classes from the template’s stylesheets.
visibleWhen object A condition. When it is false, the component and its children are left out.

Groups other components. Lay them out with CSS in style.

Field Type Description
children array Required. Up to 1,000 child components.
Field Type Description
text value Required. A literal, a binding, or mixed text.
Field Type Description
assetId string or binding Required. The ID of an uploaded image, or a binding that selects one.
width number Width in CSS pixels, above 0.
height number Height in CSS pixels, above 0.

To get an ID, add the image to the workspace in Studio and open it: the ID is shown as Asset ID. Or upload it with POST /v1/assets (see the API reference). Publishing copies the image into the version, so deleting it later does not change published versions.

A binding in assetId can only select an image the template already names literally, for example in another image component. Anything else fails with 400 ASSET_INVALID. To pick one of several logos, add one image per logo and show the right one with visibleWhen and equals.

Renders its children once for each item of an array. Inside, bindings read from the current item.

Field Type Description
source data source Required. The path of the array.
children array Required. Up to 1,000 components rendered for each item.
empty array Up to 1,000 components rendered instead when the array has no items.

The repeat is a flex column, and each item gets its own container. A source that is missing or null counts as empty; any other value that is not an array fails with 400 BAD_REQUEST.

Renders a header row and one row per item of an array.

Field Type Default Description
source data source Required. The path of the array; each item becomes a row.
columns array Required. 1 to 30 columns, described below.
showHeader boolean true false leaves out the header row.
headerStyle object Style for header cells, applied last.
cellStyle object Style for every cell.
emptyText string Up to 10,000 characters shown in one full-width row when the array is empty.

Each column has:

Field Type Description
id string Required. Unique in the table. Up to 128 letters, digits, _ and -, starting with a letter or digit.
header string Required. Header text, up to 1,000 characters. Can be empty.
value value Required. What the cell shows. Bindings read from the row’s item.
width number Width in CSS pixels, above 0 and up to 2,000. Without it, the table sizes the column.
style object Style for this column’s cells.

Default look and behaviour:

  • The table spans the full width with collapsed borders.
  • Cells have 6px padding, a 1px #d1d5db border and left-aligned text. Header cells are semibold on #f3f4f6.
  • cellStyle applies first, then the column’s style, then headerStyle on header cells.
  • A row is never split across a page break.
  • Without emptyText, an empty array leaves only the header row.

Set column widths with width, not a percentage in style: percentage cell widths can clip the contents.

Renders a reusable component.

Field Type Description
componentId string Required. The key of the reusable component.
data data source Where the component reads its data. Without it, it reads the caller’s data.

Define a block once under components, next to root, and use it anywhere with a component. The key is its ID: up to 128 letters, digits, _ and -, starting with a letter or digit. Each has a name (1 to 200 characters) and a root component.

{
"root": {
"type": "component",
"componentId": "address",
"data": { "path": ["billing"] }
},
"components": {
"address": {
"name": "Postal address",
"root": {
"type": "container",
"children": [
{ "type": "text", "text": { "path": ["name"] } },
{ "type": "text", "text": { "path": ["street"] } },
{ "type": "text", "text": { "path": ["city"] } }
]
}
}
}
}

Reusable components can use each other, but not themselves, directly or indirectly. An unknown componentId, a circular reference, or data pointing at a missing value fails the preview or render with 400 BAD_REQUEST.

text, a column’s value and an image’s assetId take a value:

  • A literal string (up to 100,000 characters), number or boolean: "Invoice", 42, true.
  • A binding to a path in the data: { "path": ["customer", "name"] }.
  • Mixed text joining literals and bindings.
Field Type Default Description
path string[] Required. 1 to 20 property names, each up to 128 characters.
scope string current current reads from the current repeat or table item, root from the whole data.
fallback string, number or boolean Shown when the path is missing or not a plain value.
format object How to format a number or date.
prefix string Text before the value, up to 1,000 characters.
suffix string Text after the value, up to 1,000 characters.

Array items use their index as a string, such as ["lines", "0", "name"]. The names __proto__, constructor and prototype are not allowed.

A data source (source in repeat and table, data in component) takes only path and scope.

Outside any repeat or table, the current item is the whole data. Inside one, use "scope": "root" to reach data outside the item:

{
"id": "currency",
"header": "Currency",
"value": { "path": ["invoice", "currency"], "scope": "root" }
}

A binding must find a string, number or boolean. If the value is missing, null, an object or an array, the preview or render fails with 400 BAD_REQUEST naming the path. Give every optional value a fallback, even "":

{ "path": ["order", "note"], "fallback": "" }

The fallback is shown as written, without format, but with prefix and suffix.

parts joins 1 to 100 literals and bindings into one string of up to 1,000,000 characters:

{
"type": "text",
"text": {
"parts": ["Dear ", { "path": ["customer", "firstName"] }, ", your order has shipped."]
}
}

A binding’s format turns a number or date into text for a locale:

Field Type Default Description
type string Required. number, currency, percent or date.
locale string see below A locale such as de-CH, 2 to 64 characters.
currency string A three-letter ISO 4217 code such as EUR. Required for currency.
minimumFractionDigits integer 0 to 20. For number, currency and percent.
maximumFractionDigits integer 0 to 20. For number, currency and percent.
dateStyle string medium short, medium, long or full. For date.
timeZone string UTC An IANA time zone such as Europe/Berlin. For date.
Type Value Output (en-GB)
number A number 1,234.5
currency A number €1,234.50
percent A number, where 0.2 is 20% 20%
date An ISO date (2026-10-08), an ISO timestamp with a zone (2026-10-08T07:30:00Z), or milliseconds since 1970 8 Oct 2026

Numbers must be JSON numbers, not strings such as "12.50". A value of the wrong type fails with 400 BAD_REQUEST.

Without locale, the format uses the render’s language: the request’s locale for the Reports API (en-US when left out) and for a preview that sends one, otherwise the locale in page.json. The Renders API always uses page.json.

visibleWhen shows a component only when a condition about the data holds:

{
"type": "text",
"text": "PAID",
"visibleWhen": {
"path": ["invoice", "status"],
"operator": "equals",
"value": "paid"
}
}
Operator Shows the component when the value is
truthy Present and not false, 0, "" or null.
falsy Missing, false, 0, "" or null.
exists Present and not null.
notExists Missing or null.
equals Exactly equal to value, same type and content.
notEquals Not exactly equal to value.

Conditions take path and scope like a binding. equals and notEquals need a value (a string, number or boolean); without one, the component is hidden. A missing path never fails a condition.

style takes CSS properties in camelCase. Numbers are CSS pixels; anything else is a string:

{
"style": {
"display": "flex",
"gap": 12,
"padding": 8,
"borderWidth": 1,
"borderStyle": "solid",
"borderColor": "#e5e7eb"
}
}

Styles are static and cannot be bound to data. To change the look with the data, show alternative components with visibleWhen.

For shared styles, apply workspace stylesheets (stylesheets, or Template files in Studio) and refer to their classes with className. Stylesheets can load workspace fonts and images. See supported CSS.

header and footer are Handlebars, drawn in the page margins with the same data and page numbers as in any template. See build a template.

Translation files work as in Handlebars templates: the file for the render’s language is added to the data as localization, unless the data already has one. Bind its labels like any value:

{ "type": "text", "text": { "path": ["localization", "invoiceTitle"] } }

Field limits are in the tables above. Limits on the whole composition, such as the number of components, nesting depth and items in one repeat or table, are in limits. Limits checked when you save the draft fail with 400 BAD_REQUEST; limits checked when a preview or render fills in the data fail with 400 LIMIT_EXCEEDED.

To render a tree you build in code without keeping a template, send a node document to POST /v1/renders. It has its own shape, with no bindings; see render documents.