PDF Build
How to generate a PDF by sending Impression a JSON description of your pages and content.
Overview
The PDF Build endpoint turns a JSON payload into a finished PDF. You describe the document you want: the page backgrounds, positioned text, styled cards, and pie or donut charts, plus flowing content such as headings, fields, and tables. The service renders it and hands back the PDF.
A payload has three main parts:
- Templates: reusable page backgrounds (an image or a PDF) at a set physical size.
- Screens: the logical sections of your document. Each screen produces one or more pages and holds your content.
- Document: global settings and the order the screens appear in.
You never deal with DPI, page-break math, or font files. You give positions and sizes in real-world units (millimeters or inches) and the service handles the rest, including flowing long content onto extra pages automatically.
The endpoint can also stamp an existing PDF: drop a signature and a date onto a document you already have, without rebuilding it. See Stamping an existing PDF.
Making a request
Send an HTTP POST with your payload as the JSON request body:
POST https://api.toflourish.org/impression/pdf-build
Content-Type: application/json
Every request needs three headers:
| Header | Purpose |
|---|---|
fl-api-org | Your organization identifier. |
fl-api-env | The environment to run the request against. |
fl-api-token | Your API token. Treat it as a secret: keep it server-side and out of source control. |
Your Flourish contact issues the org identifier, the environment values, and the token.
The response is always 200 with a JSON body. On success it contains the finished PDF as a base64 string:
{ "base64": "JVBERi0xLjcKJ..." }
Decode that string to get the PDF bytes.
Payloads are not validated before rendering, so a malformed payload fails at render time rather than returning a description of what was wrong. When a request misbehaves, check the field names and types against this page first.
A minimal example
This renders one blank A4 page with a single line of text:
{
"screens": [
{
"id": "page1",
"blocks": [
{ "type": "text", "x": 20, "y": 20, "width": 170, "value": "Hello, world." }
]
}
],
"document": {
"unit": "mm",
"screens": ["page1"]
}
}
No templates are required. With no background and no size given, the page defaults to A4 (210 x 297 mm).
Document settings
The document object controls global behavior and page order. It is the one part of the payload you always send.
| Setting | Purpose |
|---|---|
unit | "mm" or "in", the unit for every coordinate and size in the payload. Optional; defaults to "mm". |
screens | An ordered list of screen IDs. This defines the final page sequence. List a screen ID more than once to repeat it. |
isSynchronous | Optional, defaults to true. Leave it true to get the PDF back as base64 in the response. |
defaultFont | Optional: "sans" (default, Noto Sans) or "serif" (Noto Serif). The font for all text unless a block overrides it. |
If you set unit to "in", then a width of 8.5 means 8.5 inches everywhere: positions, sizes, font box heights, all of it.
Templates (page backgrounds)
A template is a reusable background applied to a page. Define templates once in the top-level templates array and reference them from screens by id.
{
"id": "letterhead",
"asset": "<base64 or https URL>",
"assetType": "png",
"width": 210,
"height": 297
}
| Field | Purpose |
|---|---|
id | A name you choose, referenced by screens. |
width / height | The physical page size, in the document unit. Required. |
asset | The background, as either a base64 string or a public https:// URL. Optional: omit it for a blank page at the given size. |
assetType | jpg, jpeg, png, or pdf. Set it whenever asset is present. If you leave it off, the service falls back to the file extension of an https:// URL asset (for example .../logo.jpg gives jpg). It cannot infer the type of a base64 asset. |
- Image backgrounds (
png,jpg,jpeg) are drawn to fill the page, with your content rendered on top. - PDF backgrounds (
pdf) use the first page of the supplied PDF as the background, with your content merged on top of it.
The extension fallback only works for URLs. If a template has a base64 asset but no assetType, the page renders without the background rather than failing. A missing assetType shows up as a blank background, not an error.
When a screen uses a template, the template's width and height set the page size.
Screens and pages
A screen is one logical section of your document. Each screen produces at least one page, and more pages automatically if its content overflows.
{
"id": "invoice",
"initialTemplate": "letterhead",
"overflowTemplate": "plain",
"blocks": [ ]
}
| Field | Purpose |
|---|---|
id | Referenced from document.screens. |
initialTemplate | Template ID for the screen's first page. Optional. |
overflowTemplate | Template ID for any continuation pages when content overflows. Often the same as initialTemplate. Optional. |
width / height | Page size to use when the screen has no template, so you can set a size without a background. Ignored if a template is present. |
blocks | The content placed on the screen. |
If you give neither a template nor width and height, the page falls back to A4.
Blocks
A screen's blocks array holds two kinds of content.
Positioned blocks are placed at an exact x and y. Use as many as you like, in any mix:
- Text blocks (
type: "text"): a fixed string. - Image blocks (
type: "image"): an image at a fixed spot, such as a signature, seal, or logo. - Card blocks (
type: "card"): a styled box around text, for stat tiles, callouts, and bars. - Charts (
type: "pie"): a pie or donut chart.
Flowing content is a single form block (type: "form"), a column of content that paginates itself. A screen can hold at most one form block.
Positioned blocks are drawn in the order you list them, so a later block sits on top of an earlier one where they overlap.
Text blocks
A text block places a string at a precise spot, which suits filling in a letterhead, labels, or anything with a fixed position.
{
"type": "text",
"x": 20,
"y": 30,
"width": 80,
"value": "Invoice #1024",
"fontSize": 14,
"fontWeight": "bold",
"color": "#222222",
"align": "left"
}
| Field | Purpose |
|---|---|
x / y | Position on the page, in the document unit. |
width | Width of the text box. Text wraps within it. |
value | The string to render. |
fontSize | Points. Defaults to 11. |
fontWeight | "normal" (default) or "bold". |
color | Hex, such as #000000 (the default). |
align | "left" (default), "center", or "right". |
fontFamily | Optional: "sans" or "serif". Overrides the document's defaultFont for this block. |
height | Optional: the box height, used for vertical anchoring. |
Text renders in Noto Sans (default) or Noto Serif. Pick one per document with defaultFont, or per block with fontFamily. Both are Unicode fonts covering Latin (including accents), Cyrillic, Greek, and Vietnamese, so curly quotes and accented text work. Arabic, Hebrew, Thai, CJK, and emoji are not supported yet.
Positioning with anchors
By default x and y mark the top-left of the text box. Anchors change what the coordinates mean, which helps when centering on a page or aligning to a fixed point:
xAnchor:"left"(default),"center", or"right".yAnchor:"top"(default),"center", or"bottom".
To horizontally center a title on a 210 mm wide page, set x to 105, width to the box width, and xAnchor to "center".
Vertical centering (yAnchor: "center") uses the box height, so set height to the space you want the text centered within.
Text on overflow pages
If a screen overflows onto extra pages, you choose whether each positioned block repeats:
writeMode: "once"(default): only on the screen's first page.writeMode: "each": on every page, including continuation pages. Use this for a running header or footer.
Image blocks
An image block puts a picture at an exact spot: a signature, a seal, a logo.
{
"type": "image",
"x": 20,
"y": 240,
"width": 50,
"asset": "<base64 or https URL>"
}
| Field | Purpose |
|---|---|
x / y | Position of the image box, in the document unit. |
asset | The image, as base64 or a public https:// URL. SVG works here and is converted automatically. |
width | Render width. |
height | Render height. |
origin | Which page corner x and y are measured from. |
writeMode | "once" (default) or "each", as for text blocks. |
Give width only and the height follows the image's real proportions, which is usually what you want for a signature. Give height only and the width follows. Give both and the image is stretched to exactly that box. Give neither and the image renders at its natural pixel size.
This is the positioned twin of the image element you can put inside a form block. Use the block when you want the image at a fixed spot on the page; use the element when it should flow in sequence with the rest of your form content.
The two behave differently on purpose. A form image element always treats width and height as an exact box, with no aspect-ratio fitting. Prefer the positioned block when you want a signature at a fixed spot.
Positioning from any page corner
By default x and y are measured from the top-left corner of the page, with y increasing downward. Any positioned block (text, image, card, or pie) can measure from a different corner instead with origin:
{ "type": "image", "origin": "bottom-right", "x": 30, "y": 20, "width": 50, "asset": "..." }
origin:"top-left"(default),"top-right","bottom-left", or"bottom-right".- Coordinates are always positive distances measured inward from that corner. There are no negative coordinates to reason about.
- The block's matching corner is what gets placed.
"bottom-right"withx: 30, y: 20means the block's bottom-right corner sits 30 from the right edge and 20 up from the bottom, so the block always stays on the page.
Use this whenever you do not know the page size in advance, most obviously when stamping a PDF that someone else produced. A signature at bottom-right lands correctly on Letter, A4, and a mixed document, with no measuring on your side.
For text blocks, a right or bottom origin takes over from xAnchor or yAnchor on that axis. They are two answers to the same question, and origin is the more specific one. Leaving origin off (or setting top-left) keeps anchors behaving exactly as before.
Card blocks
A card is a styled box that hugs its text: a colored fill, an optional border, rounded corners, and padding, with one or more lines inside. The box auto-sizes to fit its lines plus padding unless you set an explicit height. Cards suit stat tiles, callout panels, and highlighted numbers.
{
"type": "card",
"x": 0.8, "y": 2.8, "width": 1.6, "height": 0.95,
"fill": "#e6f2f2", "borderRadius": 0.08, "padding": 0.15,
"vAlign": "middle", "lineGap": 0.07,
"lines": [
{ "value": "STUDENTS REACHED", "fontSize": 8, "fontWeight": "bold", "color": "#2f7d7d" },
{ "value": "2,380", "fontSize": 26, "fontWeight": "bold", "color": "#2f7d7d" }
]
}
| Field | Purpose |
|---|---|
x / y / width | Position and width. |
height | Optional. Omit to auto-fit the content; set the same value across a row of cards to force uniform heights. |
fill | Background hex color. Omit for no fill. |
border | { "color": "#...", "width": 0.01 }. |
borderRadius | Corner radius, in the document unit. Defaults to 0. |
padding | A single number (all sides) or { "top", "right", "bottom", "left" }. |
lines | Text lines, each with its own fontSize, fontWeight, color, and align. |
lineGap | Extra vertical space between lines. |
vAlign | "top" (default) or "middle", which centers the lines within the box height. |
Cards are positioned like text, so putting several at the same y and different x gives a row of tiles side by side.
A card with no lines and an explicit height is a filled box, which works for a colored rule or a data bar whose width encodes a value:
{ "type": "card", "x": 2.4, "y": 5.9, "width": 1.8, "height": 0.16, "fill": "#439799", "borderRadius": 0.08 }
Charts (pie and donut)
A pie block draws a pie or donut from a list of slices. Set innerRadius for a donut, and optionally a centerLabel in the hole. Legend percentages are computed automatically from each slice's value.
{
"type": "pie",
"x": 0.8, "y": 3.3, "size": 1.0,
"innerRadius": 0.6,
"centerLabel": "93%",
"slices": [
{ "label": "Completed", "value": 93, "color": "#439799" },
{ "label": "Left early", "value": 7, "color": "#dddddd" }
]
}
| Field | Purpose |
|---|---|
x / y | Top-left of the chart's square area. |
size | Diameter, in the document unit. |
slices | Each is { value, color, label }. Drawn clockwise from the top, sized by share of the total. |
innerRadius | 0 (default) is a full pie; a fraction such as 0.6 makes a donut. |
legend | Show a swatch, label, and percent list to the right. Defaults to true. |
centerLabel | Text drawn in the middle, for donuts only. |
centerLabelSize | Point size of that text. Defaults to 13. |
centerLabelColor | Hex color of that text. Defaults to #1a1a1a. |
Form blocks (flowing content)
A form block is a single column of content that lays itself out top to bottom and flows onto new pages automatically when it runs out of room. This is where most document content lives.
{
"type": "form",
"x": 20,
"y": 40,
"width": 170,
"elements": [
{ "type": "heading", "value": "Order Summary" },
{ "type": "field", "label": "Customer", "value": "Acme Corp" },
{ "type": "divider" },
{ "type": "table",
"headers": ["Item", "Qty", "Price"],
"rows": [["Widget", "2", "$10"], ["Gadget", "1", "$25"]] }
]
}
| Field | Purpose |
|---|---|
margin | Content insets from the page edges: either one number (all four sides) or { "top", "right", "bottom", "left" }. This is the simplest way to place a form, since it defines the whole content box. The bottom defaults to 10 if you do not set it. |
x / y | Optional: where the column starts on the first page. Overrides the left and top margin. |
overflowY | Optional: where the column restarts on continuation pages when content overflows. Defaults to the form's top. |
width | Optional: column width. Overrides the right margin; defaults to the page width minus the left and right margins. Height is automatic. |
elements | The content, rendered in order. |
Place a form either with x, y, and width (precise coordinates) or with margin (insets from the page edges), whichever is simpler. If you give both, the explicit coordinate wins for that side.
When your page background has a header at the top and a footer (a signature line, say) at the bottom, use margin to reserve space for both so form fields never overlap them:
{ "type": "form", "margin": { "top": 42, "right": 18, "bottom": 34, "left": 18 }, "elements": [ ] }
The top clears the header and the bottom reserves the footer. Because the top margin also applies to continuation pages, every overflow page stays clear of both. The bottom margin defaults to 10, so without it a tall footer can be overlapped once a page fills up.
If your form begins partway down the first page (below a chart, for instance) and overflows, continuation pages would otherwise restart at that same y, leaving their tops empty. Set overflowY to a small value such as 20 so overflow pages flow from near the top. Leave it unset when your overflowTemplate has header artwork the content must clear.
Element types
| Type | What it is | Key fields |
|---|---|---|
heading | A bold section title. Extra space is added above it automatically so sections read as separate, and dropped when the heading falls at the top of a page, where that space would look like a stray indent. You do not need to insert spacing before headings yourself. | value |
field | A label above a value, with an underline beneath. | label, value, size |
table | A table with a header row and data rows. Columns are even width, and long cell text wraps within its column (rows grow to fit) rather than spilling into the next column. | headers (array), rows (array of arrays, each row matching the header count) |
text | A paragraph of text. Plain by default, with blank lines separating paragraphs. If the value is HTML it is auto-detected and rendered with formatting. Flows across pages between paragraphs and list items. | value, size |
image | An embedded image. | asset, and optionally label, width, height |
divider | A horizontal rule. | - |
blank | Vertical spacing, or, with size, a spacer cell holding columns open in a row. | height, size |
Examples:
{ "type": "text", "value": "Thank you for your business." }
{ "type": "blank", "height": 10 }
{ "type": "image", "label": "Signature", "asset": "<base64 or URL>", "width": 60, "height": 20 }
A table does not split across a page break. If it does not fit in the space left, it moves whole to the next page.
Rich text (HTML in a text element)
There is no separate rich-text element. A text element auto-detects HTML: if the value contains formatting tags or entities it renders with formatting, and otherwise it renders as plain text. So you can hand a Salesforce Rich Text Area field's stored value straight to a text element, with no stripping:
{ "type": "text",
"value": "<p>Please <b>agree</b> to the following:</p><ul><li>I consent to the procedure.</li><li>I confirm the information is accurate.</li></ul>" }
- Formatting is preserved. The list renders with real bullets and numbers,
<b>,<i>, and<u>styling, and links, instead of collapsing into one paragraph. - It flows across pages naturally, breaking between list items and paragraphs rather than mid-line, and respects your form
marginandoverflowYso it clears baked headers and footers on every page. - Supported HTML is a common subset:
b,i,u,s,a,ul,ol,li,h1toh6,<font color/size>, alignment,blockquote,hr, andbr. Malformed or unsupported HTML degrades to plain text rather than failing the render. For images, use animageelement. - Plain values are safe. A value like
"5 < 10","R&D", or"use <name> here"renders literally and is not treated as HTML. To force a literal that looks like a tag (to show the text<br>, say), HTML-escape it as<br>.
One long paragraph with no break point inside it is treated as a single unit. If it does not fit on the current page it moves whole to the next one rather than splitting mid-line.
Side-by-side content with size
By default an element fills the full column width and the next one stacks below it. Give it a size, a 1 to 12 column span in a 12-column grid, to place elements side by side:
{ "type": "field", "label": "First name", "value": "Wile", "size": 6 },
{ "type": "field", "label": "Last name", "value": "Coyote", "size": 6 }
Two size: 6 elements share one row; three size: 4 make three columns; size: 12 (the default) is full width.
size works on field, text, and blank, and they mix freely in the same row, so you can put explanatory text beside a field:
{ "type": "field", "label": "Dose", "value": "0.5 mL", "size": 4 },
{ "type": "text", "value": "Administered in the left deltoid.", "size": 8 }
- Cells pack in order. Consecutive sized elements fill a row left to right until the next one will not fit in the remaining columns, then it wraps. A
size: 6followed by asize: 8puts the second on its own row, because 6 plus 8 is more than 12. heading,table,divider, andimageare always full width and start a fresh row after them.- A sized
blankis a spacer cell, not a gap. It holds its columns open, so asize: 6field followed by asize: 6blank leaves the right half of that row empty. - Uneven content is fine. If one cell wraps to more lines than its neighbor, the row grows to match, and the next row starts below everything. Field underlines stay just beneath their own value rather than dropping to the bottom of a taller neighbor.
Long text in a column still flows across pages. A sized text breaks between its paragraphs or list items just as a full-width one does, and the remainder continues in the same column on the next page. Anything beside it that already fits, a field for instance, is drawn once and not repeated.
Stamping an existing PDF
Sometimes you do not want to build a document: you already have one, and you need to add something to it. A user clicks Sign and you need their signature and today's date on every page of a completed form. That is a stamp.
Send the existing PDF as source and the things to add as stamps:
{
"source": { "asset": "<base64 of the existing PDF>", "assetType": "pdf" },
"stamps": [
{
"pages": "all",
"blocks": [
{ "type": "image", "origin": "bottom-right", "x": 1.4, "y": 0.6,
"width": 1.8, "asset": "<signature base64>" },
{ "type": "text", "origin": "bottom-left", "x": 0.75, "y": 0.6,
"width": 3, "value": "Signed 2026-07-28", "fontSize": 9 }
]
}
],
"document": { "unit": "in", "isSynchronous": true }
}
You get back the same PDF with the stamps applied, as the same base64 response as always. Nothing is laid out again and nothing is re-encoded, so the original text stays selectable and the pages keep their exact size.
A stamp draws the positioned blocks described above: text, image, card, and pie. It cannot contain a form block, because a stamp is applied after the document has finished paginating.
Choosing pages
pages | Stamps |
|---|---|
"all" | Every page. This is the default. |
"first" | Page 1 only. |
"last" | The final page, however long the document turned out. |
[1, 2, 5] | Those page numbers, counting from 1. |
[-1] | Counting back from the end, so -1 is the last page and -2 the second to last. |
Page numbers that do not exist are quietly skipped, so asking for page 5 of a 3-page document is not an error.
Use several stamp groups when different pages need different things:
"stamps": [
{ "pages": "all", "blocks": [ ] },
{ "pages": "last", "blocks": [ ] }
]
Generating and stamping in one request
If you are generating the document and stamping it in the same operation, do not make two calls. Send screens and stamps together, and the service renders the document, then stamps the finished result:
{
"screens": [ ],
"stamps": [ { "pages": "last", "blocks": [ ] } ],
"document": { "unit": "in", "screens": ["form"] }
}
Two reasons this beats render-then-stamp as separate calls:
- The document crosses the wire once. A round trip sends the whole PDF back up as base64 and returns it again, which for a large document can push you into the response size limit described below.
"last"becomes meaningful. When you are generating, you often do not know how many pages you will get, because that depends on how the content flowed. Stamps are applied after layout finishes, so"last"resolves to the page that actually ended up last.
What is handled for you
- Rotated pages. A page saved at 90 or 180 degrees (common in anything that has been through a scanner) is stamped as it looks, not as it is stored. Your coordinates always describe the page the way a reader sees it.
- Unusual page boxes. Pages that have been cropped, or whose coordinates do not start at zero, are handled, so the stamp lands where you would expect on screen.
- Mixed page sizes. Use
originand each page is measured on its own terms.
Limits
- Only PDFs can be stamped.
sourcemust be a PDF, not an image. - Interactive form fields are left alone. If the source has fillable form fields, stamps are drawn over the page as ordinary content: the fields are not filled in and not flattened. Keep stamps clear of live fields.
- Encrypted PDFs. A document encrypted only to restrict permissions opens automatically. One with a real password needs
passwordon thesourceobject. - Size. The PDF goes up as base64 and comes back the same way, against a response ceiling of roughly 6 MB. Large documents are exactly why you should render and stamp in one request rather than two.
Images, URLs, and SVG
Anywhere an asset is accepted (template backgrounds, image blocks, and form image elements), supply either:
- a base64-encoded string, or
- a public
https://URL, which the service fetches.
For image blocks and image elements, SVG is supported: pass SVG markup as base64 or via a URL and it is converted to a crisp raster image at render time. SVG is not supported for template backgrounds. Use PNG, JPG, or PDF for a background.
Page sizes
Set the page size by giving explicit width and height, on a template, or directly on a screen when you do not have a background. There is no "A4" or "letter" shorthand, so use the dimensions:
| Size | Millimeters | Inches |
|---|---|---|
| A4 | 210 x 297 | 8.27 x 11.69 |
| US Letter | 215.9 x 279.4 | 8.5 x 11 |
| US Legal | 215.9 x 355.6 | 8.5 x 14 |
If you give no size at all, pages default to A4.
A fuller example
A two-page-capable invoice: a letterhead background on the first page, a plain background on any overflow page, a positioned invoice number, and a flowing form block.
{
"templates": [
{ "id": "letterhead", "asset": "<base64>", "assetType": "png", "width": 215.9, "height": 279.4 },
{ "id": "plain", "width": 215.9, "height": 279.4 }
],
"screens": [
{
"id": "invoice",
"initialTemplate": "letterhead",
"overflowTemplate": "plain",
"blocks": [
{ "type": "text", "x": 107.95, "y": 25, "width": 120, "value": "Invoice #1024",
"fontSize": 16, "fontWeight": "bold", "align": "center", "xAnchor": "center" },
{ "type": "text", "x": 20, "y": 12, "width": 80, "value": "ACME",
"writeMode": "each" },
{ "type": "form", "x": 20, "y": 45, "width": 175.9, "elements": [
{ "type": "heading", "value": "Order Summary" },
{ "type": "field", "label": "Billed To", "value": "Wile E. Coyote" },
{ "type": "table",
"headers": ["Item", "Qty", "Price"],
"rows": [["Anvil", "3", "$30"], ["Rocket", "1", "$99"]] },
{ "type": "divider" },
{ "type": "text", "value": "Payment due within 30 days." }
]}
]
}
],
"document": { "unit": "mm", "screens": ["invoice"], "isSynchronous": true }
}
Tips
- One unit, everywhere. Pick
mmorinonce indocument.unitand use it for every number in the payload. A payload that mixes them silently renders at 25.4 times the size you meant. - Backgrounds set the size. When a screen uses a template, the template's
widthandheightdecide the page size. You do not need to set them on the screen as well. - Let content flow. Put long or variable-length content (tables, repeated fields) in a form block and it paginates for you. Use
overflowTemplateto control what continuation pages look like. - Running headers and footers. Add a text block with
writeMode: "each"so it repeats on every page of an overflowing screen. - Center with anchors. To center a title, set
xAnchor: "center"and putxat the page midpoint, rather than computing the left edge yourself. - Reuse screens. List the same screen ID twice in
document.screensto render it more than once. - Vector logos. Use an SVG in an image block or element for sharp logos and signatures at any size.
- Stat-tile rows. Place card blocks at the same
y, differentx, with the sameheight, for an aligned row of tiles. - Bars and rules. A card with no text and an explicit height is a filled bar or divider. Set its
widthfrom your data for a simple bar chart.