Certificate
Produce one certificate per student from a single request. You learn repeating sections, starting each item on a new page, logo images, and a QR code for verification.
What this teaches
Building it in the editor
- Add a rectangle covering the whole page. This rectangle is the frame for one person — everything inside it is copied along with it.
- In the rectangle properties set the array path to data.students and the item name to st. Inside, you now write {{ st.name }}.
- Turn on "start each item on a new page". A certificate must be one page per person.
- Inside the frame add labels for the heading and the name. Make the name large and centred. Turning on italic for the citation gives it the tone of a certificate.
- Add an image and pick your logo file. The editor converts it to a data URI and stores it in the document — an external address is rejected at render time, so picking a file is the only way.
- Add a QR code and set the value to your verification address. Use {{ st.code }} so each copy carries a different code.
- Raise the QR error correction to Q. Printed paper gets folded and scuffed, so a slightly denser pattern that survives more damage is the better trade.
- Use a circle and a line to mark the seal area. Only the first copy is editable on screen; the others are made by your data and there is nothing there to edit.
Building it with the schema and API
- A repeating section is turned on by props.repeat on a rect node. There is no separate node type.
- A repeating block expands at most 100 times per render. With props.break set to "page" each copy starts a new page, so that is 100 pages. This is a separate budget from the table row limit (2,000 rows) and the two never eat each other — 100 recipients with 8 line items each is 100 blocks and 800 rows, both within limits. Copies past the limit are dropped and reported in warnings.
- props.as is the name for the current item. Leave it empty and it is block, so you write {{ block.name }}.
- Setting props.break to "page" starts every copy on a new page.
- Nodes inside set parent to the frame id, and their coordinates become relative to that frame.
- An image src accepts data: URIs only. An http or https address fails the render with E:render.compile#external-ref, because we send no outbound request while rendering.
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": "landscape",
"margin": {
"t": 48,
"r": 48,
"b": 56,
"l": 48
},
"header": "",
"footer": ""
},
"fonts": [],
"styles": [
{
"id": "s_kicker",
"fontFamily": "Liberation Sans",
"fontSize": 13,
"fontWeight": 400,
"italic": false,
"color": "#666666",
"bg": "transparent",
"align": "center",
"valign": "top",
"lineHeight": 1.5,
"border": "none",
"radius": 0,
"opacity": 1
},
{
"id": "s_name",
"fontFamily": "Liberation Sans",
"fontSize": 40,
"fontWeight": 700,
"italic": false,
"color": "#111111",
"bg": "transparent",
"align": "center",
"valign": "top",
"lineHeight": 1.5,
"border": "none",
"radius": 0,
"opacity": 1
},
{
"id": "s_cite",
"fontFamily": "Liberation Sans",
"fontSize": 14,
"fontWeight": 400,
"italic": true,
"color": "#333333",
"bg": "transparent",
"align": "center",
"valign": "top",
"lineHeight": 1.8,
"border": "none",
"radius": 0,
"opacity": 1
},
{
"id": "s_cap",
"fontFamily": "Liberation Sans",
"fontSize": 10,
"fontWeight": 400,
"italic": false,
"color": "#777777",
"bg": "transparent",
"align": "center",
"valign": "top",
"lineHeight": 1.5,
"border": "none",
"radius": 0,
"opacity": 1
}
],
"nodes": [
{
"id": "n_frame",
"parent": "",
"order": 0,
"type": "rect",
"x": 0,
"y": 0,
"w": 1026,
"h": 600,
"show": "",
"styleId": "s_cap",
"props": {
"text": "",
"src": "",
"fit": "",
"thickness": 2,
"stroke": "#c9a227",
"fill": "",
"repeat": "data.students",
"as": "st",
"break": "page",
"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": "One person frame"
},
{
"id": "n_logo",
"parent": "n_frame",
"order": 1,
"type": "image",
"x": 453,
"y": 40,
"w": 120,
"h": 60,
"show": "",
"styleId": "s_cap",
"props": {
"text": "",
"src": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAgNjAiPjxyZWN0IHdpZHRoPSIxMjAiIGhlaWdodD0iNjAiIHJ4PSI4IiBmaWxsPSIjMWYyOTM3Ii8+PHRleHQgeD0iNjAiIHk9IjM4IiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIyMCIgZmlsbD0iI2ZmZiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+TE9HTzwvdGV4dD48L3N2Zz4=",
"fit": "contain",
"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": "Logo"
},
{
"id": "n_kicker",
"parent": "n_frame",
"order": 2,
"type": "label",
"x": 0,
"y": 130,
"w": 1026,
"h": 20,
"show": "",
"styleId": "s_kicker",
"props": {
"text": "{{ data.org }}",
"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": "Organisation"
},
{
"id": "n_head",
"parent": "n_frame",
"order": 3,
"type": "label",
"x": 0,
"y": 160,
"w": 1026,
"h": 30,
"show": "",
"styleId": "s_kicker",
"props": {
"text": "CERTIFICATE OF COMPLETION",
"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": "Heading"
},
{
"id": "n_name",
"parent": "n_frame",
"order": 4,
"type": "label",
"x": 0,
"y": 210,
"w": 1026,
"h": 56,
"show": "",
"styleId": "s_name",
"props": {
"text": "{{ st.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_cite",
"parent": "n_frame",
"order": 5,
"type": "label",
"x": 163,
"y": 290,
"w": 700,
"h": 0,
"show": "",
"styleId": "s_cite",
"props": {
"text": "has successfully completed the {{ data.course }} course, and is awarded this certificate.",
"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": "Citation"
},
{
"id": "n_seal",
"parent": "n_frame",
"order": 6,
"type": "circle",
"x": 820,
"y": 420,
"w": 110,
"h": 110,
"show": "",
"styleId": "s_cap",
"props": {
"text": "",
"src": "",
"fit": "",
"thickness": 2,
"stroke": "#c9a227",
"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": "Seal area"
},
{
"id": "n_qr",
"parent": "n_frame",
"order": 7,
"type": "code",
"x": 80,
"y": 420,
"w": 110,
"h": 110,
"show": "",
"styleId": "s_cap",
"props": {
"text": "https://brewmypdf.com/verify/{{ st.code }}",
"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": "qr",
"ecc": "Q",
"fieldKind": "",
"fieldName": "",
"options": [],
"required": false,
"readonly": false,
"html": ""
},
"name": "Verification QR"
},
{
"id": "n_qr_l",
"parent": "n_frame",
"order": 8,
"type": "label",
"x": 50,
"y": 536,
"w": 170,
"h": 16,
"show": "",
"styleId": "s_cap",
"props": {
"text": "Verify · {{ st.code }}",
"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": "QR caption"
}
]
}The request body. Put the document above into template and the data below into data.
{
"template": "(the document above)",
"data": {
"org": "brewmypdf Academy",
"course": "Document Automation in Practice",
"students": [
{
"name": "Do-hyun Kim",
"code": "C-2026-0001"
},
{
"name": "Seo-yeon Lee",
"code": "C-2026-0002"
},
{
"name": "Jun-ho Park",
"code": "C-2026-0003"
}
]
}
}