Pulp EngineDocument Rendering
Get started

Embeds — Integration Guide

Looking to embed rendered output (PDF/HTML) rather than an interactive surface? See embedding-guide.md.

Pulp Engine ships two companion custom elements:

  • <pulp-engine-editor> — the full template editor. For operators who let end users design and edit templates in-product.
  • <pulp-engine-form> — a fillable form derived from a template’s inputSchema. For end users who only need to enter data and receive a rendered PDF.

Both elements are iframe-isolated, share the same auth and CSP configuration, and are distributed by the Pulp Engine API server. Most of this guide covers the editor; the Form Embed section at the bottom documents the form element.

Install and compatibility

npm install @pulp-engine/editor-embed @pulp-engine/form-embed
# React 18 or later:
npm install @pulp-engine/embed-react

These ESM packages are typed wrappers. They load the custom element and iframe from your own Pulp Engine server; the designer itself stays on that server. Keep the wrapper minor version at or below the server minor within the same major. New methods feature-detect the server element and report unsupported features clearly. The plugin API has a separate 0.2.x version line.

Quick Start

1. Load the embed script

Add the script tag pointing at your Pulp Engine API server:

<script src="https://your-pulp-engine-server.com/pulp-engine-embed.js"></script>

2. Add the editor element

<pulp-engine-editor
  api-url="https://your-pulp-engine-server.com"
  template-key="invoice-v2"
  token="et_..."
  allowed-nodes="heading,text,table,image,container,columns"
  theme="light"
  style="width: 100%; height: 700px;"
></pulp-engine-editor>

3. Listen for events

const editor = document.querySelector('pulp-engine-editor')
editor.addEventListener('saved', (e) => {
  console.log(`Template ${e.detail.key} saved as version ${e.detail.version}`)
})
editor.addEventListener('dirty-change', (e) => {
  // Show/hide a "You have unsaved changes" banner
  setBannerVisible(e.detail.isDirty)
})

Authentication

Your backend mints short-lived editor tokens by calling the Pulp Engine API. The browser never sees the API key.

[Your Backend] --POST /auth/editor-token--> [Pulp Engine API]
                                              |
                                        { token, expiresAt }
                                              |
[Your Frontend] <-- pass token via props -----+

Pass the token to the editor element:

<pulp-engine-editor
  api-url="https://pulp-engine.internal"
  template-key="invoice-v2"
  token="et_abc123..."
  token-expires-at="2026-04-10T12:00:00Z"
></pulp-engine-editor>

When the token nears expiry, mint a new one server-side and refresh:

const editor = document.querySelector('pulp-engine-editor')
editor.setToken(newToken, newExpiresAt)

Development only: API key attribute

For prototyping, you can pass the API key directly. Do not use in production — the key is visible in the DOM.

<pulp-engine-editor
  api-url="http://localhost:3000"
  api-key="your-editor-key"
  template-key="invoice-v2"
></pulp-engine-editor>

Server Configuration

EMBED_ALLOWED_ORIGINS (required for production)

By default, the Pulp Engine server blocks iframe embedding via CSP frame-ancestors 'none'. To allow embedding, set:

EMBED_ALLOWED_ORIGINS=https://app.acme.com https://staging.acme.com

Space-separated list of origins permitted to embed the editor. This sets frame-ancestors on /embed.html responses.

EMBED_CONNECT_ORIGINS (optional)

Space-separated list of extra origins the embedded editor is permitted to open fetch/WebSocket connections to (connect-src in the embed CSP). Only set this if the embedding host needs to proxy API calls through a domain distinct from the Pulp Engine server.

EMBED_CONNECT_ORIGINS=https://forms-proxy.acme.com

CORS

If your frontend is on a different origin than the Pulp Engine API, ensure CORS_ALLOWED_ORIGINS includes your frontend origin:

CORS_ALLOWED_ORIGINS=https://app.acme.com

Attributes

Attribute Required Description
api-url Yes Base URL of the Pulp Engine API server
template-key No Template key to auto-load on init
token No* Pre-minted editor token (recommended for production)
token-expires-at No ISO date string for token expiry
api-key No* Raw API key (dev/prototyping only)
allowed-nodes No Comma-separated list of allowed node types
theme No light (default) or dark
locale No UI language: en, de, fr or ar; regional tags match their base language. Updates live.
locale-selectable No true lets users change a host-selected editor language. Otherwise the Language menu is hidden when locale is supplied.
preview No true (default) or false to hide preview
brand No JSON brand tokens; see white-labelling below
hidden-features No Comma-separated feature keys, updated live
readonly No Boolean attribute; use readonly="false" or remove it to enable editing
locked-properties No Comma-separated property paths, updated live
api-auth No editor-token (default) or sandbox-token

* Either token or api-key must be provided for authenticated servers.

Embed-mode defaults

The element and SDK default to one template, without tabs, a template picker or a New Template button. Preview stays enabled. Help and onboarding default off; collaboration follows the server capability. Raw pulp-engine:init integrations may enable the existing features flags. None of the new white-label options change these defaults.

setTemplate(key) replaces the active template and refreshes its preview data. Hosts may also supply an inline definition and preview data through the methods below.


Events

All events are standard DOM CustomEvents dispatched on the <pulp-engine-editor> element. Use addEventListener() to subscribe.

Event detail fields Description
ready protocolVersion: 2 Editor loaded and authenticated
template-loaded key, version Template fetched and rendered
dirty-change isDirty Unsaved changes state changed
saved key, version Template saved successfully
save-error message Save failed
action id, templateKey, version, isDirty Host toolbar action selected
snapshot requestId, template, previewData Snapshot response
snapshot-error requestId, message Snapshot unavailable
error message, code Auth/API/runtime error

Error codes

Code Meaning
AUTH_FAILED Token exchange or API key validation failed
AUTH_EXPIRED Editor session expired — provide a new token
TEMPLATE_LOAD_FAILED Could not fetch the specified template
READ_ONLY Host requested a save while view-only mode is active

Methods

Method Description
save() Programmatically trigger a save
setToken(token, expiresAt) Refresh the auth token
setTemplate(key) Load a different template
setTheme(theme) Switch light/dark live
setLocale(locale, selectable?) SDK: change UI language live; null removes the host override. Optional selectable controls the Language menu.
setBrand(brand) Replace brand tokens live
setConfig(partial) Update brand, hiddenFeatures, readOnly, lockedProperties or toolbarActions
setToolbarActions(actions) Replace up to five host actions
setInlineTemplate(definition) Load a definition without fetching it
setPreviewData(data) Replace preview data
requestSnapshot() Promise of {template, previewData}; rejects if unavailable
getSnapshot() SDK alias for requestSnapshot()

White-labelling and UI configuration

const editor = createEditor({
  apiUrl: 'https://pulp.example.com', token, tokenExpiresAt,
  brand: { productName: 'Acme Reports', primary: '#0f766e', radius: '4px',
    logoUrl: '/acme-logo.svg', fontFamily: 'Inter, sans-serif',
    dark: { primary: '#5eead4', primaryForeground: '#000000' } },
  hiddenFeatures: ['ai', 'schedules'], readOnly: true,
  lockedProperties: ['renderConfig.margin', 'style.fontFamily'],
  toolbarActions: [{ id: 'approve', label: 'Approve', icon: 'check' }],
})
editor.on('action', ({ id, templateKey, version, isDirty }) => {
  if (id === 'approve') approve(templateKey, version, isDirty)
})
editor.mount(document.querySelector('#designer')!)
Brand field CSS token / behavior
primary --primary, --ring; #RRGGBB converted to HSL triplets
primaryForeground --primary-foreground; below 4.5:1 contrast selects black/white and warns
accent --accent
radius --radius; 0-24px or 0-1.5rem
fontFamily --editor-ui-font; letters, digits, spaces, underscores, quotes, commas and hyphens; at most 200 characters
productName Logo alternative text, screen-reader text and iframe title; at most 80 characters
logoUrl Replaces the header and welcome logo
dark Optional primary, primaryForeground and accent overrides in dark mode

Tokens are injected into an unlayered :root rule, with :root.dark overrides. Invalid values warn and are ignored. Logos accept same-origin paths beginning with /, or base64 PNG/JPEG/WebP/SVG data URIs up to 64 KiB. Remote logos and remote fonts are blocked by the embed CSP. Serve custom fonts from the Pulp origin and provide their font-face stylesheet there.

Hidden feature keys Removed UI
ai AI generation and rewrite entry points/dialogs
schedules, labels, history Corresponding header, command-menu and dialog entries
export, duplicate, publish Export JSON, duplicate/save-as, or Publish controls and shortcuts
document-tab, schema-tab, mappings-tab Corresponding inspector tabs; active hidden tab falls back to Properties
custom-css, font-imports Document setting groups, including inherited-layout settings
expression-playground, theme-selector, command-menu Corresponding controls/dialogs; Ctrl+K is suppressed when the command menu is hidden

Unknown feature keys and property paths warn and are ignored. Attribute changes and setConfig() apply live. publish only hides the host-independent save controls; a host may still call save() when editing is enabled.

View-only mode blocks tracked, silent, bulk and history mutations, local-key renaming and saving. save() emits error with code: 'READ_ONLY'. It removes Blocks, disables the inspector, drag-and-drop and rich-text editing, and replaces Undo/Redo/Publish with a View only chip. Selection, Outline, preview and host-driven template loading remain available.

Locks accept renderConfig.<key> for any current RenderConfig field, style.<key>, inputSchema and fieldMappings. Locked controls show a lock with an administrator/brand tooltip. The store preserves locked values through single edits, silent edits, bulk styling, whole-definition edits and undo/redo. Node-specific paths such as image.src are outside v1.

At most five host actions are rendered. IDs match [a-z0-9-]{1,40}, labels are at most 32 characters, icons are send, check, download, share, mail, external-link or flag. Placement is header (default) or more-menu; variant is ghost (default) or primary. disabledWhenDirty prevents selection with unsaved changes. Actions emit an event; hosts implement the action themselves.

These are presentation-layer controls. Use API roles, template permissions and restricted credentials for server enforcement. A user with a write-capable editor token can call the API directly.

Node Constraints (allowed-nodes)

Control which block types your customers can add. Existing blocks of disallowed types remain visible and editable — they just cannot be duplicated or added.

<pulp-engine-editor
  allowed-nodes="heading,text,table,image,container,columns"
  ...
></pulp-engine-editor>

Always-allowed types

document, section, and column are structural necessities and are always permitted regardless of constraints.

Constrainable types

All other types can be restricted: container, columns, heading, text, image, table, chart, pivotTable, richText, spacer, divider, pageBreak, conditional, repeater, formField, signatureField, positioned, toc.

Custom plugin nodes

Plugin-registered custom types use the custom:<customType> format:

allowed-nodes="heading,text,custom:barcode,custom:qrCode"

Enforcement points

Constraints are enforced at four levels:

  1. Palette: disallowed types hidden from the block palette
  2. Inline add: disallowed types hidden from canvas “+ Add block” dropdowns
  3. Paste: disallowed nodes stripped from pasted trees (allowed root with disallowed children → children stripped; disallowed root → paste blocked)
  4. Store mutations: addNode, addNodeToBranch, duplicateNode reject disallowed types

TypeScript SDK (@pulp-engine/editor-embed)

For programmatic control with full type safety:

import { createEditor, loadPulpEngineEmbed } from '@pulp-engine/editor-embed'

// Load the custom element definition from your server
await loadPulpEngineEmbed('https://pulp-engine.internal')

// Create and mount the editor
const editor = createEditor({
  apiUrl: 'https://pulp-engine.internal',
  templateKey: 'invoice-v2',
  token: tokenFromBackend,
  tokenExpiresAt,
  allowedNodes: ['heading', 'text', 'table', 'image'],
})

editor.on('saved', ({ key, version }) => {
  showToast(`Saved ${key} v${version}`)
})

editor.on('dirty-change', ({ isDirty }) => {
  setUnsavedBanner(isDirty)
})

editor.mount(document.getElementById('editor-container')!)

// Later: cleanup
editor.unmount()

Framework Examples

React

import { useRef } from 'react'
import { PulpEngineEditor, PulpEngineForm, type PulpEngineEditorHandle } from '@pulp-engine/embed-react'

export function TemplateEditor({ templateKey, token, expiresAt }: { templateKey: string; token: string; expiresAt: string }) {
  const editor = useRef<PulpEngineEditorHandle>(null)
  return <PulpEngineEditor ref={editor} apiUrl="https://pulp-engine.internal"
    templateKey={templateKey} token={token} tokenExpiresAt={expiresAt}
    onSaved={({ key, version }) => console.log(key, version)}
    style={{ width: '100%', height: 700 }} />
}
export function InvoiceForm() {
  return <PulpEngineForm apiUrl="https://pulp-engine.internal" templateKey="invoice"
    onSuccess={({ filename }) => console.log(filename)} />
}

The entry has use client for Next.js. Server rendering does not touch the DOM. Callbacks always use the latest props. Token and template-key updates preserve the editor iframe; changes to other editor options remount it. Form option changes remount because its element has no imperative update methods.

Vue 3

Configure Vue’s template compiler to recognize the two elements:

const compilerOptions = {
  isCustomElement: tag => ['pulp-engine-editor', 'pulp-engine-form'].includes(tag),
}
// Pass compilerOptions to @vitejs/plugin-vue's template.compilerOptions,
// or app.config.compilerOptions when using the runtime compiler.

Load the server script in onMounted, then mount the vanilla createEditor or createForm instance in a container. Call unmount() in onUnmounted. If an async script load finishes after disposal, skip the mount.

<pulp-engine-editor api-url="https://pulp-engine.internal" template-key="invoice" />

Angular

Add CUSTOM_ELEMENTS_SCHEMA (imported from @angular/core) to the standalone component or NgModule that uses the element. Load the server script before showing it. For programmatic events and methods, use the vanilla wrapper in ngAfterViewInit and call its unmount() from ngOnDestroy.

import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
export const embedSchemas = [CUSTOM_ELEMENTS_SCHEMA]
<pulp-engine-editor api-url="https://pulp-engine.internal" template-key="invoice"></pulp-engine-editor>

Vanilla HTML

<script src="https://pulp-engine.internal/pulp-engine-embed.js"></script>

<pulp-engine-editor
  id="my-editor"
  api-url="https://pulp-engine.internal"
  template-key="invoice-v2"
  token="et_abc123"
  allowed-nodes="heading,text,table,image"
  style="width: 100%; height: 700px;"
></pulp-engine-editor>

<script>
  document.getElementById('my-editor').addEventListener('saved', (e) => {
    alert(`Saved version ${e.detail.version}`)
  })
</script>

Form Embed (<pulp-engine-form>)

<pulp-engine-form> reads a template’s inputSchema and renders a matching web form. End users fill in the fields, click submit, and receive a rendered PDF (or DOCX / PPTX / CSV / XLSX, depending on the template’s renderer). No template-authoring surface is exposed.

The element is iframe-isolated like the editor. All schema-to-form logic lives inside /form.html served from the Pulp Engine API; the host page only needs the wrapper script.

Quick Start

<script src="https://your-pulp-engine-server.com/pulp-engine-form.js"></script>

<pulp-engine-form
  api-url="https://your-pulp-engine-server.com"
  template-key="invoice-v2"
  api-key="your-render-key"
  submit-label="Generate invoice"
  style="width: 100%; height: 600px;"
></pulp-engine-form>

<script>
  const form = document.querySelector('pulp-engine-form')
  form.addEventListener('ready', (e) => {
    console.log('Form loaded. hasFieldMappings:', e.detail.hasFieldMappings)
  })
  form.addEventListener('success', (e) => {
    console.log(`Generated ${e.detail.filename} (${e.detail.size} bytes)`)
  })
  form.addEventListener('error', (e) => {
    console.error(`Form error ${e.detail.code}: ${e.detail.message}`)
  })
</script>

On mount, the form fetches GET /templates/:key/form-metadata, renders a React form from the returned inputSchema, and submits to POST /render/pdf when the user clicks the submit button. The resulting blob is either downloaded in-iframe (result-mode="download", default) or posted back to the host page as a Blob (result-mode="blob").

Authentication

For public end-user forms, mint a scoped render token on your backend with allowFormMetadata: true, the allowed template and PDF format, an expiry and a render budget. Pass only that token to the page:

<pulp-engine-form api-url="https://pulp.example.com" template-key="certificate"
  render-token="TOKEN_FROM_YOUR_BACKEND" result-mode="blob"></pulp-engine-form>

The SDK accepts renderToken. The iframe sends X-Render-Token for metadata and rendering, taking precedence over any API-key/editor-token attributes. The token carries its tenant; no separate tenant header is needed. Fixed data is merged by the server after browser input. See Browser render tokens for mint/revoke examples and deployment settings.

For same-origin form iframes, omit the optional allowedOrigins restriction: browsers normally omit Origin on same-origin metadata GETs, and an origin-bound token rejects requests without Origin. Cross-origin API calls must send an allowed Origin and need CORS configuration. Render tokens cannot call parameter-resolution endpoints; v1 public forms should use an inputSchema without dynamic parameters or field mappings. A backend proxy remains an option for those advanced flows.

Raw api-key is available for trusted internal pages only. Editor tokens cannot call production /render endpoints. Keep minting API keys on the backend.

Server Configuration

The form embed uses the same CSP, CORS, and embedding rules as the editor. If you have already configured EMBED_ALLOWED_ORIGINS and CORS_ALLOWED_ORIGINS for <pulp-engine-editor>, no additional server configuration is needed. See Server Configuration above.

Attributes

Attribute Required Description
api-url Yes Base URL of the Pulp Engine API server (or your proxy). Used for both metadata and render calls.
template-key Yes Template key to generate a form for.
render-token No* Scoped browser render token with allowFormMetadata: true; sent as X-Render-Token.
api-key No* Admin or render-scoped API key. Sent as X-Api-Key. Trusted internal pages only.
token No* Editor token. Sent as X-Editor-Token. Kept for forward compatibility; cannot call /render in v1.
embed-url No Override the origin that serves /form.html and /pulp-engine-form.js (for split-origin proxy deployments). Defaults to api-url.
version No Pin a specific template version. Mutually exclusive with label.
label No Resolve a named label (e.g. stable, prod). Mutually exclusive with version.
tenant-id No Multi-tenant slug, forwarded as X-PulpEngine-Tenant-Id.
result-mode No download (default) or blob. Controls how the generated output is delivered.
submit-label No Override the default “Generate PDF” button label.
theme No light (default) or dark.
locale No UI language (en, de, fr, ar) and selection of authored form-language variants. Updates live without losing answers.
hidden-fields No JSON object literal. Values merged into the submission and hidden from the UI. Example: hidden-fields='{"locale":"en-AU","channel":"web"}'

* Either api-key or token must be provided on authenticated servers.

Events

Standard DOM CustomEvents dispatched on the <pulp-engine-form> element:

Event detail fields Description
ready hasFieldMappings Metadata fetched and form rendered. hasFieldMappings: true means the template refused to render — see v1 Limitation.
change — User edited a field. Fires on every form input.
submit — User clicked submit; render request dispatched.
success contentType, filename, size, blob? Render completed. blob is populated only when result-mode="blob".
error code, message, status? Metadata fetch, validation, or render failed.

Error codes

Code Meaning
unsupported_mapped_template Template has fieldMappings; form embed refuses to render it (v1).
unsupported_schema_root Template inputSchema is not an object with properties.
metadata_fetch_failed GET /templates/:key/form-metadata returned non-2xx. Check status for HTTP status and message for the server detail.
network_error Fetch threw before a response arrived (offline, DNS, CORS preflight).
Server render codes On render failure, the server’s own code is forwarded verbatim — e.g. validation_failed, asset_blocked, template_expression_error. See Render execution error codes.

Result modes

  • download (default) — the iframe creates an in-iframe <a download> and clicks it, kicking off a browser download directly from the iframe’s origin. The host page receives a lightweight success event with contentType, filename, and size. No Blob is transferred.
  • blob — the iframe posts the full Blob back to the host via structured clone. The host owns the URL lifecycle (typically URL.createObjectURL + URL.revokeObjectURL). Use this when the host needs to inline the PDF into its own UI (e.g. a viewer, an attachment flow) rather than dropping it into the user’s Downloads folder.

v1 Limitation: field mappings

Templates with fieldMappings are refused by the form embed in v1. The ready event fires with hasFieldMappings: true and an error event with code: 'unsupported_mapped_template' is also dispatched; the form shows an inline message and no submit button.

Why: a template’s inputSchema describes the post-mapping data shape (what the template body’s {{…}} references). fieldMappings is a one-way transform from raw submission → post-mapping shape. The v1 form only knows the post-mapping schema, so it cannot honestly generate a form matching what the template expects end users to submit.

Detecting at build time: call GET /templates/:key/form-metadata before you render the embed and check hasFieldMappings. If true, either (a) author a companion template without mappings that produces the same PDF, or (b) build a custom form on your side that submits directly to /render/pdf.

Supported JSON Schema subset

The v1 form renders templates whose inputSchema uses these keywords. Anything outside this subset is either ignored or rendered as a generic free-text field.

  • Types: string, number, integer, boolean, object, array
  • Common: required, enum, default, title, description
  • String: minLength, maxLength, pattern, format (email, uri, date, date-time)
  • Number: minimum, maximum, multipleOf
  • Array: items (primitive item schemas), minItems, maxItems
  • Object: nested properties, required[]

Out of scope in v1: oneOf / anyOf / allOf, if / then / else, $ref, patternProperties, dependencies, tuple items. Client-side validation is best-effort; the server re-validates the submission on /render/pdf and surfaces violations via the error event.

TypeScript SDK (@pulp-engine/form-embed)

import { createForm, loadFormScript } from '@pulp-engine/form-embed'

await loadFormScript({ embedUrl: 'https://pulp-engine.internal' })

const form = createForm({
  apiUrl: 'https://pulp-engine.internal',
  templateKey: 'invoice-v2',
  apiKey: adminKey,            // trusted internal pages only
  resultMode: 'blob',
  submitLabel: 'Generate invoice',
  hiddenFields: { channel: 'web' },
})

form.on('ready', ({ hasFieldMappings }) => {
  if (hasFieldMappings) {
    alert('This template is not supported by the form embed.')
  }
})

form.on('success', ({ blob, filename }) => {
  const url = URL.createObjectURL(blob!)
  window.open(url, '_blank')
  setTimeout(() => URL.revokeObjectURL(url), 60_000)
})

form.on('error', ({ code, message, status }) => {
  console.error(`[${code}] ${message}`, status)
})

form.mount(document.getElementById('form-container')!)

loadFormScript is idempotent — multiple calls resolve immediately once the element is registered.

Framework Examples

React

import { useEffect, useRef } from 'react'
import { createForm, loadFormScript } from '@pulp-engine/form-embed'

function InvoiceForm({ templateKey, apiKey }: { templateKey: string; apiKey: string }) {
  const containerRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    let form: ReturnType<typeof createForm> | null = null

    loadFormScript({ embedUrl: 'https://pulp-engine.internal' }).then(() => {
      form = createForm({
        apiUrl: 'https://pulp-engine.internal',
        templateKey,
        apiKey,
        resultMode: 'download',
      })
      form.mount(containerRef.current!)
    })

    return () => { form?.unmount() }
  }, [templateKey, apiKey])

  return <div ref={containerRef} style={{ width: '100%', height: '600px' }} />
}

Vue 3

<template>
  <div ref="container" style="width: 100%; height: 600px" />
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { createForm, loadFormScript } from '@pulp-engine/form-embed'

const props = defineProps<{ templateKey: string; apiKey: string }>()
const container = ref<HTMLElement>()
let form: ReturnType<typeof createForm> | null = null

onMounted(async () => {
  await loadFormScript({ embedUrl: 'https://pulp-engine.internal' })
  form = createForm({
    apiUrl: 'https://pulp-engine.internal',
    templateKey: props.templateKey,
    apiKey: props.apiKey,
    resultMode: 'download',
  })
  form.mount(container.value!)
})

onUnmounted(() => form?.unmount())
</script>

Vanilla HTML

<script src="https://pulp-engine.internal/pulp-engine-form.js"></script>

<pulp-engine-form
  id="my-form"
  api-url="https://pulp-engine.internal"
  template-key="invoice-v2"
  api-key="your-render-key"
  result-mode="download"
  style="width: 100%; height: 600px;"
></pulp-engine-form>

<script>
  document.getElementById('my-form').addEventListener('success', (e) => {
    console.log('Downloaded', e.detail.filename)
  })
</script>

The visual editor can author these constraints, primitive array items and x-ui: textarea, infer from preview data, import JSON Schema and edit custom schema shapes as JSON. See Input schema.

Inline templates, preview data and snapshots

apiAuth: 'sandbox-token' selects the hosted sandbox credential transport; normal integrations use the default editor-token transport. createEditor exposes setInlineTemplate(template), setPreviewData(data) and requestSnapshot(): Promise<{ template, previewData }>. Subscribe to snapshot and snapshot-error for observation. Snapshot requests also reject on timeout or if the iframe is disconnected. Mint tokens on the backend and supply their tokenExpiresAt value alongside token.

Calculated mappings retain the existing hasFieldMappings signal. The form embed continues to reject templates containing field mappings, including additive computed mappings; use the editor embed to author them. GET /capabilities now advertises expressions.functions for compatibility checks.

Report options (Wave 37)

<pulp-engine-form> renders a Report options fieldset from safe form-metadata parameter descriptors, resolving defaults and cascading options with POST /templates/:key/parameters/resolve using the same credential/tenant headers and version/label pin as metadata. Render submits { template, data, parameters }. The ready message adds optional hasParameters for compatibility with existing hosts. A mapped template with declared parameters gets a parameters-only form; its data contains the configured hidden fields. Mapped templates without parameters retain the unsupported-template response. Hidden parameters are not prompted; internal parameters are never exposed. Choice labels, multi-select checkboxes, select-all and date/range presets share the editor’s form controls.

The production-bundle embed checks run with pnpm --dir apps/editor test:e2e:embed after the usual API/editor builds. They start an isolated API and Chromium, covering white-label view-only behavior and render-token forms.

Guided interviews

<pulp-engine-form format="pdf|docx|xlsx|csv|pptx|html"> routes to the matching render endpoint and filename. Server capabilities and template support apply. Existing flat v1 schemas and "x-ui":"textarea" remain supported.

Root "x-ui-steps":[{"id":"applicant","title":"Applicant"},{"id":"details","title":"Details"},{"id":"confirm","title":"Confirm"}] defines steps. Property "x-ui":{"step":"details"} assigns a step; others use the first. Next validates visible current-step fields; a final Review page precedes submission. preview="true" adds a PDF preview on review.

Conditions support draft-07 if/then/else with const/enum predicates, array-form dependencies, and x-ui.visibleWhen. A visibility leaf is {"field":"business","op":"truthy"}. Operators: eq, neq, in, gt, lt, truthy, falsy, with nested all/any groups and root answer paths. Hidden fields are pruned before submission. Browser and server waive missing hidden-field requirements, while supplied values retain server type/value validation. Visibility is presentation/validation metadata, not authorization. Advanced references, combinators and tuple items still need custom forms.

Uploads and signatures

Set "x-ui":{"widget":"image"} or "file" on a string. PNG/JPEG/WebP magic bytes are checked and re-encoded; SVG is rejected. Defaults are 262144 bytes and 1600 px; maxBytes/maxDimension configure limits. Aggregate payloads above 900 KiB fail inline.

"x-ui":{"widget":"signature"} offers pointer drawing, Clear and a keyboard typed-name alternative, producing a PNG data URI. Bind through an image node’s src: "{{signature}}". Captured marks are not cryptographic signatures. readonly displays a preset/default and preserves it in submission.

Drafts

draft="local" saves {step,data} under pe-form:{templateKey}:{version}:{tenantId} and restores it on reload, tolerating storage failures. draft="host" emits debounced draft events; restore with initial-data/initial-step or wrapper options initialData/initialStep. Files/signatures are omitted unless draft-include-files="true". Host drafts work where iframe storage is unavailable. Protect saved answers according to their sensitivity.

Options and localisation

Lookups use "x-ui":{"widget":"lookup","lookup":{"source":"host:accounts","valuePath":"id","labelPath":"name"}}. Call element.setOptions('accounts', [{id:'AC-1',name:'Account One'}]); options replay on ready. Only the configured parent window and origin can supply options. URL sources require exact origins in server-provided EMBED_CONNECT_ORIGINS (same-origin also allowed), omit credentials, reject redirects, time out after 15 seconds and cap responses at 1 MiB/1000 items.

locale="fr" selects French UI messages and matching authored field text from x-ui.i18n.fr. Schema titles, help, placeholders and enum labels remain authored data; the UI catalogue does not translate them automatically. Object-form x-ui supports these authored variants.

messages='{"next":"Suivant","required":"est obligatoire"}' overrides the active UI catalogue with host wording. An explicit submit-label takes precedence for the submit button. Public wrapper options mirror these attributes. Mapped templates use typed parameters when available; otherwise provide a custom form.

UI language

English is the fallback. German, French and Arabic ship as Beta pending native review. Non-English catalogues load on demand. Language selection resolves the host’s locale first, then the saved editor preference, then navigator.languages, then English. Unsupported host languages use English. The standalone editor offers a Language menu next to the theme selector.

const editor = createEditor({
  apiUrl, templateKey: 'invoice', token,
  locale: 'fr', localeSelectable: true,
})
editor.setLocale('ar')
editor.setLocale(null) // Remove the host override.

const form = createForm({apiUrl, templateKey: 'invoice', renderToken, locale: 'de'})
form.setLocale('fr')

Both custom elements observe their locale attribute. Changing or removing it preserves the iframe, template edits and form answers. React wrappers accept live locale props; the editor wrapper also accepts localeSelectable. The form SDK’s setLocale(locale) accepts null to remove the override.

Raw integrations may include locale?: string in either init message and localeSelectable?: boolean in the editor init message. Runtime messages are {type:'pulp-engine:set-locale', locale:'ar', localeSelectable:false} for the editor and {type:'pulp-engine-form:set-locale', locale:'de'} for the form. A null locale clears the host override. Locale values must be strings of at most 64 characters or null for a runtime reset. Existing origin and source-window checks apply.

The iframe document gets the active lang and dir; Arabic mirrors chrome and Radix menus. Authored document direction and renderConfig.locale remain independent, and preview frames keep their document coordinates. Host-page lang and dir are not modified. UI-language changes do not reload form metadata or translate template content and plugin labels. Server error details remain in their original language; classified UI errors and local validation are localised.