Embedded live preview
The embed gives your users a live preview that follows their typing — your form on the left, the rendered document on the right. The preview runs entirely in the visitor’s browser with the same layout engine that renders the final PDF, so what they see is what the API will produce.
No API key is involved: the iframe is authorized by an embed token, which by default can only display the template — its blast radius stops at pixels, and PDF bytes come from your backend with an API key. Tokens issued with export enabled can also produce the final bytes in the browser and hand them to your page; see rendering from your host page.
1. Issue an embed token
Section titled “1. Issue an embed token”In the studio, open your canvas’s API panel and create an embed token.
Restrict it to the domains that may host the iframe — the allowlist is
enforced by the browser via Content-Security-Policy: frame-ancestors.
Add localhost or 127.0.0.1 to the allowlist to load the iframe from a
local dev server (any port, http or https). Prefer a separate token for
development: anyone who runs a server on their own machine can load the
preview from a token that allows localhost. Allowlist edits reach the
iframe’s origin check within a minute; hard-reload the page to pick them up
immediately.
2. Drop in the snippet
Section titled “2. Drop in the snippet”<iframe id="composepdf-preview" src="https://composepdf.com/embed/{token}" style="border: 0; width: 100%; height: 640px"></iframe>
<script> const frame = document.getElementById('composepdf-preview');
// Call this from your form's input handler. function preview(data) { frame.contentWindow.postMessage( { type: 'composepdf:data', v: 1, data }, 'https://composepdf.com', ); }
preview({ customer: { name: 'Acme Inc.' } });</script>Every composepdf:data message re-renders the preview with the new payload —
the shape is exactly the data object you would POST to the
Render API.
Options
Section titled “Options”Pass options in the URL or by message:
-
?outline=1— draw an outline around data-bound (variable) parts, useful while wiring up your form. -
postMessage({ type: 'composepdf:options', v: 1, outline: true }, …)— the same, toggleable at runtime. -
?bg=transparent— drop the preview’s own background so your page shows through.?bg=%23ffffffsets an explicit color (#rgbor#rrggbbonly; anything else falls back to the default). The paper itself keeps its panel and shadow either way. -
?form=1— generate the input form inside the iframe from the template’s data contract, instead of wiring up your own. Submit hands the values to your page (composepdf:submit); on a token that allows export, the Download PDF button above the paper stays available too and saves the current form values on the viewer’s device. Line-item arrays are edited as a table — one column per field, one row per item — and pressing Enter in a cell moves to the same column of the next row, adding a row when you are on the last one, so a 10-line invoice is typed without reaching for the mouse.Each field is labeled with the label of the matching variable (its
descriptionin the data contract — the same text the API schema and the mobile app show), falling back to the variable’s own name. Write those labels in the studio, either in the variables pane or in the field list of the embed panel, then publish the template for the embed to pick them up. -
?fields=customer(name,address),items(name,qty,price),total— which fields that generated form shows, and in what order. Each name is a field of the data contract, listed in display order; put a field’s own children in parentheses after it, which is how you order (or hide) the columns of a line-item table. The embed panel lets you drag them into place. Without it, the form shows every field in contract order, and a name with no parentheses keeps contract order below it. Unknown names are ignored, and if none of them match at a given level, that level is shown in full — a republished template never leaves an already-pasted iframe with an empty form. A malformed list (unbalanced parentheses, an empty name) is ignored entirely and you get the default form. Needs?form=1.Hidden fields stay in the payload as empty values, so what the form emits is still the exact
datashape the API expects, and anything you injected withcomposepdf:datasurvives. Their validation problems are not shown in the form, so if you hide a required field, fill it in from your page withcomposepdf:data.
Matching your design
Section titled “Matching your design”The embed panel has a picker for all of these, with a live preview of how the
frame will look. Issue a token after choosing and the snippet comes with them
baked in; for an iframe you have already pasted, copy the query string it shows
and append it to the src.
?accent=%230d437d— recolor the export button (#rgbor#rrggbbonly). You give one color; the hover and pressed shades are derived from it, and the label switches between light and dark automatically so the button stays readable. Anything else falls back to the default.?actions=left|center|right|none— where the button sits above the paper. Default iscenter.nonehides the button without disabling export, which is what you want when your own page triggers the export (see below) and you do not want two buttons that both produce a PDF.
The preview fits the container width by default, with no zoom controls. In a narrow column an A4 page can end up too small to read, so there are three ways to change that:
?zoom=1.5— initial scale, as a multiple of the fitted width. Clamped to 0.5–3; anything that is not a number falls back to fit-width.?zoomui=1— show−/+/ Fit width above the paper. Off by default, because the iframe otherwise carries no chrome of its own. The controls sit on the opposite side from the export button — at the right end normally, at the left end when?actions=right— so that zooming and exporting never read as one toolbar. In a narrow frame they drop onto their own line, each keeping its side.postMessage({ type: 'composepdf:options', v: 1, outline: false, zoom: 1.5 }, …)— drive the scale from your own toolbar.zoom: 'fit'returns to fit-width, and omittingzoomleaves the current scale alone, so messages from older integrations keep working unchanged.
Raising the zoom re-rasterizes every page, so keep long documents in mind: the canvas is capped at 8192px per side (past that the page is stretched in CSS rather than dropped by the browser), but the cost still scales with page count.
Rendering the PDF from your host page
Section titled “Rendering the PDF from your host page”Tokens issued with export enabled let the iframe produce the real PDF bytes on the visitor’s CPU — same engine, same fonts, same bytes as the API. Your page asks for them and receives them back:
// After composepdf:ready. Omit `data` to use whatever is on screen.frame.contentWindow.postMessage( { type: 'composepdf:export', v: 1, data, download: false }, EMBED_ORIGIN,);
window.addEventListener('message', (e) => { if (e.origin !== EMBED_ORIGIN) return; if (e.data?.type === 'composepdf:pdf') { const blob = new Blob([e.data.pdf], { type: 'application/pdf' }); // …upload it, attach it, or hand it to the visitor. } if (e.data?.type === 'composepdf:error') { // The export did not happen — e.g. the payload misses a required field. }});This is what makes a host-driven flow possible: your own button records the
document, then asks the iframe to bake it — the visitor never presses twice.
Sending data matters, because the preview lags your state by a debounce;
the payload you pass is the one that gets rendered and contract-checked.
On tokens without export, composepdf:export does nothing. Render the
PDF from your server instead, with an API key:
POST /v1/canvases/{canvasId}/pdfGET /api/embed/{token}/doc returns that canvasId, so your integration only
needs the embed token in its configuration.
Notes and limits
Section titled “Notes and limits”- Preview and export are separate permissions. By default the iframe only displays: no PDF bytes, no download button. Enable export on the token to turn on the flow above.
- Base-PDF underlays are drawn in the preview, so overlaying a fixed form looks right before you render.
- The preview is free — it renders on the visitor’s CPU and does not count against your rendering usage. Exports from the iframe are free for the same reason, and leave no server-side record; keep your own copy of any bytes you need to retain.