Skip to content

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.

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.

<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.

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=%23ffffff sets an explicit color (#rgb or #rrggbb only; 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 description in 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 data shape the API expects, and anything you injected with composepdf:data survives. Their validation problems are not shown in the form, so if you hide a required field, fill it in from your page with composepdf:data.

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 (#rgb or #rrggbb only). 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 is center. none hides 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 omitting zoom leaves 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.

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}/pdf

GET /api/embed/{token}/doc returns that canvasId, so your integration only needs the embed token in its configuration.

  • 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.