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 minutes | Quickstart |
| Keep templates in git, preview locally, test in CI | Templates as code |
| Understand workspaces, versions, keys and units | Core concepts |
| Write a good template: pages, headers, fonts, images | Writing templates |
| Call the API from my backend | API overview · Rendering · TypeScript SDK · Python SDK |
| Render thousands of documents | Async and batch · Webhooks |
| Look up an endpoint or a field | API reference |
| Understand an error I got | Errors |
| Let an AI agent render documents | MCP server |
| Move from apitemplate.io, PDFMonkey or jsreport | Migrate |
How it works
- 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.
- Publish it. By default the API renders the published version, so editing a draft never changes what your production code gets.
- Send data.
POST /v1/renderswith the template slug and a JSON object. A render answers synchronously by default, with the finished file. Set"mode": "async"to get202at 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. - 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
typelinks to a page that explains the fix. - Safe retries. Once a request with an
Idempotency-Keyhas 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.