Payslip
Produce every employee payslip in one call and ship it password-protected, because it is personal data. You learn batches, passwords and permissions, and a bar chart of the monthly trend.
What this teaches
Building it in the editor
- Design one payslip as it should look. A batch calls this one template once per person, so the template itself must not contain the list of people.
- Make earnings and deductions two repeating tables, bound to data.earnings and data.deductions.
- Compute the net amount rather than accepting it: {{ format(sum(data.earnings, "amount") - sum(data.deductions, "amount"), "number", "0,0", "en-US") }} can never disagree with the two tables.
- Add a chart and set it to bar. Point it at data.trend, with {{ item.month }} as the label expression and {{ item.net }} as the value expression.
- A chart uses the same shape as a table — one array and two column definitions. That is deliberate, so there is no new concept to learn.
- Save and note the template_id. Each batch item carries a template_id and that person data.
Building it with the schema and API
- Post an items array to POST /v1/batch — up to 50 — each with its own template or template_id and its own data.
- Set merge to true and you get one merged file back. That path is capped at 20 and is the only one that waits and answers with a download URL.
- A merged file keeps each document header and footer and does not renumber pages — it is several documents in one file, not one document.
- Quota is checked once for the whole batch before queueing. If there is not enough it is rejected whole — a half-processed batch is awkward to refund and awkward to retry.
- password sets the open password and owner_password the permissions password. Making them the same renders the permissions below meaningless.
- permissions turns off print, copy, modify, annotate and fill individually. We never block text extraction for assistive software.
The finished document
The JSON below is exactly the document those two routes produce. Our tests validate it and check that it renders without warnings, so you can paste it and use it as is.
{
"v": 1,
"unit": "px",
"page": {
"size": "a4",
"orientation": "portrait",
"margin": {
"t": 48,
"r": 48,
"b": 56,
"l": 48
},
"header": "{{ data.company }} payslip",
"footer": "{{ data.period }} · [[page]] / [[pages]]"
},
"fonts": [],
"styles": [
{
"id": "s_h1",
"fontFamily": "Liberation Sans",
"fontSize": 20,
"fontWeight": 700,
"italic": false,
"color": "#111111",
"bg": "transparent",
"align": "left",
"valign": "top",
"lineHeight": 1.5,
"border": "none",
"radius": 0,
"opacity": 1
},
{
"id": "s_l",
"fontFamily": "Liberation Sans",
"fontSize": 12,
"fontWeight": 400,
"italic": false,
"color": "#333333",
"bg": "transparent",
"align": "left",
"valign": "top",
"lineHeight": 1.5,
"border": "none",
"radius": 0,
"opacity": 1
},
{
"id": "s_tbl",
"fontFamily": "Liberation Sans",
"fontSize": 11,
"fontWeight": 400,
"italic": false,
"color": "#111111",
"bg": "transparent",
"align": "left",
"valign": "middle",
"lineHeight": 1.5,
"border": "none",
"radius": 0,
"opacity": 1
},
{
"id": "s_net",
"fontFamily": "Liberation Sans",
"fontSize": 18,
"fontWeight": 700,
"italic": false,
"color": "#111111",
"bg": "transparent",
"align": "right",
"valign": "top",
"lineHeight": 1.5,
"border": "none",
"radius": 0,
"opacity": 1
}
],
"nodes": [
{
"id": "n_title",
"parent": "",
"order": 0,
"type": "label",
"x": 0,
"y": 0,
"w": 400,
"h": 26,
"show": "",
"styleId": "s_h1",
"props": {
"text": "{{ data.employee.name }}",
"src": "",
"fit": "",
"thickness": 0,
"stroke": "",
"fill": "",
"repeat": "",
"as": "",
"break": "",
"keep": "",
"padding": 0,
"padY": 0,
"padX": 0,
"align": "",
"valign": "",
"wrap": "",
"bind": "",
"headerHeight": 0,
"rowHeight": 0,
"max": 0,
"columns": [],
"rows": 0,
"cells": [],
"chartKind": "",
"codeKind": "",
"ecc": "",
"fieldKind": "",
"fieldName": "",
"options": [],
"required": false,
"readonly": false,
"html": ""
},
"name": "Name"
},
{
"id": "n_meta",
"parent": "",
"order": 1,
"type": "label",
"x": 0,
"y": 30,
"w": 400,
"h": 18,
"show": "",
"styleId": "s_l",
"props": {
"text": "{{ data.employee.dept }} · ID {{ data.employee.no }} · {{ data.period }}",
"src": "",
"fit": "",
"thickness": 0,
"stroke": "",
"fill": "",
"repeat": "",
"as": "",
"break": "",
"keep": "",
"padding": 0,
"padY": 0,
"padX": 0,
"align": "",
"valign": "",
"wrap": "",
"bind": "",
"headerHeight": 0,
"rowHeight": 0,
"max": 0,
"columns": [],
"rows": 0,
"cells": [],
"chartKind": "",
"codeKind": "",
"ecc": "",
"fieldKind": "",
"fieldName": "",
"options": [],
"required": false,
"readonly": false,
"html": ""
},
"name": "Department"
},
{
"id": "n_earn",
"parent": "",
"order": 2,
"type": "table",
"x": 0,
"y": 76,
"w": 340,
"h": 150,
"show": "",
"styleId": "s_tbl",
"props": {
"text": "",
"src": "",
"fit": "",
"thickness": 1,
"stroke": "#c9ced6",
"fill": "#eef4ff",
"repeat": "",
"as": "",
"break": "",
"keep": "",
"padding": 0,
"padY": 5,
"padX": 8,
"align": "",
"valign": "middle",
"wrap": "",
"bind": "data.earnings",
"headerHeight": 26,
"rowHeight": 22,
"max": 30,
"columns": [
{
"header": "Earnings",
"cell": "{{ item.name }}",
"width": 200,
"align": "left"
},
{
"header": "Amount",
"cell": "{{ format(item.amount, \"number\", \"0,0\", \"en-US\") }}",
"width": 140,
"align": "right"
}
],
"rows": 0,
"cells": [],
"chartKind": "",
"codeKind": "",
"ecc": "",
"fieldKind": "",
"fieldName": "",
"options": [],
"required": false,
"readonly": false,
"html": ""
},
"name": "Earnings"
},
{
"id": "n_ded",
"parent": "",
"order": 3,
"type": "table",
"x": 357,
"y": 76,
"w": 340,
"h": 150,
"show": "",
"styleId": "s_tbl",
"props": {
"text": "",
"src": "",
"fit": "",
"thickness": 1,
"stroke": "#c9ced6",
"fill": "#fff1f0",
"repeat": "",
"as": "",
"break": "",
"keep": "",
"padding": 0,
"padY": 5,
"padX": 8,
"align": "",
"valign": "middle",
"wrap": "",
"bind": "data.deductions",
"headerHeight": 26,
"rowHeight": 22,
"max": 30,
"columns": [
{
"header": "Deductions",
"cell": "{{ item.name }}",
"width": 200,
"align": "left"
},
{
"header": "Amount",
"cell": "{{ format(item.amount, \"number\", \"0,0\", \"en-US\") }}",
"width": 140,
"align": "right"
}
],
"rows": 0,
"cells": [],
"chartKind": "",
"codeKind": "",
"ecc": "",
"fieldKind": "",
"fieldName": "",
"options": [],
"required": false,
"readonly": false,
"html": ""
},
"name": "Deductions"
},
{
"id": "n_net",
"parent": "",
"order": 4,
"type": "label",
"x": 0,
"y": 244,
"w": 697,
"h": 26,
"show": "",
"styleId": "s_net",
"props": {
"text": "Net pay {{ format(sum(data.earnings, \"amount\") - sum(data.deductions, \"amount\"), \"number\", \"0,0\", \"en-US\") }}",
"src": "",
"fit": "",
"thickness": 0,
"stroke": "",
"fill": "",
"repeat": "",
"as": "",
"break": "",
"keep": "",
"padding": 0,
"padY": 0,
"padX": 0,
"align": "",
"valign": "",
"wrap": "",
"bind": "",
"headerHeight": 0,
"rowHeight": 0,
"max": 0,
"columns": [],
"rows": 0,
"cells": [],
"chartKind": "",
"codeKind": "",
"ecc": "",
"fieldKind": "",
"fieldName": "",
"options": [],
"required": false,
"readonly": false,
"html": ""
},
"name": "Net pay"
},
{
"id": "n_trend_l",
"parent": "",
"order": 5,
"type": "label",
"x": 0,
"y": 296,
"w": 697,
"h": 18,
"show": "",
"styleId": "s_l",
"props": {
"text": "Recent net pay",
"src": "",
"fit": "",
"thickness": 0,
"stroke": "",
"fill": "",
"repeat": "",
"as": "",
"break": "",
"keep": "",
"padding": 0,
"padY": 0,
"padX": 0,
"align": "",
"valign": "",
"wrap": "",
"bind": "",
"headerHeight": 0,
"rowHeight": 0,
"max": 0,
"columns": [],
"rows": 0,
"cells": [],
"chartKind": "",
"codeKind": "",
"ecc": "",
"fieldKind": "",
"fieldName": "",
"options": [],
"required": false,
"readonly": false,
"html": ""
},
"name": "Trend heading"
},
{
"id": "n_trend",
"parent": "",
"order": 6,
"type": "chart",
"x": 0,
"y": 320,
"w": 697,
"h": 200,
"show": "",
"styleId": "s_l",
"props": {
"text": "",
"src": "",
"fit": "",
"thickness": 0,
"stroke": "",
"fill": "",
"repeat": "",
"as": "",
"break": "",
"keep": "",
"padding": 0,
"padY": 0,
"padX": 0,
"align": "",
"valign": "",
"wrap": "",
"bind": "data.trend",
"headerHeight": 0,
"rowHeight": 0,
"max": 0,
"columns": [
{
"header": "Month",
"cell": "{{ item.month }}",
"width": 0,
"align": "left"
},
{
"header": "Net",
"cell": "{{ item.net }}",
"width": 0,
"align": "right"
}
],
"rows": 0,
"cells": [],
"chartKind": "bar",
"codeKind": "",
"ecc": "",
"fieldKind": "",
"fieldName": "",
"options": [],
"required": false,
"readonly": false,
"html": ""
},
"name": "Trend chart"
}
]
}The request body. Put the document above into template and the data below into data.
{
"template": "(the document above)",
"data": {
"company": "brewmypdf",
"period": "2026-08",
"employee": {
"name": "Do-hyun Kim",
"dept": "Engineering",
"no": "E-2041"
},
"earnings": [
{
"name": "Base salary",
"amount": 4200
},
{
"name": "Role allowance",
"amount": 300
},
{
"name": "Meal allowance",
"amount": 200
}
],
"deductions": [
{
"name": "Pension",
"amount": 189
},
{
"name": "Health insurance",
"amount": 148
},
{
"name": "Income tax",
"amount": 262
}
],
"trend": [
{
"month": "May",
"net": 4050
},
{
"month": "Jun",
"net": 4090
},
{
"month": "Jul",
"net": 4010
},
{
"month": "Aug",
"net": 4101
}
]
},
"password": "{{employee date of birth, 6 digits}}",
"owner_password": "{{your owner password}}",
"permissions": {
"print": true,
"copy": false,
"modify": false,
"annotate": false,
"fill": false
}
}