Skip to main content

Formfeed documentation

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.

curl https://api-eu.formfeed.dev/v1/renders \
-H "Authorization: Bearer $FORMFEED_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "html": "<h1>Hello {{ name }}</h1>", "engine": "liquid", "data": { "name": "Jane" } }'
{
"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 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 minutesQuickstart
Keep templates in git, preview locally, test in CITemplates as code
Understand workspaces, versions, keys and unitsCore concepts
Write a good template: pages, headers, fonts, imagesWriting templates
Call the API from my backendAPI overview · Rendering · TypeScript SDK · Python SDK
Render thousands of documentsAsync and batch · Webhooks
Look up an endpoint or a fieldAPI reference
Understand an error I gotErrors
Let an AI agent render documentsMCP server
Move from apitemplate.io, PDFMonkey or jsreportMigrate

How it works​

  1. Write a template. HTML and CSS with a template language for the variable parts, in the web editor or as files in your repository. Paper size, margins, headers and footers live in the template's 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 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.
  4. Download the result. The response carries a signed download_url, the page count, the 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 share 50+ 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 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 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 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.
  • EU hosted. Rendering and the database run in Frankfurt, and rendered files are stored in the EU. See Data residency.