# Formfeed documentation > Formfeed renders PDFs and images from HTML templates written in Jinja2, Liquid or Handlebars and > JSON data, and fills Word and PowerPoint files that carry the same tags. Hosted in the EU > (Frankfurt), priced in EUR. It has a browser editor, a CLI for templates as files, SDKs for > TypeScript and Python, and an MCP server for AI agents. The complete documentation as one file, for reading in a single request: https://docs.formfeed.dev/llms-full.txt The API reference is left out of both files: it is generated from the OpenAPI document, and that document is the better machine-readable source: https://api-eu.formfeed.dev/v1/openapi.json ## Contents - Formfeed documentation: https://docs.formfeed.dev/ - Quickstart: https://docs.formfeed.dev/getting-started/quickstart - Templates as code: https://docs.formfeed.dev/getting-started/templates-as-code - Core concepts: https://docs.formfeed.dev/getting-started/concepts - Editor tour: https://docs.formfeed.dev/getting-started/editor-tour - Two-factor authentication: https://docs.formfeed.dev/getting-started/two-factor-authentication - Template languages: https://docs.formfeed.dev/templates/languages - Writing templates: https://docs.formfeed.dev/templates/writing-templates - Word and PowerPoint templates: https://docs.formfeed.dev/templates/office-templates - Visual editor: https://docs.formfeed.dev/templates/visual-editor - E-invoices (ZUGFeRD, Factur-X): https://docs.formfeed.dev/templates/e-invoices - E-invoice coverage: https://docs.formfeed.dev/templates/e-invoice-coverage - E-invoices by country: https://docs.formfeed.dev/templates/e-invoice-countries - Accessible PDFs (PDF/UA-1): https://docs.formfeed.dev/templates/accessible-pdf - Settings reference: https://docs.formfeed.dev/templates/settings - Building blocks: https://docs.formfeed.dev/templates/building-blocks - Brand kit: https://docs.formfeed.dev/templates/brand-kit - Shared partials: https://docs.formfeed.dev/templates/shared-partials - Typed template data: https://docs.formfeed.dev/templates/typed-data - Release channels: https://docs.formfeed.dev/templates/release-channels - Reproduce a failed render: https://docs.formfeed.dev/templates/reproduce-a-failed-render - Helper reference: https://docs.formfeed.dev/templates/helpers - Batch from a spreadsheet: https://docs.formfeed.dev/templates/batch-from-spreadsheet - API overview: https://docs.formfeed.dev/api/overview - TypeScript SDK: https://docs.formfeed.dev/api/sdks/typescript - Authentication: https://docs.formfeed.dev/api/authentication - Python SDK: https://docs.formfeed.dev/api/sdks/python - Rendering: https://docs.formfeed.dev/api/rendering - Async and batch: https://docs.formfeed.dev/api/async-and-batch - apitemplate.io compatibility endpoints: https://docs.formfeed.dev/api/compat-apitemplate - Data residency: https://docs.formfeed.dev/api/data-residency - PDF tools: https://docs.formfeed.dev/api/pdf-tools - Webhooks: https://docs.formfeed.dev/api/webhooks - Develop webhooks locally: https://docs.formfeed.dev/api/develop-webhooks-locally - Templates API: https://docs.formfeed.dev/api/templates - Limits and usage: https://docs.formfeed.dev/api/limits-and-usage - Errors: https://docs.formfeed.dev/api/errors - Brand and partials: https://docs.formfeed.dev/api/brand-and-partials - Bring your own storage: https://docs.formfeed.dev/api/bring-your-own-storage - Files: https://docs.formfeed.dev/api/files - invalid_request: https://docs.formfeed.dev/errors/invalid-request - unauthorized: https://docs.formfeed.dev/errors/unauthorized - forbidden: https://docs.formfeed.dev/errors/forbidden - not_found: https://docs.formfeed.dev/errors/not-found - template_not_found: https://docs.formfeed.dev/errors/template-not-found - conflict: https://docs.formfeed.dev/errors/conflict - quota_exceeded: https://docs.formfeed.dev/errors/quota-exceeded - template_syntax_error: https://docs.formfeed.dev/errors/template-syntax - template_runtime_error: https://docs.formfeed.dev/errors/template-runtime - data_validation_error: https://docs.formfeed.dev/errors/data-validation - navigation_failed: https://docs.formfeed.dev/errors/navigation-failed - page_limit_exceeded: https://docs.formfeed.dev/errors/page-limit - output_too_large: https://docs.formfeed.dev/errors/output-too-large - rate_limited: https://docs.formfeed.dev/errors/rate-limited - worker_exhausted: https://docs.formfeed.dev/errors/worker-exhausted - region_unavailable: https://docs.formfeed.dev/errors/region-unavailable - render_timeout: https://docs.formfeed.dev/errors/render-timeout - not_implemented: https://docs.formfeed.dev/errors/not-implemented - source_not_found: https://docs.formfeed.dev/errors/source-not-found - source_invalid: https://docs.formfeed.dev/errors/source-invalid - source_encrypted: https://docs.formfeed.dev/errors/source-encrypted - tool_failed: https://docs.formfeed.dev/errors/tool-failed - file_too_large: https://docs.formfeed.dev/errors/file-too-large - schema_breaking_change: https://docs.formfeed.dev/errors/schema-breaking-change - channel_not_found: https://docs.formfeed.dev/errors/channel-not-found - channel_name_invalid: https://docs.formfeed.dev/errors/channel-name-invalid - channel_in_use: https://docs.formfeed.dev/errors/channel-in-use - render_input_expired: https://docs.formfeed.dev/errors/render-input-expired - listen_session_limit: https://docs.formfeed.dev/errors/listen-session-limit - listen_session_ended: https://docs.formfeed.dev/errors/listen-session-ended - last_workspace: https://docs.formfeed.dev/errors/last-workspace - file_type_unsupported: https://docs.formfeed.dev/errors/file-type-unsupported - office_document_invalid: https://docs.formfeed.dev/errors/office-document-invalid - office_document_too_large: https://docs.formfeed.dev/errors/office-document-too-large - office_conversion_failed: https://docs.formfeed.dev/errors/office-conversion-failed - office_unavailable: https://docs.formfeed.dev/errors/office-unavailable - test_output_unavailable: https://docs.formfeed.dev/errors/test-output-unavailable - unsupported_in_office: https://docs.formfeed.dev/errors/unsupported-in-office - storage_not_configured: https://docs.formfeed.dev/errors/storage-not-configured - storage_test_failed: https://docs.formfeed.dev/errors/storage-test-failed - einvoice_data_invalid: https://docs.formfeed.dev/errors/einvoice-data-invalid - einvoice_profile_unsupported: https://docs.formfeed.dev/errors/einvoice-profile-unsupported - einvoice_display_mismatch: https://docs.formfeed.dev/errors/einvoice-display-mismatch - pdfa_conversion_failed: https://docs.formfeed.dev/errors/pdfa-conversion-failed - conformance_unavailable: https://docs.formfeed.dev/errors/conformance-unavailable - pdfua_validation_failed: https://docs.formfeed.dev/errors/pdfua-validation-failed - CLI overview: https://docs.formfeed.dev/cli - Install and log in: https://docs.formfeed.dev/cli/installation - Configuration: https://docs.formfeed.dev/cli/configuration - Commands: https://docs.formfeed.dev/cli/commands - CI usage: https://docs.formfeed.dev/cli/ci - Exit codes: https://docs.formfeed.dev/cli/exit-codes - Development kit overview: https://docs.formfeed.dev/devkit/overview - Project layout: https://docs.formfeed.dev/devkit/project-layout - Dev server: https://docs.formfeed.dev/devkit/dev-server - Testing templates: https://docs.formfeed.dev/devkit/testing - Engine package: https://docs.formfeed.dev/devkit/engine-package - Coding agents: https://docs.formfeed.dev/devkit/coding-agents - Integrations: https://docs.formfeed.dev/integrations/overview - MCP server: https://docs.formfeed.dev/integrations/mcp - Zapier: https://docs.formfeed.dev/integrations/zapier - n8n: https://docs.formfeed.dev/integrations/n8n - Make: https://docs.formfeed.dev/integrations/make - Airtable: https://docs.formfeed.dev/integrations/airtable - Stripe: https://docs.formfeed.dev/integrations/stripe - WooCommerce: https://docs.formfeed.dev/integrations/woocommerce - GitHub Actions: https://docs.formfeed.dev/integrations/github-actions - Power Automate: https://docs.formfeed.dev/integrations/power-automate - Pipedream: https://docs.formfeed.dev/integrations/pipedream - Activepieces: https://docs.formfeed.dev/integrations/activepieces - Migrate from apitemplate.io: https://docs.formfeed.dev/migrate/apitemplate-io - Migrate from PDFMonkey: https://docs.formfeed.dev/migrate/pdfmonkey - Migrate from jsreport: https://docs.formfeed.dev/migrate/jsreport - Changelog: https://docs.formfeed.dev/changelog ================================================================================ # Formfeed documentation Source: https://docs.formfeed.dev/ Summary: Formfeed turns an HTML template and a JSON payload into a PDF or an image. One REST call, three template languages, EU hosting. Formfeed turns an HTML template and a JSON payload into a PDF or an image. You write the template once, in HTML and CSS with Jinja2, Liquid or Handlebars. Your code sends only the data and gets back a signed download URL. ```bash curl https://api-eu.formfeed.dev/v1/renders \ -H "Authorization: Bearer $FORMFEED_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "html": "

Hello {{ name }}

", "engine": "liquid", "data": { "name": "Jane" } }' ``` ```json { "id": "rnd_7f3c9a1e2b4d4c8f9e0a1b2c3d4e5f60", "status": "succeeded", "download_url": "https://cdn.formfeed.dev/o/eu/…?exp=…&sig=…", "page_count": 1, "units": 1 } ``` The answer is shortened here; [Quickstart](getting-started/quickstart) shows a complete one. `units` is what the render costs: 1 with a live key (`ff_live_…`), 0 with a test key (`ff_test_…`). That is the whole integration. Everything else in these docs is about making the template good and the integration robust. ## Start here | I want to… | Read | | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | Render my first PDF in five minutes | [Quickstart](getting-started/quickstart) | | Keep templates in git, preview locally, test in CI | [Templates as code](getting-started/templates-as-code) | | Understand workspaces, versions, keys and units | [Core concepts](getting-started/concepts) | | Write a good template: pages, headers, fonts, images | [Writing templates](templates/writing-templates) | | Call the API from my backend | [API overview](api/overview) · [Rendering](api/rendering) · [TypeScript SDK](api/sdks/typescript) · [Python SDK](api/sdks/python) | | Render thousands of documents | [Async and batch](api/async-and-batch) · [Webhooks](api/webhooks) | | Look up an endpoint or a field | [API reference](api/reference/formfeed) | | Understand an error I got | [Errors](api/errors) | | Let an AI agent render documents | [MCP server](integrations/mcp) | | Move from apitemplate.io, PDFMonkey or jsreport | [Migrate](migrate/apitemplate-io) | ## How it works ```mermaid flowchart LR subgraph You T[Template
HTML + CSS + Jinja2 / Liquid / Handlebars] D[Your data
JSON] end T -->|publish| F[Formfeed API] D -->|POST /v1/renders| F F --> C[Chromium
render worker] C --> S[(Output storage
+ CDN)] S -->|signed download_url| R[PDF · PNG · JPG] F -.->|webhook, when async| W[Your endpoint] ``` 1. **Write a template.** HTML and CSS with a template language for the variable parts, in the [web editor](getting-started/editor-tour) or as [files in your repository](getting-started/templates-as-code). Paper size, margins, headers and footers live in the template's [settings](templates/settings). 2. **Publish it.** By default the API renders the published version, so editing a draft never changes what your production code gets. 3. **Send data.** `POST /v1/renders` with the template slug and a JSON object. A render answers synchronously by default, with the finished file. Set `"mode": "async"` to get `202` at once and a [webhook](api/webhooks) when it is done; a synchronous render that takes longer than 60 seconds turns into an asynchronous one by itself. Batches of up to 1,000 documents are always [asynchronous](api/async-and-batch). 4. **Download the result.** The response carries a signed `download_url`, the page count, the [units](getting-started/concepts#units) charged and the template and engine versions that produced it. ## What makes it pleasant to work with - **One engine everywhere.** The editor preview, the CLI's local preview and the production render worker all run the same template engine package, so your local preview and CI checks agree with production. - **Three template languages, one set of helpers.** [Jinja2, Liquid and Handlebars](templates/languages) share [50+ helpers](templates/helpers) for money, dates, QR codes, barcodes, SEPA payment codes, charts and more. - **Errors that point at the line.** A template error comes back as an [RFC 9457 problem](api/errors) with the line and column. Every error `type` links to a page that explains the fix. - **Safe retries.** Once a request with an [`Idempotency-Key`](api/overview#idempotency) has been answered, a repeat with the same key within 24 hours gets that stored answer back (an error below 500 too) instead of rendering and charging again; the SDKs send a key with every render. The key does not hold back a request that arrives while the first one is still running: both render. A slow synchronous render falls back to asynchronous instead of timing out, and [deduplication](api/rendering#deduplication) answers an identical live render of a template or HTML within 24 hours with the first file, at 0 units. - **Test keys are free.** Build and test with `ff_test_` keys: their renders are not metered, do not count against your quota, and their files are kept for 24 hours. See [API keys and environments](getting-started/concepts#environments). - **EU hosted.** Rendering and the database run in Frankfurt, and rendered files are stored in the EU. See [Data residency](api/data-residency). ================================================================================ # Quickstart Source: https://docs.formfeed.dev/getting-started/quickstart Summary: Render your first PDF with curl, TypeScript or Python in five minutes, then turn it into a reusable template. By the end of this page you will have rendered a PDF from raw HTML, saved that HTML as a template, and rendered the template with your own data. You need a Formfeed account and a terminal. ## 1. Get a test API key 1. Sign in at [app.formfeed.dev](https://app.formfeed.dev); a new address creates the account, with an organisation you own and a first workspace. The Free plan needs no card. 2. Open **API keys → Create key**, choose the **Test** environment and keep the default scopes. Only owners and admins can create keys; as the account's creator you are the owner. 3. Copy the key. It starts with `ff_test_` and is shown only once. ```bash export FORMFEED_API_KEY=ff_test_... ``` Test keys cost nothing: renders are not metered and do not count against your quota, outputs expire after 24 hours, and on the Free plan they carry a small test mark. Use them until you go to production. See [Authentication](../api/authentication) and [API keys and environments](concepts#environments) for the difference between test and live keys. ## 2. Render some HTML No template needed yet: send HTML and a template language, and Formfeed fills in the data. curl: ```bash curl https://api-eu.formfeed.dev/v1/renders \ -H "Authorization: Bearer $FORMFEED_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "html": "

Invoice {{ number }}

Total: {{ total | money: \"EUR\" }}

", "engine": "liquid", "data": { "number": "2026-0042", "total": 960 } }' ``` TypeScript: ```bash npm install @formfeed/sdk ``` ```ts const client = new Formfeed({ apiKey: process.env.FORMFEED_API_KEY! }); const render = await client.renders.create({ html: '

Invoice {{ number }}

Total: {{ total | money: "EUR" }}

', engine: 'liquid', data: { number: '2026-0042', total: 960 }, }); console.log(render.download_url); await writeFile('invoice.pdf', await client.renders.download(render)); ``` Python: ```bash pip install formfeed ``` ```python import os from formfeed import Formfeed client = Formfeed(os.environ["FORMFEED_API_KEY"]) render = client.renders.create( html='

Invoice {{ number }}

Total: {{ total | money: "EUR" }}

', engine="liquid", data={"number": "2026-0042", "total": 960}, ) print(render.download_url) with open("invoice.pdf", "wb") as f: f.write(client.renders.download(render)) ``` The answer is a render object (shortened; every field is described in [The render object](../api/rendering#the-render-object)): ```json { "id": "rnd_7f3c9a1e2b4d4c8f9e0a1b2c3d4e5f60", "status": "succeeded", "output": "pdf", "download_url": "https://cdn.formfeed.dev/o/eu/…?exp=1789293600&sig=…", "expires_at": "2026-09-13T10:00:00.000Z", "bytes": 18734, "page_count": 1, "units": 0, "region": "eu", "environment": "test", "deduplicated": false, "timings": { "queue_ms": 0, "compile_ms": 4, "render_ms": 612, "post_ms": 0, "upload_ms": 38, "total_ms": 702 }, "error": null, "created_at": "2026-09-12T10:00:00.000Z", "completed_at": "2026-09-12T10:00:00.702Z" } ``` Open `download_url` in a browser, which saves the file (it is served as a download), or fetch it with `curl -o invoice.pdf ""`. The link is signed and valid for up to seven days, never longer than the output itself is kept: with a test key that is 24 hours, which `expires_at` shows. [`GET /v1/renders/{id}`](../api/reference/get-render) hands out a fresh link while the file exists. `units` is `0` because a test key renders for free; the same render with a live key costs 1 unit. If something is wrong with the request, the answer is a [problem](../api/errors) instead, for example [`401 unauthorized`](../errors/unauthorized) when the key is missing or mistyped, or [`422 template_syntax_error`](../errors/template-syntax) with the line and column when a template tag such as `{{ number` is not closed. ## 3. Save it as a template Sending HTML with every request works, but a stored template gives you versioning, a live preview, autocomplete from your data and a data schema that integrations can read. Or start from a finished template: **Templates → Examples** holds the templates of [formfeed.dev/examples](https://formfeed.dev/examples): invoices, certificates, reports, labels and social images, each with its sample data. Pick one and **Use this template** puts a copy into the workspace, already published. Its **API** panel then has the request of step 4 for that copy, with its slug and its sample data. 1. In the app, open **Templates → New template**. Name it `Invoice`; the slug becomes `invoice`. Keep kind **PDF**, choose **Liquid** as the engine (the code below is Liquid) and click **Create template**. 2. The editor opens. On the rail at the left, **Code** is selected; replace the content of its **Template** tab with: ```html

Invoice {{ number }}

{% for line in lines %} {% endfor %}
{{ line.description }} {{ line.qty }} × {{ line.price | money: "EUR" }}

Total: {{ lines | sum: "total" | money: "EUR" }}

``` (This is Liquid. The [template languages](../templates/languages) page shows the same thing in Jinja2 and Handlebars. Liquid prints values without escaping HTML; where a value comes from your users, write `{{ line.description | escape }}`.) 3. Open **Data and schema** on the rail and paste this sample data. The preview and autocomplete use it; an API render never does, it always uses the data of the request: ```json { "number": "2026-0042", "lines": [ { "description": "Consulting", "qty": 8, "price": 120, "total": 960 } ] } ``` 4. Press `Ctrl+S` (`⌘S`), add a note if you like and confirm with **Save version**, then click **Publish**. By default the API renders the published version. ## 4. Render the template {#render-the-template} Now the request carries only the slug and the data: curl: ```bash curl https://api-eu.formfeed.dev/v1/renders \ -H "Authorization: Bearer $FORMFEED_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "template": "invoice", "data": { "number": "2026-0043", "lines": [{ "description": "Workshop", "qty": 1, "price": 1500, "total": 1500 }] }, "filename": "invoice-2026-0043.pdf" }' ``` TypeScript: ```ts const data = { number: '2026-0043', lines: [{ description: 'Workshop', qty: 1, price: 1500, total: 1500 }], }; const render = await client.renders.create({ template: 'invoice', data, filename: `invoice-${data.number}.pdf`, }); await writeFile( `invoice-${data.number}.pdf`, await client.renders.download(render), ); ``` Python: ```python data = {"number": "2026-0043", "lines": [{"description": "Workshop", "qty": 1, "price": 1500, "total": 1500}]} render = client.renders.create( template="invoice", data=data, filename=f"invoice-{data['number']}.pdf", ) with open(f"invoice-{data['number']}.pdf", "wb") as f: f.write(client.renders.download(render)) ``` `filename` is the name the file is stored and downloaded under. It is taken as it is, not filled with the data, so build it in your code; characters other than letters, digits, `.`, `-`, `_`, `(`, `)` and spaces become `_`, and the extension of the output is added when it is missing. If the template fails, the API answers `422` with the [problem](../api/errors) and the line it happened on, instead of a broken PDF: [`template_syntax_error`](../errors/template-syntax) for a tag that does not parse, [`template_runtime_error`](../errors/template-runtime) for one that fails with your data. A value missing from your data does not fail the render; it prints as empty. To have the data checked first, give the template a data schema and send `"validate_data": true`, which answers [`422 data_validation_error`](../errors/data-validation) listing every mismatch before anything is rendered or counted. The same request, for your template, in the editor: Open the template and click **API** on the editor's rail. The panel shows this request with the template's own slug in eight languages, says which version a call renders, and **Send test request** runs it and shows the answer. See the [editor tour](editor-tour#api-tab). ## Next steps - **Make the template production-ready:** paper size, margins, headers and footers with page numbers, fonts and images are in [Writing templates](../templates/writing-templates). - **Harden the integration:** add an `Idempotency-Key` to every render, handle `429` with `Retry-After`, and move long documents to [async renders with webhooks](../api/async-and-batch). The SDKs do the first two for you. - **Keep templates in git:** after `npx formfeed login`, `npx formfeed init my-templates --from-workspace` pulls your published templates into a folder with a local hot-reload preview. See [Templates as code](templates-as-code). - **Go live:** create a key in the **Live** environment and swap it in. Nothing else changes, except that renders now cost [units](concepts#units) and their files are kept for your plan's retention instead of 24 hours. - **Take a complete program:** the [language guides](https://formfeed.dev/guides) have one per language that reads its data from disk, handles the error answer and saves the PDF, for Node.js, Python, PHP, Go, C#, Java, Ruby and curl. ================================================================================ # Templates as code Source: https://docs.formfeed.dev/getting-started/templates-as-code Summary: Keep templates in your repository, preview them locally with hot reload, test them, and publish from CI. If you prefer your editor and git over a web UI, the `formfeed` CLI keeps every template as a folder of plain files. You get a local preview with hot reload, offline validation with the production engine, snapshot tests, and a publish step for CI. The web editor and the files stay interchangeable: what you push shows up in the editor, and what someone edits there you can pull. You need Node.js 22 or later. ## 1. Log in ```bash npx formfeed login ``` The CLI prints a short code and opens `app.formfeed.dev/device`. Approve it, pick a workspace, and the CLI stores an API key named "Formfeed CLI on" and your machine's name. That key is a **live** key with the scopes of your role in the organisation, so renders with it are metered. On a server or over SSH, add `--no-browser` and open the printed URL yourself. To work with a free [test key](concepts#environments) instead, create one on the **API keys** page and store it with `npx formfeed login ff_test_…`. In CI, skip the login and set `FORMFEED_API_KEY`. When several are present, `--api-key` wins over `FORMFEED_API_KEY`, which wins over the stored key. ```bash npx formfeed whoami # workspace, plan and usage of the key in use ``` ## 2. Create a project Start from your workspace's published templates, together with the organisation's [shared partials](../templates/shared-partials) they may include: ```bash npx formfeed init my-templates --from-workspace cd my-templates ``` Or start empty. Without `--from-workspace` you get a `hello` starter template. `--engine` sets the template language of new templates (`jinja2`, the default, `liquid` or `handlebars`): ```bash npx formfeed init my-templates --engine liquid ``` Either way you get this layout (the full reference is in [Project layout](../devkit/project-layout)): ```text my-templates/ formfeed.json project settings templates/ invoice/ template.json name, kind, engine template.html the body style.css header.html optional running header footer.html optional running footer settings.json paper, margins, locale… data/default.json sample data (one file per data set) offer/ template.json kind docx template.docx a Word template: the document instead of the HTML files settings.json data/default.json partials/ shared fragments ``` A [Word or PowerPoint template](../templates/office-templates) is a folder with `template.docx` or `template.pptx`, edited in Word or PowerPoint. ## 3. Preview with hot reload ```bash npx formfeed dev invoice # ➜ preview on http://localhost:4400 ``` `--port` picks another port, `--data` the data set the preview starts with, and `--open` opens the browser. Edit `template.html`, `style.css` or a data file, and the browser shows the new pages as soon as you save, with headers, footers and page numbers. Diagnostics (unknown variables, unclosed tags, unknown filters) appear with their line numbers. The preview runs offline; nothing is sent to Formfeed until you ask for a true render. For a Word or PowerPoint template, the page lists the findings of its tags by part and paragraph, and the true render shows the converted PDF. See [Dev server](../devkit/dev-server). ## 4. Validate and test ```bash npx formfeed validate # compile every template and check it against its default data set; exit 1 on errors npx formfeed test # render every data set and compare the HTML (XML for Word and PowerPoint) with approved snapshots npx formfeed test -u # approve the current output as the new snapshot ``` `validate` checks against the `default` data set unless `--data ` names another; `test --data ` runs one data set only. Both take a slug to check one template, and both run offline with the same engine as production. Put `validate` in a pre-commit hook. For assertions in your own test suite (Vitest or Jest), see [Testing templates](../devkit/testing). ## 5. Render a real PDF ```bash npx formfeed render invoice --data default --out invoice.pdf ``` This fills your _local_ files with the data set on your machine, sends the document through the API and writes the PDF production would produce. `--data` takes a data set name or the path of a JSON file; `--remote` renders the published version in the workspace instead of the local files. It is free with a test key; with the live key `formfeed login` stores, it costs [units](concepts#units). A Word or PowerPoint template is filled on your machine instead, with the engine the render servers use, because the API renders only saved versions of such templates: ```bash npx formfeed render offer --output docx --out offer.docx # the filled document, no render npx formfeed render offer --output pdf --out offer.pdf # filled here, converted by the API ``` Without `--output`, the template's default output decides (`office.output` in `settings.json`, else the document's own format). The PDF comes from [`POST /pdf/convert`](../api/reference/convert-office-to-pdf), which needs the Starter plan or above; the filled document needs only a key. QR codes, barcodes and SVG pictures are drawn by `@resvg/resvg-js`, an optional dependency npm installs with the CLI where the package supports the platform; without it they are left out with a warning. ## 6. Push and publish ```bash npx formfeed templates diff # what changed against the workspace npx formfeed templates push --dry-run # which templates are new, modified or unchanged npx formfeed templates push # create draft versions from your files npx formfeed templates push --publish # …and publish them npx formfeed templates push --channel staging # …or point the staging channel at them ``` `push` skips templates whose files have not changed since the last pull or push (unless `--publish` or `--channel` asks for more), and `--message` gives the new versions a note. It refuses to overwrite when the template got a newer version in the workspace since your last pull or push, saved in the editor or pushed by someone else: it answers with a [conflict](../errors/conflict) instead. Run `npx formfeed templates pull `, merge, and push again, or pass `--force` to push over it. When a template stores a data schema, `--publish` and `--channel` also refuse a change that would break callers of the current version, until you add `--allow-breaking`; see [Release channels](../templates/release-channels#schema-check). `npx formfeed types` generates types for your application code from the same schemas ([Typed template data](../templates/typed-data)). ## 7. Let CI do it Validate and preview on every pull request, publish on merge. Use a test key for the checks and a live key only for the publish step: ```yaml # .github/workflows/templates.yml name: templates on: pull_request: { paths: ['templates/**', 'partials/**'] } push: { branches: [main], paths: ['templates/**', 'partials/**'] } jobs: templates: runs-on: ubuntu-latest env: FORMFEED_API_KEY: ${{ secrets.FORMFEED_TEST_KEY }} steps: - uses: actions/checkout@v4 with: { fetch-depth: 0 } - uses: actions/setup-node@v4 with: { node-version: 22 } - run: npx formfeed validate --ci - run: npx formfeed test --ci - if: github.event_name == 'pull_request' run: npx formfeed ci preview --base origin/main --ci - if: github.event_name == 'push' run: npx formfeed templates push --publish --message "${{ github.sha }}" --ci env: FORMFEED_API_KEY: ${{ secrets.FORMFEED_LIVE_KEY }} ``` `formfeed ci preview` renders only the templates changed since the base ref (hence `fetch-depth: 0`) into `formfeed-preview/` and writes a Markdown summary, `formfeed-preview.md`, you can post as a pull-request comment. More in [CI usage](../cli/ci). On GitHub, the [Formfeed GitHub Action](../integrations/github-actions) runs the same steps and keeps the comment up to date for you. ## Where to go next - [Writing templates](../templates/writing-templates): pages, headers, fonts, images. - [Command reference](../cli/commands): every command and flag. - [Configuration](../cli/configuration): `formfeed.json`, environment variables and their precedence. - [Coding agents](../devkit/coding-agents): Formfeed's plugin, so Claude Code, Cursor and other agents run this loop themselves. ================================================================================ # Core concepts Source: https://docs.formfeed.dev/getting-started/concepts Summary: Organisations, workspaces, templates and versions, renders and jobs, API keys, units and regions, and how they fit together. A handful of nouns cover the whole product. Once they make sense, the API reference reads like a list of things you already know. ```mermaid flowchart TD O[Organisation
billing, members, plan] --> W1[Workspace
region, retention] O --> W2[Workspace] W1 --> T[Template
slug: invoice] W1 --> K[API keys
ff_live_… / ff_test_…] W1 --> H[Webhook endpoints] T --> V1[Version 1
archived] T --> V2[Version 2
published] T --> V3[Version 3
draft] V2 --> R[Render
rnd_…] R --> OUT[Output
PDF / PNG / JPG] ``` ## Organisation and workspace An **organisation** is the billing and membership boundary: one plan, one invoice, one member list. It also holds what every document shares across workspaces: the fonts, the [brand kit](../templates/brand-kit) and the [shared partials](../templates/shared-partials). Every member has one of four roles in the organisation: | Role | May | | ------ | ------------------------------------------------------------------------------------------------------------ | | Viewer | read templates, renders and files | | Editor | everything a viewer may, plus edit, save and publish templates, render, and upload files | | Admin | everything an editor may, plus create and revoke API keys, manage webhooks, invite members, delete templates | | Owner | everything an admin may, plus the plan and billing | A **workspace** is where the work lives: templates, API keys, renders, webhook endpoints and the [file library](../api/files). Each workspace has its own region and output retention. Keep production and staging apart with two workspaces; agencies usually create one workspace per client. How many workspaces an organisation may have depends on its plan (1 on Free, 3 on Starter, 10 on Pro). An API key belongs to exactly one workspace and works only there: it renders that workspace's templates and lists that workspace's renders. The one exception is the `workspace:delete` [scope](../api/authentication#scopes), which may delete another workspace of the same organisation. ## Template and version A **template** is a named document definition with a `slug` (`invoice`, `certificate-en`) that your code refers to. The API also gives every template an id (`tpl_` followed by 32 hexadecimal characters); a render accepts either. A template has a kind (`pdf`, `image`, or `docx` and `pptx` for [Word and PowerPoint templates](../templates/office-templates)), a template language (`jinja2`, `liquid` or `handlebars`, see [Template languages](../templates/languages)), and these parts: | Part | What it holds | | --------------------------------- | ----------------------------------------------------------------------------------------------------------- | | Template (`html`) | The body: HTML with template tags | | CSS, Head | Stylesheet, and extra `` content such as font links | | Header, Footer | Running header and footer for every PDF page | | [Settings](../templates/settings) | Paper, margins, image size, locale, timezone, currency, Tailwind | | Sample data | Named JSON data sets for the preview, autocomplete and tests; never used by an API render | | Data schema | A JSON Schema of the expected `data`, inferred from the sample data unless you provide one | | Translations | Per-locale dictionaries for the `t()` helper | | Partials | Fragments the template includes by name, saved with the version (from the CLI project's `partials/` folder) | A Word or PowerPoint template keeps its document file in place of the HTML, CSS, head, header and footer; its settings, sample data, schema and translations work the same way. A **version** is an immutable snapshot of all of those parts. Saving in the editor or running `formfeed templates push` creates a new **draft** version. **Publishing** a version makes it the one the API renders; the previously published one is archived. Exactly one version is published at a time, so you can keep editing drafts without touching production. Restoring an old version saves a new draft with its content. Besides `published`, a template can have named [release channels](../templates/release-channels) such as `staging` that point at other versions; a render picks one with `version`. A render's `version` field takes: | Value | Renders | | ------------------ | -------------------------------------------------------------------------- | | `published` | The published version (the default) | | a channel name | The version the [release channel](../templates/release-channels) points at | | a number, e.g. `3` | That version | | `latest` | The newest saved version, published or not; test keys only | The API renders the published version unless a render names a version or a channel, and picks up a newly published one within a minute. The editor's preview and its **True render** work on what is open in the editor, unsaved changes included, with the sample data set you picked. ## Render and job A **render** is one execution: a source (a template, ad-hoc `html`, or a `url`), plus `data`, plus options, producing one output file. Every render has an id (`rnd_` followed by 32 hexadecimal characters), a status (`queued` → `rendering` → `succeeded` or `failed`), timings, the units it cost, and checksums that make it reproducible. See [Rendering](../api/rendering). A **job** is a batch of up to 1,000 renders submitted in one request (`POST /v1/renders/batch`), always processed asynchronously, optionally zipped. A job has its own id (`job_…`) and status: `queued`, `processing`, then `completed`, `partially_failed` or `failed`. See [Async and batch](../api/async-and-batch). ## API keys and environments {#environments} API keys come in two environments, and the prefix tells you which. Owners and admins create them on the **API keys** page of a workspace. | | Live (`ff_live_…`) | Test (`ff_test_…`) | | ------------------------ | ------------------------------------------------------- | --------------------------------------------------------------------------- | | Metered | Yes, `units` in the answer | No, `units` is `0` | | Counts against the quota | Yes | No | | Output kept | The plan's retention, or the workspace's shorter one | At most 24 hours | | Test mark on the file | Never | On the Free plan; on every plan for an [e-invoice](../templates/e-invoices) | | Deduplicated | Yes | No | | `version: "latest"` | Refused | Allowed | | Rate limit | The plan's requests per second on renders and PDF tools | Same, plus 30 per minute on the Free plan | Use test keys for development, CI and the dev kit, and switch to a live key in production. Each key also carries [scopes](../api/authentication#scopes) that decide what it may do. How long each plan keeps outputs and its other limits are in [Limits and usage](../api/limits-and-usage). ## Units {#units} Formfeed meters in **units**: - a PDF of up to 5 pages is 1 unit, and every further 5 pages (or part of them) add 1: 6–10 pages cost 2 units, 11–15 pages cost 3; - an image (PNG, JPG or WebP) is 1 unit, and so is a filled Word or PowerPoint file; a PDF from a Word or PowerPoint template, and one from [`/pdf/convert`](../api/pdf-tools), follow the PDF rule; - each post-processing step (merging PDFs, adding a password, stamping a watermark) is 0.5 units, on a render's `post` as with the [PDF tools](../api/pdf-tools); reading a PDF's information is free; - an [e-invoice](../templates/e-invoices) adds 1 unit to its render; - renders with a test key, previews in the editor and offline validation are free; - a [deduplicated](../api/rendering#deduplication) render costs 0 units. Every render response reports the units in its body (`units`) and in the `X-Units-Charged` header, and `X-Units-Remaining` tells you what is left in the billing period. Plans include an amount per billing period. Paid plans continue with overage, up to a hard cap unless you switch on auto top-up; the Free plan stops with [`402 quota_exceeded`](../errors/quota-exceeded). Details in [Limits and usage](../api/limits-and-usage). ## Region The **region** is where rendering and output storage happen. Today every render runs in `eu`: rendering in Frankfurt, storage in the EU. `us` and `apac` are names the API contract reserves for regions that have no render worker yet, so leave the `region` field of a render out; it defaults to the workspace's region. See [Data residency](../api/data-residency). ## Webhook endpoint A **webhook endpoint** is an HTTPS URL in your application that receives signed events such as `render.completed` or `job.completed`. It is how asynchronous work reports back. See [Webhooks](../api/webhooks). ================================================================================ # Editor tour Source: https://docs.formfeed.dev/getting-started/editor-tour Summary: The web editor in two minutes, with every keyboard shortcut. The editor is built on Monaco, the editor component of VS Code, and it knows your data: it reads the sample data of the template and uses it for autocomplete, diagnostics and the preview. The page has three parts. The **rail** on the left switches between the editor's panels: **Code** (the tabs Template, CSS, Head, Header and Footer), **Data and schema**, **Translations**, **Page setup** (paper, margins and the other [settings](../templates/settings)), **Versions** and **API**; image templates also have **Design**, the [visual editor](../templates/visual-editor). The triangle at the bottom of the rail opens the problems list. The middle shows the panel you picked, the right side the preview, and the status bar at the bottom names the engine, the paper, the cursor position, the problem counts and the data set in use. ## Autocomplete from your data Type `{{ invoice.` and the editor offers the fields of your sample data with their type. Press `Ctrl+Space` once more to open the details beside the list, which show each field's example value. ## Diagnostics Unclosed tags, unknown filters and variables missing from the sample data are underlined as you type. The strip under the code counts the errors and warnings and names the first one; click it for the whole list, and click a problem to go to its line. `F8` jumps to the next problem. Errors are what would fail a render (an unknown filter, a tag that does not parse); warnings are worth a look but render (a variable your sample data lacks prints as empty). ## Blocks **Blocks**, at the start of the tab strip, opens a library of ready-made pieces beside the code: letterhead, window envelope address, invoice table, totals, payment terms, SEPA payment QR code, signature, page break and more, followed by your organisation's [shared partials](../templates/shared-partials). Search by name, point at a block to see a preview of it, and click to insert it at the cursor. A block brings the CSS it needs and adds the sample data and translations it reads, so the preview shows it straight away. ## Paginated preview The switch above the preview has three modes: | Mode | Shows | | --------------- | ------------------------------------------------------------------------------------------------ | | **Flow** | The document as one long page, rendered in your browser as you type | | **Paged** | Real pages with headers, footers and page numbers, rendered in your browser (PDF templates only) | | **True render** | The file the render servers make, after you click **Render** (see [True render](#true-render)) | In the Flow and Paged previews, click an element and the editor opens the line that produced it, in the Header or Footer tab when that is where it comes from. Zoom the flow and paged previews with the buttons above them, `Ctrl` and the mouse wheel (or a pinch on a touchpad), or `Ctrl` with `+`, `-` and `0`. The true render shows the PDF in the browser's own viewer, which has its own zoom. ## Edit text in the preview {#edit-text} To change the wording without reading the code, switch on **Edit text** (the pencil above the preview, or _Edit text in the preview_ in the command palette). Text you can edit is outlined as you point at it; click it and type. - **Values stay values.** A variable such as `{{ customer.name }}` shows its value from the sample data as a highlighted chip. You can type around it or delete it, but its expression is kept as written. A heading that holds nothing but a variable works the same way: the chip stands alone, and you add text before or after it, while the value itself is changed in the data. The same goes for inline markup such as `` or a link: click it to edit its own text. - **Enter or Tab keeps the change, Escape drops it.** `Shift+Enter` starts a new line. The change goes into the template like one you typed in the code, so it shows as unsaved and is undone with `Ctrl+Z`, in the preview or in the code. - **Translated text edits the dictionary.** Where the whole text is one `t()` call, the edit changes the translation of the language the preview shows (the template's locale). If that language had no text yet and the preview showed English, your text becomes the new translation for that language; English stays as it was. - **Repeated text changes everywhere.** Text inside a loop is written once in the template, so editing one row changes every row; a note above the text says how many. - **Some text stays in the code.** Text with an `if` or a loop inside it, text a helper or a partial writes, and elements that hold other blocks are edited in the code; clicking them opens their line as outside edit mode. The brand kit's texts, such as the legal footer, are changed on the Brand page. Editing is for owners, admins and editors. A text split across two pages in the paged preview is edited in the flow preview. ## True render One click on **Render** (or `Ctrl+Enter`) renders what is open in the editor, unsaved changes included, on our servers with the data set you picked, and shows the exact PDF or image, with how long the render took, its size and its page count below it. It costs nothing; on the Free plan the result carries a small test mark, and an e-invoice carries it on every plan. ## Data sets Keep several named sample data sets per template (one line, fifty lines, missing optional fields) and switch between them in the list of the **Data and schema** panel. Sample data never leaves the editor: it drives the preview, the completion and the inferred schema, while an API render always uses the data in the request. The preview and the schema follow the set you pick. `default` is the set a template starts with; the API uses it only where a call brings no data of its own (`POST /templates/{id}/validate` without `data`, and the schema it infers for a template that stores none). The command line sees every set as a `data/*.json` file. ## More in the editor - **Brand kit and shared partials.** The preview and True render use the organisation's [brand kit](../templates/brand-kit) and [shared partials](../templates/shared-partials); autocomplete offers `brand.*` and the names of shared partials, and the problems list tells you when a partial stored with the template hides a shared one. - **History and diff.** Every saved version is kept. The **Versions** panel shows the working copy at the top, then the [release channels](../templates/release-channels), then every saved version with its note and who saved it. **Compare** puts a saved version beside what is open in the editor, **Restore as new draft** saves its content as a new version, and **Publish** makes it the one the API renders. The panel lists the latest fifty, and **Show older versions** under them brings the rest, fifty at a time. - **Word and PowerPoint templates** open a page of their own instead of the code editor, with a quick preview of Word documents and the true render as PDF. See [Word and PowerPoint templates](../templates/office-templates). - **Search and commands.** `Ctrl+K` (`⌘K` on a Mac) opens one search box on every page. It finds the commands of the page you are on (save, publish, history, true render, preview mode, zoom, a tab, a data set, a block), your templates, the examples, shared partials, files, API keys (by name and prefix), webhook endpoints (by address), the members of your organisation (by name and e-mail), the app's pages and the sections of settings, billing and brand, and the documentation's main pages. Words may come in any order and umlauts need not be typed; the part of each result that matched is marked. With nothing typed, the box starts with the templates you opened last. - **Narrowing the search.** Type `#` to search templates only, `>` for commands, `/` for pages and settings, `?` for help; Backspace in the empty field goes back to everything. A pasted render id (`rnd_…`) or template id (`tpl_…`) opens that render or template. A pasted API key finds the key it belongs to by its first characters, in your browser only: nothing is sent for it. - **What to do with a template.** `→` on a template lists its actions: open it in the editor, open its API tab, see its renders, or copy its slug or id. `←` goes back to the results. Owners and admins also find **Create an API key**, **Add a webhook endpoint** and **Invite members**, which open that page with its dialog or its invite field. ## Keyboard shortcuts The search box lists the shortcut next to each command, so `Ctrl+K` is also the fastest way to look one up. | Shortcut | What it does | | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `Ctrl+K` / `⌘K` | Open the search box. Works on every page, and inside the editor too. | | `↑` / `↓`, `PageUp` / `PageDown` | In the search box: move through the results, or jump to the next or previous group. | | `Enter`, `Ctrl+Enter` / `⌘Enter` | In the search box: open the highlighted result, or open it in a new tab. | | `→` / `←` | In the search box: list the actions of the highlighted template, or go back to the results. | | `Ctrl+S` / `⌘S` | Save a version: opens the **Save version** dialog with an optional note. The browser's own save dialog stays out of the way. | | `Ctrl+Enter` / `⌘Enter` | True render: render what is open in the editor on our servers. | | `Shift+Alt+F` | Format the current tab. | | `F8` | Go to the next problem. | | `Ctrl+Shift+M` / `⌘⇧M` | Open or close the problems list. | | `Ctrl+Alt+→` / `Ctrl+Alt+←` | Next or previous code tab (Template, CSS, Head, Header, Footer). | | `Ctrl+\` / `⌘\` | Collapse or expand the code pane; on a narrow screen, switch between code and preview. | | `Ctrl+Wheel` / `⌘Wheel` | Zoom the preview in or out. | | `Ctrl` + `+` / `-` / `0` | Zoom the preview in, out, or back to 100 %, while the preview has the focus. | | `Escape` | Close the search box, a dialog or the blocks library; in the preview, drop a text edit. | `Shift+Alt+F` works on every code tab. Data and Translations are formatted as JSON, CSS as CSS, and Template, Head, Header and Footer as HTML — with `{{ ... }}`, `{% ... %}` and `{# ... #}` left exactly as they are, so reindenting never moves an engine tag. Inside the code area every [Monaco](https://microsoft.github.io/monaco-editor/) shortcut works as well — `Ctrl+F` to find, `Ctrl+H` to replace, `Alt+↑`/`Alt+↓` to move a line, `Ctrl+/` to comment, `F1` for Monaco's own command list. ## The templates list The **Templates** page lists every template of the workspace. Beside each slug, two small buttons copy the slug and the template ID, the two ways an API call names a template. **API request** in a row's menu opens the template on its [API panel](#api-tab). Tick the checkboxes to act on several templates at once: **Archive…** hides them from the list, and their API renders fail with [`404 template_not_found`](../errors/template-not-found) until you restore them; renders and outputs made before stay. With **Archived** switched on, the same bar offers **Restore** and, for owners and admins, **Delete permanently…**, which removes their versions, channels and stored files after you type the confirmation. Only archived templates can be deleted, and a deleted template cannot be restored. Renders keep their log entries. ## From the editor to your code {#api-tab} The editor works on drafts; the API renders the published version. Save with `Ctrl+S` as often as you like; nothing changes for your integration until you publish. The **API** panel, the last entry of the rail, is where the open template meets your code. Word and PowerPoint templates have it as a tab of their page. - **Which version a call renders.** The first line names the published version and says what is not in it: a newer saved version, or changes you have not saved. Where a saved draft is ahead of the published version, a switch offers it; the request then carries `"version": "latest"`, which the API accepts from test keys only. - **The request** for this template, in curl, TypeScript, Python, PHP, Go, C#, Java and Ruby. It names the template's slug, reads the key from `FORMFEED_API_KEY` and the data from `data.json`. What else a render can be told is in [Rendering](../api/rendering). - **`data.json`** is the data set the editor shows, to copy or download. It is the shape your code sends; your own data takes its place. - **A key.** Where the workspace has no test key, owners and admins create one with a click. Its secret is shown once, as the `export` line a shell needs, and is gone when you leave the panel. An existing test key is named by its prefix, because a secret cannot be shown twice. - **Send test request** sends the request on screen through the API and shows what comes back: the file, the render in the render log, and the JSON your code will hold, or the [problem](../api/errors) with the page of its code. It is a test render, as a test key makes one: free, kept for 24 hours, on the Free plan with the test mark, and it may render the latest draft. It needs no key, since the app sends it with your session. Owners, admins and editors can send it. `?panel=api` on a template's address opens it on this panel. ================================================================================ # Two-factor authentication Source: https://docs.formfeed.dev/getting-started/two-factor-authentication Summary: Protect your Formfeed account with a code from an authenticator app, what changes when it is on, and what to do when the phone is gone. With two-factor authentication on, signing in to the app takes two things: what you sign in with today (your password, a magic link, or your Google or GitHub account) and a 6-digit code from an authenticator app on your phone. Someone who learns your password still cannot get in. It is optional, every member decides for their own account, and it is included in every plan. ## Turn it on 1. In the app, open the menu at the bottom of the sidebar (your name) and choose **Two-factor authentication**. 2. Choose **Set up two-factor authentication**, name the authenticator, for example "iPhone" or "1Password", and choose **Continue**. The name tells your devices apart later and cannot be changed afterwards. 3. Scan the QR code with an authenticator app: 1Password, Google Authenticator, Microsoft Authenticator, Authy and every other app that speaks TOTP work. Without a camera, type the setup key shown beside the code into the app. 4. Enter the 6-digit code the app shows and choose **Turn on two-factor authentication**. From then on every sign-in asks for a code after the first step. Turning it on signs out your other sessions, so every browser proves itself once. Codes change every 30 seconds; if one is refused although you typed it right, set the clock of your phone to automatic. Add a second authenticator: There are no recovery codes. Add a second authenticator as a backup on the same page with **Add another authenticator**, such as a password manager next to the phone, and either one signs you in. With several, the code prompt lets you pick which one you are reading from. ## What it covers | | With two-factor authentication on | | ----------------------------------------------------------------- | -------------------------------------------------------------------------- | | Signing in to the app (password, magic link, Google, GitHub) | asks for a code | | Resetting a forgotten password | the link from the email asks for a code before the new password can be set | | `formfeed login` | you approve the command line in the app, so behind the code | | AI agents on the [hosted MCP endpoint](../integrations/mcp#oauth) | you allow each agent on its consent page, behind the code | | **API keys** | **unchanged**: a key is a credential of its own and never asks for a code | API keys keep working because servers cannot type codes. Treat them accordingly: a key in the wrong hands is not stopped by your authenticator, so [revoke keys](../api/authentication) you no longer need. ### Agents that were connected before An agent acts for an account with two-factor authentication only after that account allowed it from a session that passed the code. An agent you allowed before you turned two-factor authentication on is therefore refused from its next token on, which it gets within the hour: connect it again and allow it once more on the consent page. Until then, and for any agent that was never allowed this way, the API answers [`403 forbidden`](../errors/forbidden) with the reason. **Connected agents** in the same menu lists the agents your account allowed and revokes one. ## Turn it off On the same page, remove your authenticators with the bin icon (**Remove authenticator**) and confirm with **Remove**. Removing the last one switches two-factor authentication off, and signing in takes no code any more. ## Lost your phone? Use your second authenticator, if you added one, and replace the lost one on the page above. Without one, write to [support@formfeed.dev](mailto:support@formfeed.dev) from the address of the account; a request from any other address gets only the answer to write from that one. We switch two-factor authentication off once we have checked that the account is yours, which takes longer than reading a code: expect questions only the account's owner can answer, and an owner of your organisation may vouch for you from their own address. Your API keys keep working in the meantime, so nothing that renders is interrupted. Afterwards you sign in with your password, magic link or Google or GitHub account alone. Turn two-factor authentication on again, this time with a second authenticator. ================================================================================ # Template languages Source: https://docs.formfeed.dev/templates/languages Summary: Jinja2-compatible, Liquid and Handlebars side by side, with the same invoice in all three, and how to choose. Every template is written in one of three languages. They share the same data, the same [helpers](helpers) and the same [settings](settings); only the syntax differs. You pick the language when you create a template, and it stays with the template: it cannot be changed later, so to move a template to another language, create a new one and carry the markup over. ## Cheat sheet | | Jinja2-compatible | Liquid | Handlebars | | ---------------------- | ------------------------------------------------------------ | ------------------------------------------------------- | -------------------------------------------------- | | Variable | `{{ invoice.number }}` | `{{ invoice.number }}` | `{{invoice.number}}` | | Helper with arguments | `{{ total \| money("EUR") }}` or `{{ money(total, "EUR") }}` | `{{ total \| money: "EUR" }}` | `{{money total "EUR"}}` | | Helper with options | `{{ qrcode(url, { size: 120 }) }}` | `{{ url \| qrcode: size: 120 }}` | `{{qrcode url size=120}}` | | Loop | `{% for l in lines %}…{% endfor %}` | `{% for l in lines %}…{% endfor %}` | `{{#each lines}}…{{/each}}` | | Loop index (from 1) | `{{ loop.index }}` | `{{ forloop.index }}` | `{{add @index 1}}` | | Condition | `{% if paid %}…{% elif sent %}…{% else %}…{% endif %}` | `{% if paid %}…{% elsif sent %}…{% else %}…{% endif %}` | `{{#if paid}}…{{else if sent}}…{{else}}…{{/if}}` | | Comparison | `{% if total > 1000 %}` | `{% if total > 1000 %}` | `{{#if (gt total 1000)}}` | | Variable assignment | `{% set vat = total * 0.19 %}` | `{% assign vat = total \| times: 0.19 %}` | not available; compute in your data or use helpers | | Include a partial | `{% include "footer" %}` | `{% include "footer" %}` | `{{> footer}}` | | Comment | `{# … #}` | `{% comment %}…{% endcomment %}` | `{{!-- … --}}` | | Escape HTML in a value | done for you | `{{ name \| escape }}` | done for you | | Output HTML as it is | `{{ html \| safe }}` | done for you | `{{{html}}}` or `{{safe html}}` | Liquid does not escape: Jinja2 and Handlebars escape every value they print, so a customer name containing ` ``` For QR codes, the [`qrcode`](../templates/helpers#qrcode) helper replaces JavaScript libraries: `QR code: {{ payment.url }}`. The report points at a QR library it finds in the template. ## Paged.js {#paged-js} Formfeed paginates with Chromium and shows a Paged.js preview in the editor. Templates that loaded Paged.js themselves keep the script; check running headers and margin boxes in the true render. ## Forms {#forms} PDF form field markers (`[% data_form_field … %]`) are not supported and stay as text. ## Sample data {#sample-data} PDFMonkey's sample data becomes the template's default data set; completion, the preview and the report's check for missing variables use it. Your API requests keep sending the payload as before, now as the `data` of a render. ## Switch the API {#api} Replace the call to PDFMonkey's documents endpoint with a render. The import creates drafts, and the API renders the published version, so publish each template first. The request names the template by its slug (PDFMonkey's template identifier in lowercase with dashes): ```bash curl https://api-eu.formfeed.dev/v1/renders \ -H "Authorization: Bearer ff_live_…" \ -H "Content-Type: application/json" \ -d '{ "template": "order-confirmation", "data": { "order": { "id": "A-1" } } }' ``` `mode: "async"` plus a webhook replaces PDFMonkey's pending documents and webhook events; see [Async and batches](../api/async-and-batch). ================================================================================ # Migrate from jsreport Source: https://docs.formfeed.dev/migrate/jsreport Summary: Import Handlebars templates from a jsreport export, with assets, child templates and chrome-pdf settings. jsreport templates with the Handlebars engine and the chrome-pdf or chrome-image recipe move to Formfeed almost unchanged: both render with Chromium, jsreport's chrome options are Formfeed's page settings, and header and footer templates use the same `pageNumber` and `totalPages` classes. What changes is what jsreport runs as code on your server: custom helpers and scripts. ## Import templates {#import} Export from jsreport, in the Studio (**Export**) or with the CLI: ```bash jsreport export templates.jsrexport --serverUrl=https://reports.fennlor.example ``` In the app: **Templates → Import → jsreport**, choose the `.jsrexport` file and pick the templates. Handlebars templates with a chrome recipe are preselected; child templates and components need no import of their own, because they are inlined into the templates that use them. **Analyse** shows a report per template, **Create drafts** makes a Handlebars template each. With the CLI, in a template project: ```bash formfeed import jsreport templates.jsrexport # every Handlebars chrome template formfeed import jsreport templates.jsrexport --template invoices/invoice ``` ## Settings {#settings} `format`, `width`, `height`, `landscape`, the four margins, `displayHeaderFooter` with `headerTemplate` and `footerTemplate`, `printBackground`, `scale`, `pageRanges` and `waitForNetworkIdle` are mapped to the template's [settings](../templates/settings) (**Page setup** in the editor), and the `title`, `author`, `subject` and `keywords` of `pdfMeta` become the PDF metadata. chrome-image templates become image templates with their viewport size (1200 × 630 when the export has none), type (PNG or JPEG), quality, `fullPage`, and `omitBackground` as a transparent background. `waitForJS` (`window.JSREPORT_READY_TO_START`) has no direct equivalent: set [`waitFor.selector`](../templates/settings#waiting-for-content) in the template's `settings.json` or in the `settings` of the API request, and let your script add that element when it is done. The editor's settings form has no field for it. PDF passwords and `mediaType: screen` are not imported; the report names both. For a password, send [`post.password`](../api/rendering#post-processing) with the render (Starter plan and above). jsreport's localization files are not imported either: put the texts into the template's translations and print them with the [`t`](../templates/helpers#t) helper. ## Assets {#assets} `{{asset "logo.png" "dataURI"}}` and the other encodings are resolved from the export: images and fonts up to 256 KB become data URIs, text assets (CSS, scripts, `utf8` and `string` encodings) are inlined, and an asset jsreport links from an `http(s)` address becomes that address. Larger files, assets the export does not contain and assets linked from the jsreport server become `{{asset "logo.png"}}` and are listed in the report. Formfeed's [`asset`](../templates/helpers#asset) helper points at the workspace's [file library](../api/files): upload those files there under the same name (on the **Files** page or with `formfeed files push`) and the references resolve. A public HTTPS URL in their place works as well. A template that is larger than 1 MB after its assets are inlined is an error in the report; upload the large files instead. The jsreport 2 syntax `{#asset logo.png @encoding=dataURI}` is converted the same way. ## Child templates {#child-templates} `{{childTemplate "invoices/lines"}}` (and jsreport 2's `{#child invoices/lines}`) is replaced by the child template's content, which then renders with the same data. The report names child templates the export does not contain. ## Components {#components} `{{component "./address"}}` is replaced by the component's content. Properties passed to a component are not carried over: the inlined content sees the surrounding data, so reference those values directly. ## Helpers {#helpers} Custom helpers are JavaScript that jsreport runs on your server; Formfeed does not run custom code. The report lists every helper the template defines, and every call to one is an error, because it would fail the render. Most helpers have a built-in equivalent: | Typical jsreport helper | Formfeed | | ------------------------------ | -------------------------------------------------------------------------------------------------------------- | | number and currency formatting | `{{money total "EUR"}}`, `{{number qty 2}}` | | date formatting with moment | `{{date issued "dd.MM.yyyy"}}` | | sums and totals | `{{sum lines "total"}}` | | `toJS` | `{{{json value}}}` (converted by the importer) | | QR codes and barcodes | `QR code`, `Barcode {{sku}}` | For anything else, compute the value in your application and send it with the data. The pdf-utils helpers (`pdfAddPageItem`, `pdfCreatePagesGroup`) and `module` have no equivalent. ## Engines {#engines} Formfeed runs Handlebars, Liquid and Jinja2. jsrender, EJS and Pug templates are imported as Handlebars drafts with an error: rewrite them in one of the three languages. A template without an engine (`none`) becomes a Handlebars template. ## Recipes {#recipes} chrome-pdf and chrome-image are supported, and the html recipe becomes a PDF template. Excel, Word and other recipes are reported and imported as PDF templates for reference. ## Scripts {#scripts} jsreport scripts (`beforeRender`, `afterRender`) usually load data before the render. Load that data in your application and send it with the render request; Formfeed renders what it receives. ## pdf-utils {#pdf-utils} Merge, append and prepend operations and headers taken from another template are not imported. Render the parts as separate documents, or combine them in one template with page breaks (`{{pageBreak}}`). ## Sample data {#sample-data} The data entity a template points at becomes its default data set; completion, the preview and the report's check for missing variables use it. ## Switch the API {#api} jsreport's `POST /api/report` becomes a render. The import creates drafts, and the API renders the published version, so publish each template first. The request names the template by its slug (the template's name in lowercase with dashes) and sends the data that jsreport's request carried: ```bash curl https://api-eu.formfeed.dev/v1/renders \ -H "Authorization: Bearer ff_live_…" \ -H "Content-Type: application/json" \ -d '{ "template": "invoice", "data": { "number": "R-7", "total": 12.5 } }' ``` ================================================================================ # Changelog Source: https://docs.formfeed.dev/changelog Summary: What changed in the Formfeed API, SDKs, CLI and template engine. API changes within `/v1` are additive: new endpoints, new optional fields, new response fields, new error codes and new event types. Anything that could break a client ships in a new version and is announced here first. ## 22 October 2026: the n8n node is verified {#2026-10-22-n8n-verified} - **n8n:** n8n has verified `n8n-nodes-formfeed`, so n8n Cloud offers it as well as self-hosted n8n: open the nodes panel, search for _Formfeed_ and click **Install node** (owners and admins; see [n8n](integrations/n8n#install)). Installing by package name still works on a self-hosted n8n. A current n8n offers the node to its AI Agent as a tool without the setting `N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE`, which older self-hosted versions needed. ## 21 October 2026: packages 0.3.19 {#2026-10-21-packages-0-3-19} - **SDKs:** the types of `@formfeed/sdk` and the Python package `formfeed` know the usage's `period_start`, `period_end` and `hard_cap_units`, which the API has always sent (see [20 October](#2026-10-20-contract)). In TypeScript a version's `data_sets` and `partials` are no longer `null`, which the API never sends, and both SDKs give the range of a watermark's `rotation` and say that an image stands upright when it is left out. - **Template engine:** `renderVersion` of `@formfeed/engine` renders `pdf.metadata.author`, `subject` and `keywords` with the data, as renders do since [19 October](#2026-10-19-render-fixes), and returns them as `metadata`; one that renders empty is left out. - **Plugin for coding agents:** the plugin in [formfeed-dev/skills](https://github.com/formfeed-dev/skills) brings two skills beside the hosted MCP server: `formfeed-templates` writes and checks templates in a project with the command line, and `formfeed-api` writes the code that renders through the API. Both say so before a render with a live key costs units, and change the workspace only when asked. Claude Code installs it as a plugin; any agent that reads Agent Skills takes the skills with `npx skills add formfeed-dev/skills`. See [Coding agents](devkit/coding-agents). ## 21 October 2026: the Power Automate connector is available {#2026-10-21-power-automate} - **Power Automate:** the Formfeed connector Microsoft certified on 5 October is in makers' environments: add an action or a connection in Power Automate, Power Apps or Copilot Studio and search for _Formfeed_, published by Formfeed and marked _Preview_. Where it is not offered, the HTTP action makes the same call. See [Power Automate](integrations/power-automate). ## 21 October 2026: keys, endpoints and members in the search box {#2026-10-21-search-box} - **App:** the search box on `Ctrl+K` finds API keys by name and prefix, webhook endpoints by address, and the members of the organisation by name and e-mail, with its open invitations for owners and admins. A pasted API key finds the key it belongs to by its first characters, in the browser only. `→` on a template lists its actions (open it, its API tab, its renders, copy its slug or id) and `←` goes back. Owners and admins find **Create an API key**, **Add a webhook endpoint** and **Invite members** there too. See the [editor tour](getting-started/editor-tour#keyboard-shortcuts). - **Changed:** **Copy template ID** in the templates list copies the id the API shows (`tpl_…`) instead of the bare UUID. The API takes both, and the search box opens a pasted `tpl_…` id as the template. ## 21 October 2026: a batch keeps the order of its items {#2026-10-21-batch-order} - **Fixed:** `GET /jobs/{id}`, the `job.completed` event and the batch zip list a batch's items in the order of the request, as documented. They listed them in the order the renders finished, which differs whenever items finish out of order (renders run side by side, documents differ in length, a retried item finishes last), so `items[2]` could be another row's document and `0003-document.pdf` another item's file. A zip's files are now numbered by their item's position in the request, and a failed item leaves its number out. Batches sent before this fix keep the order they had. See [Async and batch](api/async-and-batch#batch). ## 21 October 2026: a failed render in the apitemplate.io shim, and a password's permissions {#2026-10-21-compat-permissions} - **Fixed:** an error of the [apitemplate.io shim](api/compat-apitemplate) about a render that was recorded before it failed carries the render's id as `transaction_ref`, as the error of a render still running already did. Template errors and page limits came back without it, so the failed render could not be looked up. - **Fixed:** a render with `post.password` and no `permissions` is recorded with the four permissions its PDF allows (`print`, `copy`, `modify`, `annotate`). It was recorded with none, which is what `[]` means, so the render's options in the app said readers may do nothing. The PDFs themselves were right. ## 21 October 2026: four answers that said less than they should {#2026-10-21-api-answers} - **Changed:** `POST /webhooks/{id}/test` for a disabled endpoint answers `409 conflict`. It answered `202` and nothing arrived, because a disabled endpoint receives no deliveries. See [Webhooks](api/webhooks). - **Fixed:** a repeated request with the same `Idempotency-Key` carries the first answer's `Location`, `Retry-After`, `X-Units-*` and `X-Render-*` again, not its body alone, so a client that follows the `Location` of a `202` can follow a replayed one too. See [Idempotency](api/overview#idempotency). - **Fixed:** a [PDF tool](api/pdf-tools) that fails after it was recorded names its render as `render_id` in the problem, as a failed render does; the render log and `GET /renders/{id}` show it. - **Fixed:** `POST /pdf/watermark` with an image checks the key's scope and plan before it looks for the image in the library, so a key that may not use the tool hears `403 forbidden`, never whether the library has a file of that name. ## 21 October 2026: the render log keeps 13 months {#2026-10-21-render-log-retention} - **Changed:** render log entries are kept for 13 months. Once a month, older entries are deleted with their batch jobs, each once its files are gone: an entry whose file is still stored stays as long as the file does. `GET /renders/{id}` and `GET /jobs/{id}` answer `404` for what was deleted, and `GET /usage` keeps the totals of every billing period but lists the days and templates of a period only while its entries exist. Until now entries stayed until the workspace was deleted; the oldest reach 13 months in October 2027. See [Data residency](api/data-residency). ## 20 October 2026: slugs of two characters {#2026-10-20-slugs} - **Fixed:** a slug of two characters, such as `de` or `q4`, is accepted for templates, organisations and workspaces. The rule took one character or three and more, but never two, in the API, the app and the database alike. Slugs are 1 to 64 characters for templates and 1 to 40 for organisations and workspaces, lowercase letters, digits and dashes, starting and ending with a letter or digit. See [Templates](api/templates). ## 20 October 2026: the API contract says what the API does {#2026-10-20-contract} - **Changed:** the [API reference](api/reference/formfeed) and `/v1/openapi.json` now describe every field, header and status code the API sends. A render's `error` has a schema of its own (`code`, `message` and what says more), no longer that of a problem; a render, a job, an account and the usage list every key they always carry; the problems name their members with the codes they come with; the binary answer of `output=binary` lists all six file types and its headers; and the apitemplate.io shim's answers, its `302` and its errors in apitemplate.io's shape are described as they are. The answers themselves change only where an item below says so. - **Fixed:** `POST /templates` keeps `data_sets`, the named sample data sets, in the first version. It dropped them, so `formfeed templates push` lost them for a new template. - **Fixed:** publishing a version of a Word or PowerPoint template (`POST /templates/{id}/versions/{number}/publish`) answered with the file's internal storage key, which no other answer shows. It is gone from the answer. - **Fixed:** a `cursor` that no page of the list handed out answers `400 invalid_request` on `GET /renders`, `/templates` and `/files`, where it answered `500`; so do `GET /usage?period=` with a month that does not exist and `output_image_type` values the apitemplate.io shim does not know. The shim now also takes apitemplate.io's `jpegOnly` and `pngOnly`, and answers JSON when its URL carries `output=binary`, where it answered `500` after rendering. - **Fixed:** an id of 36 characters that is no UUID answers `404` on the webhook routes, where it answered `500`; a `formfeed listen` session's id is unknown to `DELETE /webhooks/{id}` and `POST /webhooks/{id}/test`, as it already was to the list and `PUT` (end a session at [`DELETE /webhooks/listen/{id}`](api/reference/end-listen-session)); and `POST /webhooks` always answers with `environments`. - **Changed:** the `202` of [`POST /renders/batch`](api/reference/create-batch) names the job in `Location` as an absolute URL, as a render's `202` does, with `Retry-After: 5`. A `rate_limited` problem of a Free test key's minute says `window_seconds: 60`, where it said 1. ## 19 October 2026: metadata from the data, a time limit for printing, images that open {#2026-10-19-render-fixes} - **Changed:** in a template render, `pdf.metadata.author`, `subject` and `keywords` are rendered with the data like the title; one that renders empty is left out. Until now they went into the PDF as written, template syntax and all. See [the settings](templates/settings). - **Changed:** printing a PDF has a time limit, 120 seconds; a print that takes longer fails with [`504 render_timeout`](errors/render-timeout). Until now printing had none. A document of several hundred pages prints in tens of seconds. - **Changed:** a rendered image (PNG, JPG, WebP) opens in the browser under its file name when its link is opened, where it used to download; this holds for images rendered before too. PDFs and office files still download, and `download=1` on a link makes an image a download as well. See [File name](api/rendering#file-name). ## 19 October 2026: packages 0.3.18 {#2026-10-19-packages-0-3-18} - **MCP server:** `@formfeed/mcp` 0.3.18 returns the picture of an image render beside its link and starts without `FORMFEED_API_KEY` with the offline checks (see [below](#2026-10-19-mcp-images)), and every tool declares what it returns (`outputSchema`), so a client can check the structured result. The MCP registry's entry no longer requires the key. - **Template engine and command line:** `@formfeed/engine` and `formfeed` bring the new default `waitFor.networkIdle: "auto"` and mark the engine's scripts with `data-formfeed` (see [18 October](#2026-10-18-load-wait)), so a `formfeed test` snapshot of a template with fitted text changes by that attribute: approve it again with `formfeed test -u`. `formfeed dev` breaks tables in its paged preview as the PDF does (see [18 October](#2026-10-18-paged-tables)), `formfeed ci preview` names the key that rendered, and the help of `--base-url`, `--workspace`, `--ci` and `--verbose` says what each really does. The examples of the `epcQr` helper put the invoice number into `text`, the remittance line. - **n8n node:** `n8n-nodes-formfeed` 0.3.18 says that only the Free plan marks test renders, and no longer mentions a self-hosted gateway. ## 19 October 2026: agents see the images they render {#2026-10-19-mcp-images} - **MCP server:** `render` and `get_render` return the picture of an image render (PNG, JPG or WebP, up to 2 MB) beside its link, so an agent can look at what it made before handing it on. A PDF still comes as its link. The hosted server at `mcp.formfeed.dev` has it now, `@formfeed/mcp` with the next release of the packages. See [MCP server](integrations/mcp). - **MCP server:** the local server starts without `FORMFEED_API_KEY` too. `validate_template` with `html` and `check_invoice` work offline then, and every other tool answers with how to get a key, so an agent can try the checks before anyone signs up. With the next release of the packages. - **Command line:** the summary of `formfeed ci preview` names the key that rendered: until now it said "test key" also under renders a live key made, which counted units. With the next release of the packages. - **Fixed:** the help of the MCP server, the n8n credential and the Zapier connection said a test key always adds a watermark; only the Free plan marks test renders. The n8n and Make connections no longer mention a self-hosted gateway, which does not exist. ## 18 October 2026: the paged preview breaks tables as the PDF does {#2026-10-18-paged-tables} - **Editor:** a table that runs over several pages gets its header row again at the top of every page in the **Paged** preview, and its `tfoot` at the foot of every page it goes on from, with room kept for it, as Chromium prints them. Until now the preview left both out: page 2 of an invoice had no header row, its rows moved by one a page against the PDF, and at 120 line items the preview showed five pages where the PDF has six. A header row with no row under it at the foot of a page now moves on with its table, a row taller than the rest of the page breaks under the repeated header instead of moving on, and a `position: fixed` element stands where Chromium prints it, inside the page margins. The 21 tables of [Page breaks in tables](https://formfeed.dev/blog/page-breaks-in-tables) come out page for page as their PDFs do, and a test holds them there. - **Command line:** `formfeed dev` shows the same with the next release of the packages. ## 18 October 2026: renders without the half-second wait {#2026-10-18-load-wait} - **Changed:** a document is printed as soon as it has loaded, none of its requests is running and its fonts are ready. Until now every render also waited until the network had been idle for half a second, which for a simple document was most of its render time. A `url` render and a template with a script of its own, an event handler attribute or a frame still wait for that half second, because their code may start a request later; the scripts Formfeed adds for charts and fitted text do not count. This is the new default of [`waitFor.networkIdle`](templates/settings#waiting-for-content), `"auto"`; `true` keeps the old wait for a template that needs it, `false` stays as it was. - **Template engine:** the scripts `@formfeed/engine` writes into a document carry `data-formfeed`, as its styles do. A `formfeed test` snapshot of a template with fitted text (`data-ff-fit`) changes by that attribute; approve it again with `formfeed test -u`. ## 18 October 2026: file ids as sources, zips that expire, the webhook endpoints of a plan {#2026-10-18-review-fixes} - **Fixed:** a library file's id (`fil_…`) now works as a merge source (`post.merge_after`, `/pdf/merge`) and as a watermark `image`, as the SDKs' types always said. Until now only the file's name did, and an id answered [`422 source_not_found`](errors/source-not-found). See [PDF tools](api/pdf-tools#sources). - **Changed:** the zip of a batch is deleted when the first file in it expires, so no document is kept in a zip longer than its own retention. Until now a zip stayed until its workspace was deleted. Zips made earlier follow the same rule, and one whose first file has expired already is deleted now. See [Retention and deletion](api/data-residency#retention-and-deletion). - **Changed:** the number of [webhook endpoints](api/webhooks#register-an-endpoint) a plan includes is enforced: one on Free, five on Starter, any number from Pro, counted per organisation. One more answers [`403 forbidden`](errors/forbidden) with `feature: "webhooks"` and the `limit`; endpoints that exist beyond the number stay. A switched-on trigger of a no-code tool is an endpoint too, a `formfeed listen` session is not. - **Changed:** the Enterprise plan's largest output is 250 MB, the size every render is held to; the plan said 1,000 MB, which no render could reach. ## 15 October 2026: one search box on Ctrl+K {#2026-10-15-search-box} - **App:** `Ctrl+K` (`⌘K` on a Mac) finds your templates, the examples, shared partials, files, the app's pages and the sections of settings, billing and brand, and the documentation's main pages, beside the commands of the page you are on. The part of each result that matched is marked, words may come in any order and umlauts need not be typed. `#`, `>`, `/` and `?` narrow the search to templates, commands, pages or help; a pasted `rnd_…` or `tpl_…` id opens that render or template. See the [editor tour](getting-started/editor-tour#keyboard-shortcuts). ## 12 October 2026: packages 0.3.17 {#2026-10-12-packages-0-3-17} - **n8n node:** `n8n-nodes-formfeed` 0.3.17 brings _Render → Create E-Invoice_ (see [9 October](#2026-10-09-einvoice-connectors)). Its README walks through a workflow step by step: an invoice rendered from the gallery's example and sent by email, and what the trigger hands a workflow when a render finishes. - **MCP server:** `@formfeed/mcp` 0.3.17 brings `check_invoice` and the `einvoice` option of `render`. With `--http` it also answers the server card at `/mcp/server-card`, as the hosted endpoint does. - **Template engine and command line:** `@formfeed/engine` and `formfeed` check an invoice between two French businesses by France's rules (see [9 October](#2026-10-09-french-einvoices)), and the engine exports the list of business terms as `EN16931_TERMS`. - **SDKs:** the types of `@formfeed/sdk` and the Python package `formfeed` know the report's new fields `id`, `versions`, `xml_sha256` and `pdf_sha256` (see [9 October](#2026-10-09-einvoice-proof)). ## 11 October 2026: a GitHub Action for templates {#2026-10-11-github-action} - **GitHub Action:** [Formfeed templates](https://github.com/marketplace/actions/formfeed-templates) in GitHub's Marketplace runs the command line in a workflow: `validate` and `test` on every run, the templates a pull request changes rendered for review with the summary as one comment that each run updates, and publishing from the default branch. Use it as `formfeed-dev/formfeed-action@v1`. See [GitHub Actions](integrations/github-actions). ## 11 October 2026: e-invoices from WooCommerce {#2026-10-11-woocommerce-einvoices} - **Docs:** [WooCommerce](integrations/woocommerce) turns every completed order into a ZUGFeRD / Factur-X e-invoice: a function that maps the order of WooCommerce's REST API to the `_invoice` block, with products, shipping and fees as lines, coupons as allowances and the VAT of each rate as the order states it; the configuration it needs (who you are, where your VAT number plugin keeps the buyer's VAT ID, the category of a rate of 0 % and of a customer WooCommerce exempts from VAT, when an unpaid invoice is due); a webhook handler that checks WooCommerce's signature and makes each order once; and the same in n8n. Like the Stripe recipe, the page's code runs against orders shaped as WooCommerce's API returns them and through the check the API runs before it renders an e-invoice. ## 11 October 2026: e-invoices from Stripe {#2026-10-11-stripe-einvoices} - **Docs:** [Stripe](integrations/stripe) turns every invoice Stripe finalises into a ZUGFeRD / Factur-X e-invoice: a function that maps Stripe's invoice to the `_invoice` block, the configuration it needs (who you are, and which VAT category Stripe's taxability reasons and tax rates are), a webhook handler for `invoice.finalized` and the same in n8n. The function copies Stripe's amounts, keeps discounts as allowances and credits as negative quantities, and refuses what it cannot map, such as shipping costs or a reason without a category, rather than guessing. The page's code is run against invoices shaped as Stripe's API returns them and through the check the API runs before it renders an e-invoice. ## 10 October 2026: test e-invoices carry the test mark on every plan {#2026-10-10-paid-test-einvoices} - **Changed:** a test render that makes an [e-invoice](templates/e-invoices#plans-and-units) carries the test mark on every plan, and the paid plans count them per organisation and day as Free does: up to 100 a day on Starter, 500 on Pro and 2,000 on Business, a thirtieth of the plan's monthly units; Enterprise by contract. Until now a paid plan's test e-invoices were unmarked and uncounted, valid invoices that cost no unit. Once the day is used up, a test e-invoice answers `403 forbidden` with `limit`, `used` and `resets_at`. The editor's true render of an invoice template and **Send test request** in its API tab are test renders too. Test renders of other documents stay unmarked on the paid plans, and live renders are neither counted nor marked. See [Limits and usage](api/limits-and-usage#plan-limits). ## 10 October 2026: e-invoices from Zapier {#2026-10-10-einvoice-zapier} - **Zapier:** the new action **Create E-Invoice** makes a ZUGFeRD / Factur-X e-invoice from fields for the invoice itself, grouped as invoice, seller, buyer, lines and VAT, totals and payment. Lines and the VAT breakdown are line items, so the line items of a trigger map line by line. A refused invoice names every field and rule in the Zap's error, and the output carries the validator's report. It comes with version 0.2.0 of the Zapier app. See [Zapier](integrations/zapier#einvoice). ## 9 October 2026: e-invoices from n8n and from AI agents {#2026-10-09-einvoice-connectors} - **n8n node:** _Render → Create E-Invoice_ makes a ZUGFeRD / Factur-X e-invoice from the fields of the invoice (seller, buyer, lines, VAT breakdown, totals, payment), which the node sends as the `_invoice` block to a template that prints it, such as the E-invoice example of the gallery. What the fields do not offer goes into a JSON field, and the node's output carries the validator's report. See [n8n](integrations/n8n#einvoice). - **MCP server:** the new tool `check_invoice` checks an `_invoice` block offline, as the API does before it renders one, and `render` takes `einvoice` to make the PDF an e-invoice. A refused invoice comes back as a tool error that names every field and rule, so the agent can correct the block. See [MCP server](integrations/mcp). - The hosted endpoint `mcp.formfeed.dev` has both now; `@formfeed/mcp` and `n8n-nodes-formfeed` bring them with the next package release. ## 9 October 2026: the e-invoice report names what checked which file {#2026-10-09-einvoice-proof} - **API:** an e-invoice's report, `einvoice.validation`, also carries the validator's `id` for the report, the `versions` of its parts (Mustang, veraPDF, PDFBox) and the SHA-256 of the XML and of the PDF it checked, `xml_sha256` and `pdf_sha256`. For a render that succeeded, `pdf_sha256` is the delivered file and equals the render's `output_sha256`. Reports of earlier renders stay as they were. See [What comes back](templates/e-invoices#response). - **Docs:** [E-invoice coverage](templates/e-invoice-coverage) lists the 129 of the 164 business terms of EN 16931 that the `_invoice` block carries, with the field each comes from, and the versions of the validator and its rules, generated from the code that runs. [E-invoices by country](templates/e-invoice-countries) says which countries ask for what, with a source for each, and where Formfeed's part ends. - **SDKs:** the types of `@formfeed/sdk` and the Python package `formfeed` know the new fields from the next package release, and `@formfeed/engine` exports the list of business terms as `EN16931_TERMS`. ## 9 October 2026: invoices between French businesses follow France's rules {#2026-10-09-french-einvoices} - **Fixed:** an [e-invoice between two French businesses](templates/e-invoices#french-invoices) in the profile `basic` is refused with `422 einvoice_data_invalid`. France's reform takes the profiles EN 16931 and EXTENDED-CTC-FR only (AFNOR XP Z12-012), so such a file was valid by the European rules but not one a French platform accepts. `en16931`, the default, is unchanged. - **Fixed:** `operation_code` takes every billing framework the French rules list. `S3` (public buyers) and the frameworks of invoices with several sellers, `B8`, `S8` and `M8`, are no longer refused. - **Changed:** such an invoice also needs the three statutory notes (`PMT`, `PMD`, `AAB`) and the electronic addresses of seller and buyer, which France's rules call mandatory (`BR-FR-05`, `BR-FR-12`, `BR-FR-13`). Until now the validator only warned about a missing one and the file was delivered; now the request is refused before rendering with `422 einvoice_data_invalid`. - `formfeed validate`, `formfeed einvoice` and `@formfeed/engine` check the same from the next package release. ## 9 October 2026: packages 0.3.16 {#2026-10-09-packages-0-3-16} - All packages move to 0.3.16 without changes of their own. The release carries a copy of the n8n node's credential file at the root of the [public repository](https://github.com/formfeed-dev/formfeed), where n8n's verification of community nodes looks for it. ## 9 October 2026: packages 0.3.15 {#2026-10-09-packages-0-3-15} - **Template engine:** `@formfeed/engine` 0.3.15 takes only text as a template's source (body, header, footer, PDF title and partials) and requires Handlebars 4.7.10. Handlebars compiles an object as an already parsed syntax tree, and up to 4.7.9 values of such a tree could run as code ([GHSA-8r5x-fm3f-whwj](https://github.com/advisories/GHSA-8r5x-fm3f-whwj)). - **n8n node:** `n8n-nodes-formfeed` 0.3.15 follows n8n's rules for verified community nodes: option lists in alphabetical order, _Get Many_ returns 50 results when no limit is set (was 25), the default release channel is labelled _Published_, and the node and its credential carry icons for light and dark themes. - `formfeed` (CLI and Python), `@formfeed/sdk`, `@formfeed/devkit`, `@formfeed/testing`, `@formfeed/mcp` and the Activepieces piece move to 0.3.15; the CLI and the MCP server bundle the engine and bring its change. ## 8 October 2026: retention is the plan's limit, and Tailwind for plain HTML {#2026-10-08-retention-tailwind} - **Fixed:** an output is no longer kept for longer than the plan's [retention](api/rendering#download-links-access-and-retention). `expires_in` and a workspace's own retention were meant to shorten it, but a longer value simply won, so a request could ask for a year on a plan that keeps 30 days. A longer `expires_in` is now cut to the retention, and a longer period in the workspace's settings counts as the plan's. Outputs rendered before this change keep the expiry they were given, and plans without a limit (Business and above) are not affected. - **Fixed:** `"tailwind": true` in the settings compiles the Tailwind classes of ad-hoc `html` without an `engine` too. Until now only HTML sent with an engine got the stylesheet, and plain HTML was printed unstyled. A complete document gets the stylesheet at the front of its head, before its own styles. ## 7 October 2026: packages 0.3.14 {#2026-10-07-packages-0-3-14} - **SDK:** `@formfeed/sdk` 0.3.14 calls `fetch` the way browsers and Cloudflare Workers require. Until now a client on those runtimes failed every request with `Illegal invocation` unless a `fetch` was passed in; Node was never affected. - **MCP server:** `@formfeed/mcp` 0.3.14 adds the tool `get_workspace`, and its hosted endpoint `https://mcp.formfeed.dev/mcp` takes a [sign-in](integrations/mcp#oauth) beside the API key, the workspace chosen at consent. The self-hosted `--http` server takes the sign-in on `/mcp` too once `--auth-server` is set. - `formfeed` (CLI and Python), `@formfeed/engine`, `@formfeed/devkit`, `@formfeed/testing`, the n8n node and the Activepieces piece move to 0.3.14 without changes of their own. ## 7 October 2026: the hosted MCP server answers tool calls {#2026-10-07-mcp-tool-calls} - **Fixed:** every tool call on `mcp.formfeed.dev` failed with `network_error: Illegal invocation`, with an API key as with a sign-in, since the hosted endpoint went live on 17 September. The TypeScript SDK, which the endpoint is built on, called the global `fetch` as a method of its client, which Node accepts and browsers and Cloudflare Workers refuse. The endpoint answers since the deploy of this day. - **SDK:** the same fault affected `@formfeed/sdk` in a browser or a Cloudflare Worker whenever no `fetch` was passed in. Fixed in the sources; the next package release carries it. Until then, pass `fetch: (input, init) => fetch(input, init)` on those runtimes. ## 7 October 2026: one MCP address, the workspace chosen at consent {#2026-10-07-mcp-fixed-address} - **MCP server:** `https://mcp.formfeed.dev/mcp` takes a [sign-in](integrations/mcp#oauth) as well as an API key. A chat client opens the Formfeed sign-in itself, and the consent page asks which workspace the agent works in; the choice travels in the agent's token, and the API checks the membership on every call as before. `https://mcp.formfeed.dev/mcp/` still works and pins another workspace for a second connection. - **MCP server:** the new tool `get_workspace` answers the workspace, organisation, plan, environment and units of the period, as `GET /account` does, so an agent can say where a document lands and how many renders are left. An agent's token may now read the account (`account:read`) and nothing more than before. - **App:** **Account → Connected agents** lists every agent you allowed, with its workspace to change and a revoke that signs the agent out. A client allowed before this change is asked for its workspace the next time it connects. - **Changed:** `POST /templates/{id}/validate` and the other `/v1` routes answer `403` with the reason for an agent whose person chose no workspace yet, where they answered `400` for the missing `X-Formfeed-Workspace` header. ## 5 October 2026: packages 0.3.13 {#2026-10-05-packages-0-3-13} - **CLI:** `formfeed` 0.3.13 works with [accessible PDFs](templates/accessible-pdf). For a template whose `settings.json` declares `pdf.ua`, `formfeed validate` reports what the source already says about the file, such as a missing title or an image written without `alt`: as errors where the check is strict, as warnings where it reports. `formfeed render` prints the validator's verdict, and a strict render whose file fails is a finding with exit code 1. `formfeed dev` checks the page it previews. - **Engine:** `@formfeed/engine` 0.3.13 exports the static template check that `formfeed validate` and the API's validation run: `staticAudit` over a template's sources, the findings with the rules each one fails (`AUDIT_CODES`, `AUDIT_RULES`) and `resolveUa` for the setting. `chart` takes an `alt` in its options, which a chart needs under PDF/UA-1. Checking the finished file against the standard happens in the API, not in the package. - **Dev kit:** `@formfeed/devkit` 0.3.13 exports `diagnoseAccessibility`, which the CLI builds on. - **SDKs:** `@formfeed/sdk` and the Python package `formfeed` 0.3.13 know the `accessibility` report on a render: `Accessibility`, `AccessibilityFailure` and `AccessibilityWarning` in both, and `PdfUaOptions` for the setting in TypeScript. - **MCP server:** `@formfeed/mcp` 0.3.13 hands the report on with a render, and tells an agent which rules a strict render failed and in which elements. - `@formfeed/testing`, the n8n node and the Activepieces piece move to 0.3.13 without changes of their own. ## 5 October 2026: the Power Automate connector is certified {#2026-10-05-power-automate} - **Power Automate:** Microsoft has certified the Formfeed connector and released it on 5 October. It works in Power Automate, Power Apps, Logic Apps and Copilot Studio, as a premium connector marked Preview like every newly certified one. Microsoft now rolls it out region by region, which takes about two weeks; until it appears in your environment, the HTTP action makes the same call. See [Power Automate](integrations/power-automate). ## 5 October 2026: e-invoices that are PDF/UA-1 files too {#2026-10-05-einvoice-pdfua} - **API:** `settings.pdf.ua` and an [e-invoice](templates/e-invoices#pdf-ua) now go together. The render returns one file that is the ZUGFeRD / Factur-X invoice, a PDF/A-3b, and a PDF/UA-1 document. Both are checked in the file that is delivered, and the render carries `einvoice` and `accessibility`. Until now the pair answered `400 invalid_request`. - **API:** a strict render whose e-invoice is valid and whose file fails PDF/UA-1 answers `422 pdfua_validation_failed` with both reports and delivers nothing. With `{ "check": "report" }` the valid e-invoice is delivered without the PDF/UA identifier. `einvoice_display_mismatch` carries `accessibility` too where the render set `pdf.ua`. - **Changed:** `post.watermark.text` is allowed on an e-invoice. It answered `400 invalid_request` while the watermark's font was not embedded. - **Changed:** the mark on a test e-invoice is text in the embedded font, as on every other test render. For a few days it was a picture. - **Editor:** **Accessible PDF (PDF/UA-1)** and **E-invoice** no longer lock each other in the page setup, and a template that declares both is no longer reported as a problem. - **CLI:** `formfeed validate` and [`POST /templates/{id}/validate`](api/templates) no longer report `accessibility-einvoice` for a template that declares both. In the CLI from [0.3.13](#2026-10-05-packages-0-3-13), the first release with the check. ## 5 October 2026: accessible PDFs (PDF/UA-1) {#2026-10-05-accessible-pdf} - **API:** `settings.pdf.ua` makes a render a tagged PDF in the PDF/UA-1 format. Formfeed repairs what the browser's tagged output gets wrong, checks every file against the machine-checkable rules of the standard and reports the verdict in the render's new field `accessibility`: whether the file passed, and for each rule that failed what to change in the template and in which elements. `true` is the strict check, which fails a render whose file does not pass with the new problem [`pdfua_validation_failed`](errors/pdfua-validation-failed); `{ "check": "report" }` delivers the file and reports. It is part of every plan and costs no extra unit. A passed check does not make a document accessible: [Accessible PDFs](templates/accessible-pdf) says what it proves and what stays with the template. - **Editor:** the page setup has a **Document** section with the title, author, subject and keywords of the PDF, its bookmarks and tags, and **Accessible PDF (PDF/UA-1)**. With that on, the previews check the page and the **Problems** list names what they find at its line, and a **True render** and the render log show the validator's verdict. - **API:** [`POST /templates/{id}/validate`](api/templates) reports a missing title and an image written without `alt` for a version that declares `pdf.ua`. - **CLI:** `formfeed validate` reports the same for a template folder, `formfeed render` prints the verdict, and `formfeed dev` checks the page it previews. The SDKs name the new field, and the MCP server hands it on. All of these come with the [packages 0.3.13](#2026-10-05-packages-0-3-13). - **Changed:** a tagged PDF (`pdf.tagged`, `pdf.outline`) gets the same repairs whether or not it asks for PDF/UA: page headers, footers and decoration are marked as artifacts, and links carry a description. - **Changed:** a text watermark and the mark on test renders are set in Noto Sans, embedded in the file, where they were set in Helvetica. A text watermark now takes Greek and Cyrillic letters too. ## 5 October 2026: test e-invoices on the Free plan {#2026-10-05-einvoice-test} - **API:** on the Free plan, test renders now make [e-invoices](templates/e-invoices#plans-and-units): up to 20 a day per organisation, counted from midnight UTC, each a valid ZUGFeRD / Factur-X file with the test mark on its pages. Until now a Free workspace was refused even with a test key. A live render that makes one is still refused with [`403 forbidden`](errors/forbidden) and `"feature": "einvoice"`. - **Errors:** a test render beyond the day's 20 answers the same `403 forbidden` with `"feature": "einvoice"`, now with `limit`, `used` and `resets_at`, the start of the next UTC day. In a batch every item that makes an e-invoice counts, and nothing is queued when they do not all fit. - **Editor:** on Free the **E-invoice** section of the page setup works as on a paid plan and says what a test render is. The true render of an invoice template is one of the day's 20, and so is **Send test request** in the API tab. ## 5 October 2026: packages 0.3.12 {#2026-10-05-packages} - **CLI:** `formfeed` 0.3.12 works with [e-invoices](templates/e-invoices#command-line) offline. `formfeed validate` holds the `_invoice` block of every data set to the rules, for each template whose `settings.json` declares an e-invoice, and names the file, the line, the field and the rule. The new `formfeed einvoice ` writes the XML a render would embed, without an API key. `formfeed render` sends what the template declares and prints the report of the render. - **Engine:** `@formfeed/engine` 0.3.12 exports the e-invoice code the API runs before a render: the schema of the `_invoice` block (`invoiceSchema`), the check that names the rule each finding breaks (`checkInvoice`), the writer of the XML (`invoiceToCii`, `einvoiceXml`) and the check that a page shows what the XML states (`displayCheck`). The conversion to PDF/A-3 and the validation of the finished file happen in the API, not in the package. The package gains `zod` as a dependency. - **Dev kit:** `@formfeed/devkit` 0.3.12 exports what the CLI builds on: `declaredEinvoice`, `diagnoseEinvoice` and `localEinvoiceXml`. - **SDKs:** `@formfeed/sdk` and the Python package `formfeed` 0.3.12 know `post.einvoice` on a render and a batch and the `einvoice` report on a render: `EinvoiceOptions`, `Einvoice` and `EinvoiceMessage` in TypeScript, `Einvoice`, `EinvoiceValidation`, `EinvoiceDisplay` and `EinvoiceMessage` in Python. - `@formfeed/testing`, the MCP server, the n8n node and the Activepieces piece move to 0.3.12 without changes of their own. ## 4 October 2026: e-invoices {#2026-10-04-e-invoices} - **API:** a PDF render can be an [e-invoice](templates/e-invoices) in the ZUGFeRD / Factur-X format: a PDF/A-3b that carries the invoice as `factur-x.xml`, in the profiles EN 16931 and Basic, from an HTML or a Word template or from HTML sent with the request. Ask for it with `post.einvoice` on a render or a batch, or declare it once in the template's `settings.einvoice`. The invoice is the `_invoice` block of `data`, and the template prints from the same block. - **Validation:** the block is held to the rules before anything renders, and the finished file is validated before it is delivered. A render that succeeds carries the report in the new `einvoice` field of the [render](api/rendering#the-render-object), which webhook events repeat. With `xml: "both"` the XML is also stored beside the PDF and linked as `einvoice.xml_url`. A file that fails is not delivered, and no plain PDF is delivered in its place. - **Errors:** five new codes: [`einvoice_data_invalid`](errors/einvoice-data-invalid), [`einvoice_profile_unsupported`](errors/einvoice-profile-unsupported), [`einvoice_display_mismatch`](errors/einvoice-display-mismatch), [`pdfa_conversion_failed`](errors/pdfa-conversion-failed) and [`conformance_unavailable`](errors/conformance-unavailable). - **Plans:** e-invoices need the Starter plan or higher and cost 1 unit on top of the render. Test keys, previews and failed renders cost nothing. On Free the API answers [`403 forbidden`](errors/forbidden) with `"feature": "einvoice"`. See [plans and units](templates/e-invoices#plans-and-units). - **Editor:** the **Page setup** of a PDF template has an **E-invoice** section with the switch, the format and the profile, and a card that says what the data set's `_invoice` block lacks, each finding with its rule. The preview counts the values of the XML it shows, and a true render and the render log show the validator's report, which downloads as JSON. ## 2 October 2026: pictures to choose from on the canvas {#2026-10-02-image-picker} - **Editor:** on the [canvas](templates/visual-editor), an image layer that takes its picture from the file library named the file in a text field, so you had to know what it was called. **File** in the inspector now opens the pictures of the workspace's [file library](api/files), newest first, with a search by name. The picture in use is marked, and **Upload an image** in the same list adds a file and shows it on the layer at once. A long file name is cut short in the field and shown in full when you point at it. - **Fixed:** an image layer took a PDF, through its upload or when one was dropped on the canvas, because the library holds PDFs too. It was uploaded and then stood on the canvas as a broken image. An image layer now takes pictures only, and says which file is none. - **Fixed:** the search of the **Files** page read `_` and `%` in what you typed as wildcards, so `hero_2x` also found `hero-2x.png`. Both now stand for themselves. ## 2 October 2026: older versions, and pages that wait for their workspace {#2026-10-02-history-loading} - **Editor:** the **Versions** panel showed the latest fifty versions of a template and had no way to the ones before, so a template saved more often could not compare with or restore its early versions in the app. **Show older versions** under the list brings them, fifty at a time, and the line above says `50+ saved` while there are more. - **Fixed:** on a slow connection, pages said things about a workspace they did not know yet: the render log read "No renders yet", the keys and webhooks pages "ask an owner", and the billing page showed the Free plan as the current one, until the organisation had loaded. No page of a workspace is shown before its workspace is known now, and the render log no longer reads "No renders match the filters" while its first answer is on the way. - **Fixed:** an address whose workspace does not exist (a workspace that was deleted, a mistyped link) showed an empty app. It leads to the organisation's first workspace, or to the list of organisations. - **Fixed:** with **Edit text** on, a click on a text in the instant the preview took its next document was lost, or opened the text only to close it again. The text now opens in the new document without a second click. ## 2 October 2026: fixes in the editor and on the templates page {#2026-10-02-editor-fixes} - **Fixed:** with **Edit text** on, a text clicked within a moment of the edit before it (Enter, then the next text) lost its cursor as soon as the preview showed the first edit, and what was typed into it was gone. The preview now waits with that render until the text is done. - **Fixed:** on the [canvas](templates/visual-editor), a click into the inspector's text field in the instant a text opened for typing lost the focus to the canvas, and what was typed for the field landed in the text on the canvas. The focus now stays where you put it. - **Fixed:** **True render** failed with "all render slots are busy" while the renderer was at its limit. It now waits for a free slot, for up to 20 seconds. - **Fixed:** on a slow connection, the templates page read "No templates yet" until the list of a workspace had arrived, and **Create template** in the **New template** dialog did nothing when it was pressed before the workspace had loaded. The page now shows the list as loading, and the button is available once the workspace is there. ## 2 October 2026: the API panel of a template {#2026-10-02-api-tab} - **Editor:** every template has an **API** panel: the request that renders it in curl, TypeScript, Python, PHP, Go, C#, Java and Ruby, with the template's slug and its data set as `data.json`. The first line says which version a call renders, and a switch adds the latest draft to the request where one is ahead of the published version. See the [editor tour](getting-started/editor-tour#api-tab). - **Editor:** owners and admins create a test key from the panel, shown once as the `export` line a shell needs. **Send test request** sends the request through the API as a test render and shows the answer: the file, the render in the render log and the JSON, or the problem with the page of its code. It is free on every plan. - **App:** `?panel=api` on a template's address opens the panel, as do **API request** in a row's menu of the templates list and the last step of the getting-started checklist. After **Use this template**, the editor's notice leads there instead of to the example's page, since the panel names the copy's own slug. - **API:** the problem `POST /v1/renders` answers with when a render fails now carries the render's `render_id`, as the problem of its [result](api/async-and-batch) already did. See [Errors](api/errors). ## 2 October 2026: a render by the template's id {#2026-10-02-template-id} - **Fixed:** `POST /v1/renders` and `POST /v1/renders/batch` answered `404 template_not_found` when `template` was the `tpl_…` id that the [Templates API](api/templates) returns. They knew the slug and the plain UUID only. The slug and the `tpl_…` id now both work, as the [reference](api/rendering) has always said. - **Fixed:** a render named its template by the database's UUID in `template.id`, in API answers and in webhook events, where the Templates API hands out `tpl_…` and these pages have always shown `tpl_…`. Renders and events now carry the `tpl_…` id, so one template has one id. A UUID you stored keeps working wherever a template is named: a render request, the `template` filter of `GET /v1/renders` and the Templates API take the slug, the `tpl_…` id or the UUID. ## 2 October 2026: packages 0.3.11 {#2026-10-02-packages} - **n8n node:** `n8n-nodes-formfeed` 0.3.11 can be connected to the **Tools** input of n8n's AI Agent node, which then picks the operation and fills in its fields. A self-hosted n8n offers community nodes as tools only when the instance sets `N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true`. See [n8n](integrations/n8n#as-a-tool-for-an-ai-agent). - **CLI:** `formfeed` 0.3.11 keeps a design of the [visual editor](templates/visual-editor) with its template: `templates pull` writes it to `design.json`, `push` sends it back, and `validate` warns with `design-stale` when `template.html` or `style.css` were edited after it, and with `design-invalid` when the file is not a design the app can open. See the [project layout](devkit/project-layout). - **Engine:** `@formfeed/engine` 0.3.11 renders locally what the API has rendered since the end of September. Text marked `data-ff-fit="shrink"` is [fitted to its box](templates/writing-templates#fit-text); `barcode` prints the digits of an EAN-13, EAN-8 or UPC-A beside the long guard bars instead of through them; and `qrcode`, `qr` and `barcode` answer an empty value with a message that names the helper ("nothing to encode, the value is empty or missing from the data") instead of the code library's own. The package also exports the design format (`parseDesign`, `emitDesign`). - **Snapshots:** because `formfeed test` and `@formfeed/testing` compare the document the engine assembles, a snapshot that holds one of those three barcodes or a fitted text changes once with 0.3.11. Look at the difference and approve it with `formfeed test -u`. - The SDKs, the MCP server and the Activepieces piece move to 0.3.11 without changes of their own. ## 2 October 2026: the canvas after its first testers {#2026-10-02-canvas} - **Editor:** a text added on the [canvas](templates/visual-editor) opens for typing at once with its placeholder selected, a selected text takes what you type, and a click on it places the cursor. A round handle under the selection moves the layer, also while its text is being typed into. - **Editor:** bold, italics, underline and the new strikethrough are toggles with the usual keys (Ctrl+B, Ctrl+I, Ctrl+U), the weights are listed by name, and a new text starts regular. Every layer has a **Shadow** in the inspector: behind the letters of a text, behind the box of any other layer. - **Editor:** a picture on the clipboard pastes onto the canvas with Ctrl+V (it is uploaded to the [file library](api/files) first), and **Upload your own fonts** under the font list adds font files to the organisation without leaving the editor. - **Fixed:** the add bar's image button closed the canvas with "The design cannot be opened". A new image layer now waits for its picture: the canvas and the Problems list say so, and it renders as an empty box until one is chosen. - **API:** in `settings.design`, a text layer's `style` may carry `strike: true`, and an image layer's `source` may hold an empty `asset`, `url` or `path` for a picture not chosen yet. ## 2 October 2026: start from an example, and the template limit {#2026-10-02-examples} - **App:** the templates of [formfeed.dev/examples](https://formfeed.dev/examples) are in the app. **Templates → Examples** lists them with a search and filters for group, kind and engine, and **Use this template** puts a copy into the workspace: the example's settings, translations and sample data, published as version 1, so a render that names the copy's slug works at once. Every example page of the site has the same button, which leads through sign-in to that example. See the [quickstart](getting-started/quickstart). - **API:** the number of templates a plan includes is now enforced: five on Free, no limit on Starter and above. `POST /v1/templates` beyond it answers [`403 forbidden`](errors/forbidden) with `feature: "templates"` and the `limit`. Archived templates do not count, and restoring one is refused while the plan is full. Templates an organisation already has stay as they are, and renders are not affected. ## 30 September 2026: text edited in the preview, and a canvas for image templates {#2026-09-30-visual-editing} - **Editor:** with **Edit text** on, a click on a heading, a paragraph or a translated label in the flow or paged preview edits it where it is. Values from the data stay chips, the change goes into the template (or into the dictionary of the language the preview shows) as one edit that Ctrl+Z undoes, and text the preview cannot edit safely, such as control flow, is shown in the code instead. See the [editor tour](getting-started/editor-tour#edit-text). - **Editor:** image templates can be designed on a canvas, in beta: text, images, shapes, QR codes and barcodes as layers, text bound to fields of the data, brand colours and fonts, text that shrinks to fit its box, rulers and guides to snap to, and a check of every data set for text that does not fit. The editor writes the template's HTML and CSS from the design, so renders, the API and the SDKs see an ordinary template. See [the visual editor](templates/visual-editor). - **Templates:** `data-ff-fit="shrink"` gives the text of a fixed-size box the largest font size at which it fits, in any template: see [fit text to its box](templates/writing-templates#fit-text). - **API:** a version's `settings.design` holds the canvas's layers ([settings](templates/settings)); it may be at most 256 kB, and renders ignore it. - **CLI:** from the next release, `formfeed templates pull` writes a design to `design.json`, `push` sends it back, and `formfeed validate` warns when `template.html` or `style.css` no longer match it. ## 29 September 2026: deduplication that knows languages and translations {#2026-09-29-dedup} - **Fixed:** two live renders of the same template and data in different languages (`"locale": "de"`, then `"locale": "en"`) within the [deduplication](api/rendering#deduplication) window answered the second with the first one's document, in the first language. The locale is now part of what makes two renders identical; renders without a `locale` are deduplicated as before. - **Fixed:** a template version that changed only its translations had the same checksum as the version before it, so live renders with the same data could get the old wording from the deduplication cache for up to 24 hours after it was published, and `template_checksum` could not tell the two versions apart. Versions saved from now on include their translations in the checksum; the checksums of existing versions do not change. ## 28 September 2026: the result of a render {#2026-09-28-render-result} - **API:** the new [`GET /v1/renders/{id}/result`](api/reference/get-render-result) answers `202` with `Retry-After` while a render is queued or rendering. Once the render has finished, it answers what the synchronous request would have: the render, the file with `?output=binary`, or the render's error as a problem with its `render_id`. The `Location` of every `202` from `POST /v1/renders` now names this URL, absolute and with `output=binary` when the request asked for the file, and the `202` carries `Retry-After: 5`. `GET /v1/renders/{id}` is unchanged; code that polls it, like the SDKs' `waitFor`, needs no change. See [the automatic fallback](api/async-and-batch#the-automatic-fallback). - **Power Automate:** because Power Automate follows that `Location` by itself, an action whose render outlives the one-minute sync deadline now waits for the document instead of returning an unfinished render. ## 25 September 2026: packages 0.3.10 {#2026-09-25-packages} - **MCP server:** the six tools of `@formfeed/mcp` 0.3.10 describe when to use each one rather than another, what a call costs and returns, and what goes wrong, and every parameter says what it is for. `render` states that it renders a template's published version, so a template that was never published fails with `template_not_found`, and that test keys carry the watermark only on the Free plan. The hosted server at `mcp.formfeed.dev` has the new descriptions already. The SDKs, the CLI, `@formfeed/engine`, `@formfeed/testing`, the n8n node and the Activepieces piece move to 0.3.10 unchanged. ## 23 September 2026: packages 0.3.9 {#2026-09-23-packages} - **SDKs:** `@formfeed/sdk` and the Python SDK `formfeed` 0.3.9 carry the `storage` additions listed under [bring your own storage](#2026-09-23-byo-storage). - **CLI:** `formfeed init --from-workspace` also pulls the organisation's shared partials its templates include, so the first `formfeed validate` no longer fails with a missing partial, and that error now names `formfeed partials pull` for projects made before. `formfeed templates diff` compares settings and sample data regardless of key order, so a fresh pull shows no changes. The output names files relative to where the command ran; `--json` keeps absolute paths. - **MCP server:** `@formfeed/mcp` 0.3.9 declares `readOnlyHint`, `destructiveHint` and `openWorldHint` on every tool, so clients can tell the four tools that only read from `render` and `convert_to_pdf`, which create a render. The hosted server at `mcp.formfeed.dev` has them already. - **Activepieces:** the connection text of `@formfeed/activepieces-piece` 0.3.9 says to keep a key's default scopes, because the connection check needs `account:read`, and to add `file:write` and `webhook:manage`. `@formfeed/engine`, `@formfeed/testing` and the n8n node move to 0.3.9 unchanged. ## 23 September 2026: bring your own storage {#2026-09-23-byo-storage} - **Bring your own storage (Pro plan and above):** owners and admins connect an Amazon S3, Cloudflare R2 or S3-compatible bucket (Hetzner, IONOS, Scaleway, Wasabi, Backblaze B2, MinIO, Google Cloud Storage) on the new **Your own storage** card under **Settings**. Formfeed writes a test object before it saves the connection, then copies every document of the workspace into the bucket in the background: renders, batch items and zips, PDF tool results and office renders. It only ever writes (`PutObject`), never reads, lists or deletes, and keeps its own copy for the workspace's retention, so `download_url` works as before. The copy goes wherever your bucket is; see [Data residency](api/data-residency). See [Bring your own storage](api/bring-your-own-storage). - **API:** renders, batch items and the PDF tools, `/pdf/convert` included, take `storage`: `{ "key": "invoices/2026/RE-1001.pdf" }` names the object below the connection's prefix, `false` leaves the document out. A render with its own key is never deduplicated. Renders and jobs have the new field `storage` (`status`, `bucket`, `key`, `url`, `reason`, `error`); a synchronous render reports `pending`. The routes under `/v1/workspaces/{id}/storage` serve the app and take an owner's or admin's session; no API key can manage a connection yet. - **Webhooks:** the events `storage.delivered` and `storage.failed`, opt-in: no endpoint receives them until its event list names them. Permanent errors such as `AccessDenied` fail at once, other errors are retried on the webhook schedule, and five permanent failures in a row pause the connection and email the owners and admins. [Redelivery](api/bring-your-own-storage#redelivery) sends failed copies again without rendering. - **New error codes:** [`storage_not_configured`](errors/storage-not-configured) (409) and [`storage_test_failed`](errors/storage-test-failed) (422). - **SDKs:** `storage` on renders and PDF tools, `Render.storage` and the types `StorageOption`, `StorageDelivery`, `StorageDeliveryEvent` and `WebhookEventType` in `@formfeed/sdk`; the `storage=` keyword, `render.storage` and the models `StorageDelivery`, `StorageDeliveryEvent` and `StorageError` in the Python SDK. With their next release. ## 23 September 2026: Zap templates {#2026-09-23-zap-templates} - **Zapier:** ready-made Zaps for Google Forms, Typeform, Google Sheets, Google Drive, Stripe and Slack are published in [Zapier's directory](https://zapier.com/apps/formfeed/integrations). Starting from one puts the trigger and the Formfeed step in place, so only the two accounts and your own template are left to choose; they are also listed under **Integrations** in the app. See [Zapier](integrations/zapier#example-zaps). ## 22 September 2026: packages 0.3.8 {#2026-09-22-packages} - **Activepieces:** the Formfeed piece is released with the other packages, as `@formfeed/activepieces-piece` 0.3.8 with provenance; install it by that name under **Platform Admin → Setup → Pieces**. See [Activepieces](integrations/activepieces). - **MCP server:** `@formfeed/mcp` 0.3.8 is listed in the official MCP registry as `io.github.formfeed-dev/formfeed`. The other packages move to 0.3.8 unchanged. ## 22 September 2026: Formfeed in Zapier's app directory {#2026-09-22-zapier} - **Zapier:** the Formfeed app is listed in [Zapier's app directory](https://zapier.com/apps/formfeed/integrations); no invite link is needed any more. Its version 0.1.0 brings **Convert Document to PDF**, DOCX and PPTX output of **Create Document** and the **Version** field for [release channels](templates/release-channels) to Zapier. Existing Zaps stay on 0.0.0 until they are moved. See [Zapier](integrations/zapier). ## 22 September 2026: a template schema for Power Automate {#2026-09-22-swagger-schema} - **API:** [`GET /templates/{id}/schema`](api/templates#data-schema) takes `format=swagger` and answers `{ "schema": … }` in the OpenAPI 2.0 dialect that Power Automate reads for dynamic fields: one type per property, no `nullable` or `oneOf`, a label in `x-ms-summary`. Like `format=make` it describes a template without a stored schema by its sample data, and one with neither by an object without properties instead of a 404. - **API:** [`POST /webhooks`](api/webhooks) answers `201` with a `Location` header, the absolute URL of the new endpoint. - **API:** the form fields `landscape` and `single_page_sheets` of a [`POST /pdf/convert`](api/pdf-tools#convert) upload accept `true` and `false` in any case, so `True` from a .NET client works. ## 21 September 2026: charts sized for the page they print on {#2026-09-21-pdf-window} - **Rendering:** a PDF is laid out in a window as wide and as tall as its paper (A4: 794 × 1123 CSS pixels; landscape and custom sizes likewise) instead of a fixed 1240 × 1754. Printing always laid the page out at the paper's width, but scripts run before that and measured the wider window: a chart library that sizes itself from its container drew charts too large for the page, and an imported apitemplate.io report came out a page longer than it did there. Templates without scripts that measure the window print as before; a template whose script reads `window.innerWidth` now reads the paper's width. ## 21 September 2026: an editor that says when the PDF will shrink {#2026-09-21-editor-width} - **Editor:** when an element is wider than the page, Problems says so — by how many millimetres, and to what size Chromium will shrink every page of the PDF to fit it — and jumps to the element's tag. Chromium scales the whole PDF down for one such element, which no preview at 100 % can show. The flow and paged previews measure after every layout. - **Editor in German:** the template's problems (a name the sample data does not have, an unknown helper, a block that is never closed) are listed in German in the German app; syntax errors from the template parsers stay in English. ## 21 September 2026: snapshot tests see header and footer {#2026-09-21-snapshots} - **CLI:** the snapshot `formfeed test` compares for an HTML template now holds the rendered header and footer as well as the document, under `` and `` lines; before, a change to `header.html` or `footer.html` passed unnoticed, because Chromium prints them outside the document. `formfeed renders pull --snapshot` writes the same form. If your templates have a header or footer, the first `formfeed test` after the update fails once: run `formfeed test -u` and commit the snapshots. In `formfeed` 0.3.7; the other packages move to 0.3.7 with it, `@formfeed/engine` with the `args` of its diagnostics and the previews' width measurement. ## 21 September 2026: no NaN on a document, and an MCP check that knows the schema {#2026-09-21-nan} - **Template engine:** `money`, `number` and `numToWords` stop the render with a [`template_runtime_error`](errors/template-runtime) when a calculation handed them `NaN` or infinity — arithmetic on a value that is text (`"19 %"`) or missing — instead of printing `NaN` on the document. A template that printed `NaN` before now fails where it did. Text that is not a number (`"on request"`) and missing values print as before. [`POST /templates/{id}/validate`](api/templates#validate) reports the same error without rendering. - **MCP server:** `validate_template` with a template slug asks [`POST /templates/{id}/validate`](api/templates#validate) for the version a render would use, so the data is checked against the template's stored schema and run once with the template; ad-hoc `html` is still checked offline. In `@formfeed/mcp` 0.3.6; the other packages move to 0.3.6 with it. ## 18 September 2026: two-factor authentication {#2026-09-18-two-factor} - **App:** protect your account with a code from an authenticator app (TOTP): **Two-factor authentication** in the menu under your name. Optional, per person and in every plan; several authenticators can be added, so a second one can stand in for a lost phone. Every way of signing in asks for the code, the password reset included. See [Two-factor authentication](getting-started/two-factor-authentication). - **API:** unchanged for API keys, which never ask for a code. A session token of an account with two-factor authentication is accepted only once its session passed the code, and an agent's OAuth token on the [hosted MCP endpoint](integrations/mcp#oauth) only after the account allowed that agent from such a session; otherwise the API answers `403 forbidden` and says which of the two it is. Turning two-factor authentication on signs connected agents out, so they have to be connected again. ## 18 September 2026: pushes that conflict, and a CLI that shows its progress {#2026-09-18-push-conflict} - **Fixed:** creating a template version with a `base_checksum` that is no longer the latest version (`formfeed templates push` after the template changed in the app or from another machine) never answered; the request hung until the client gave up. It answers `409 conflict` at once, as documented. Pull and merge, or push with `--force`. - **CLI 0.3.5:** `templates push` and `pull`, `partials push`, `files push` and `pull`, `ci preview` and `render` print each result as it is done instead of all at the end, with a closing line that counts them. On a terminal a live line names the template in progress, what is happening to it and for how long, and says so when the API takes unusually long. Output is coloured on a terminal only (`NO_COLOR` and `FORCE_COLOR` are respected); `--json`, pipes and CI logs are unchanged. The other packages move to 0.3.5 with it. ## 17 September 2026: hosted MCP endpoint with sign-in {#2026-09-17-hosted-mcp} - **MCP:** `mcp.formfeed.dev` is live. `https://mcp.formfeed.dev/mcp/` signs in with your Formfeed account (OAuth 2.1; the client opens the browser and you allow it on the new consent page), `https://mcp.formfeed.dev/mcp` takes an API key. A signed-in agent reads templates and renders, and nothing else, whatever your role. See [MCP server](integrations/mcp#hosted). - **Packages 0.3.4:** `@formfeed/mcp --http` takes `--auth-server` and `--public-origin` for the same sign-in against your own Supabase Auth, and `@formfeed/sdk` takes `headers` sent with every request; the other packages move to 0.3.4 with them. ## 17 September 2026: release channels everywhere a version is named {#2026-09-17-channels-everywhere} - **API:** [`POST /templates/{id}/validate`](api/templates#validate) takes a channel name as `version`, and [`GET /templates/{id}/schema`](api/templates#data-schema) reads the schema of the version `?version=` names (`published`, `latest`, a number or a channel). `?format=make` adds a `version` select when the template has named channels. - **SDKs:** `templates.schema(id, { version })` in `@formfeed/sdk` and `templates.schema(id, version=...)` in Python; `validate` takes a channel name. With their next release. - **n8n, Zapier, Make:** a **Version** field that lists the template's [release channels](templates/release-channels). n8n also takes a version number as an expression; Zapier and Make show the field only for templates with named channels. Zapier and Make get it with the app's next version. - **App:** the templates list has a **Channels** column with each template's named channels. ## 17 September 2026: parallel sync renders {#2026-09-17-parallel-sync} - **Fixed:** synchronous renders sent in parallel beyond the free render slots answered `503 region_unavailable` and stayed _Queued_ or _Rendering_ in the render log for good. They now wait for a slot, as documented, and one that finds none within 30 seconds becomes an async render (`202`, `X-Render-Async-Fallback: true`) that finishes in the background. See [Rendering](api/rendering). - **apitemplate.io compatibility:** the endpoints wait for such a render and answer with the document (before, a render that went to the background answered `success` without a `download_url`), pass `Retry-After` on, and answer `504 render_timeout` with the `transaction_ref` when a render runs longer than two minutes. See [apitemplate.io compatibility](api/compat-apitemplate). ## 17 September 2026: office templates in the validate endpoint {#2026-09-17-office-validate} - **API:** [`POST /templates/{id}/validate`](api/templates#validate) now checks Word and PowerPoint templates too: the tags of the stored document, errors that only happen when it is filled with the data, and the data against the stored schema. Their diagnostics carry the new fields `part` and `paragraph`. - **SDKs:** `Diagnostic` has `part` and `paragraph` in `@formfeed/sdk` and the Python SDK, with their next release. ## 17 September 2026: converting to PDF in agents and no-code apps {#2026-09-17-convert-integrations} - **Packages 0.3.2:** `@formfeed/mcp` and `n8n-nodes-formfeed` with the changes below; the other packages move to 0.3.2 with them. - **MCP:** the new tool `convert_to_pdf` turns a Word, Excel, PowerPoint, OpenDocument or RTF document, or the render of a Word or PowerPoint template, into a PDF. The local server also reads a file by its path. See [MCP server](integrations/mcp). - **n8n:** the resource **PDF** converts a document from a binary field or a render; **Output** offers DOCX and PPTX, and a downloaded file's name takes the extension of what was rendered. See [n8n](integrations/n8n). - **Zapier:** the action **Convert Document to PDF**, and DOCX and PPTX as outputs of **Create Document**. They reach Zapier with the app's next version. See [Zapier](integrations/zapier). - **Make:** the module **Convert a document to PDF**, and DOCX and PPTX as outputs of **Create a document**, with the app's next deploy. See [Make](integrations/make). - **Navigation:** **Usage** moved to the organisation section of the sidebar, next to **Billing**: units are counted for the whole organisation, and the page breaks them down by workspace. - **Packages 0.3.3:** `@formfeed/engine` with the `round` fix, and the CLI, dev kit and test helpers that render with it locally. - **Fixed:** `round(x, 0, 'floor')` and `round(x, 0, 'ceil')` rounded to the nearest value; they now round down and up as in Jinja2, also as `method='floor'`, in all three template languages. The [helper reference](templates/helpers#round) now says that `number` without decimals keeps the number's own decimals, at most three; `number(0)` gives whole numbers. ## 17 September 2026: packages 0.3.1 {#2026-09-17-packages-0-3-1} - **Packages 0.3.1:** `@formfeed/sdk`, `formfeed` (Python), `formfeed` (CLI), `@formfeed/engine`, `@formfeed/devkit`, `@formfeed/testing`, `@formfeed/mcp` and `n8n-nodes-formfeed` with the fixes below. - **Word templates:** a tag inside a field code (`HYPERLINK`, `REF`, …) stays as typed, and the Tags panel, `formfeed validate` and the MCP server now warn about it (`office-tag-in-field`). - **CLI:** `ignore` in `formfeed.json` now leaves matching template folders out of `formfeed templates push` without a slug (it was read but had no effect); `formfeed renders pull` works for renders of Word and PowerPoint templates; the true render of `formfeed dev` uses an image template's own format instead of always PNG. See [Configuration](cli/configuration). - **MCP:** `list_templates` filters by `docx` and `pptx`, and `validate_template` checks a Word or PowerPoint template's document, naming part and paragraph. See [MCP server](integrations/mcp). - **Fixed:** `formfeed render`, `formfeed ci preview` and the true render of `formfeed dev` printed the header and footer templates unfilled (`{{ company.name }}`); they now send them filled with the same data as the body. - **Jinja2:** `sum(attribute='total')` and `sum(attribute=…, start=…)` add up the attribute (they returned 0), and `{{ values }}` prints a list or dict as Python does (`[1, 2]`, `{'a': 1}`) instead of `1,2`, so chart scripts can take `"data": {{ values }}`. - **apitemplate.io import:** `