Migrate from apitemplate.io
Import templates
In the app: Templates → Import → apitemplate.io. There are two ways to bring templates over.
With API key (many templates at once):
- Enter the API key from apitemplate.io (API Integration) and the region your integration calls (
rest-de.apitemplate.iois Europe (Frankfurt)). The key is used to read your templates and never stored. - List templates shows every template of the account with its group. PDF templates are selected; filter by name or group and use Select shown or Clear shown to pick a set. Image templates are layer designs without HTML and cannot be imported.
- Analyse reads the HTML, CSS, settings and sample JSON of each picked template and checks it (see below). Templates apitemplate.io does not return are listed under Could not be read; copy those from their editor and use Paste or files.
- Create drafts makes one Jinja2 template per imported template, with its first version saved as a draft (nothing is published). Its slug is the template's name in lowercase with dashes (
Statement of costbecomesstatement-of-cost);-2,-3and so on are added only when the workspace already has that slug, archived templates included. The draft remembers its apitemplate.io id for the compatibility endpoints. On the Free plan, which holds five templates, an import stops at the limit and the report keeps the rest.
The JSON of their editor's JSON tab becomes the draft's default data set, so the preview shows data right away. A template with an empty JSON tab starts without data, and the report then does not check for missing fields. apitemplate.io marks the endpoint that returns a template's source as experimental.
Paste or files (one template): paste what their editor shows, the HTML body, the CSS, the settings JSON (paper, margins, header and footer templates) and the sample JSON, or drop the exported files (.html, .css, .json).
Either way:
- Analyse compiles each template with the Jinja2 engine and checks it against the sample data. The report lists errors (unknown filters, syntax the engine rejects), things to check (Python-only constructs, settings without an equivalent, missing sample keys) and what the importer mapped.
- Compare with apitemplate.io (optional): enter your apitemplate.io key and the template's apitemplate.io id, and Render original renders the original there with the same sample data, so you can put their PDF next to the editor preview. The key is used for that one call and never stored.
- Create draft makes a Jinja2 template with the mapped settings and the sample data. Review it in the editor, then publish.
A template pasted this way does not know its apitemplate.io id, so it answers to its slug and Formfeed id only, also on the compatibility endpoints.
With the CLI
In a template project, the import writes one local template folder per template; review them with formfeed dev, then formfeed templates push. With an API key (--key or APITEMPLATE_API_KEY), pick the templates by id, by group or all at once, and give the region with --source-region or APITEMPLATE_REGION (default, de, us, au, alt, alt-de, alt-us; default when neither is set):
export APITEMPLATE_API_KEY=...
export APITEMPLATE_REGION=de
formfeed import apitemplate --all
formfeed import apitemplate --group Invoices
formfeed import apitemplate --template 3a677b23217dd954 69c77b2365e90a12
Templates with the same name get numbered folders (overview, overview-2); templates apitemplate.io does not return are reported and skipped. --all and --group take the PDF templates only and skip image templates with a note. From exported files instead (--css, --settings and --sample are optional; --name defaults to the HTML file's name and --slug to one made from the name):
formfeed import apitemplate --name "Invoice" --html body.html --css style.css --settings settings.json --sample sample.json
Every imported template prints its errors, warnings and notes, the same report the app shows.
Settings
These settings are mapped to the template's settings (Page setup in the editor):
| apitemplate.io | Formfeed |
|---|---|
paper_size (A3, A4, A5, Letter, Legal, Tabloid, Ledger) | paper.format; any other size becomes A4 with a note |
custom_width, custom_height | paper.width, paper.height |
orientation | paper.landscape |
margin_top, margin_right, margin_bottom, margin_left | margin |
header_template, footer_template, custom_header, custom_footer, header_left … footer_right | header and footer (see below) |
print_background, scale, page_ranges, prefer_css_page_size | printBackground, scale, pageRanges, preferCssPageSize |
output_image_type, viewport_width, viewport_height, device_scale | image.format, image.width, image.height, image.deviceScaleFactor |
A setting without an equivalent is dropped and listed in the report. JPEG and PNG templates become
image templates. Margins without a unit ("margin_top": "40") are pixels, as apitemplate.io's
renderer reads them, so 40 becomes 40px (about 10.6mm), not 40mm.
Headers and footers of stored templates: custom_header and custom_footer markup is kept when
it shows something. Otherwise the text slots header_left, header_center, header_right (and
the footer_* ones) become a row in header_font_size or footer_font_size (9px when none is set).
The placeholders {{pageNumber}} or {{page_number}}, {{totalPages}} or {{total_pages}},
{{date}}, {{title}} and {{url}} turn into the page number, page count, date, title and address as
before. A display_header_footer that is false leaves header and footer out.
apitemplate.io lays header and footer templates out in 75 % of the page width and scales them up by
4/3, so a px length comes out a third larger there. The importer wraps the markup in a box that
scales the same way, so an imported logo header keeps its size. Markup that clears the header or
footer padding (#header, #footer { padding: 0 }) gets padding: 0 in the settings and runs edge
to edge as before.
apitemplate.io's CSS field often carries <link> and <script> tags (Google Fonts, icon fonts,
chart libraries). The importer moves them to the Head tab and keeps the contents of <style>
blocks as CSS, so fonts and icons load as they did. apitemplate.io fills the CSS field with the data,
while Formfeed's head and CSS are static: <script> and <style> blocks that use template syntax
(a chart script with "data": {{ values }}) go to the end of the template instead, where they are
filled. A list or dict prints as Python prints it ([1, 2]), as on apitemplate.io.
Strings
Python string formatting with % ("%.2f" % value) has no equivalent; use "{:.2f}".format(value),
number() or money(). .upper(), .replace(), .split(), .strip() and .format() work through
the compatibility layer, and .format() understands Python's format specs, including grouping and
nested fields ("{:,.{}f}".format(value, places)). truncate(length, killwords, end),
map(attribute='name') and selectattr('field', 'equalto', value) take their Jinja2 arguments.
Loops
loop.cycle() is not available; use cycler() or loop.index % n. A {% set %} inside a loop
that changes a variable set before the loop keeps the new value after the loop in Nunjucks, while
Python Jinja2 drops it; the report points at every such set. namespace() works as in Jinja2, so
counters written as {% set ns.total = ns.total + 1 %} run unchanged.
The importer also hides the wrapper elements apitemplate.io's visual editor puts around loop tags
(class apitemplate-is-content-hidden) with a rule in the template's CSS.
Macros, call blocks, filter blocks and autoescape blocks run with Nunjucks' rules, so the report
asks you to check their output. {% import %} and {% from … import %} refer to files that stayed
at apitemplate.io: move the macros into the template or a partial.
Tests
Jinja2 tests take parentheses in Nunjucks: is divisibleby(2) instead of is divisibleby 2, which
does not parse. sequence does not exist; use is iterable. sameas compares with ===, which is
Jinja2's identity check for lists and objects. The report marks every use of divisibleby, sameas,
escaped, mapping, sequence and callable so you can check it.
Sample data
Keys the template reads but the sample lacks are reported so the preview shows every path. The editor infers a data schema from the sample when you save.
Filters
Formfeed has helpers of the same name for apitemplate.io's qrcode and
barcode; their options are Formfeed's, so check each call against the helper reference.
currency_format is an alias of money(value, currency, locale): it
renders, and the editor marks it with a warning and offers to rename it. There is no table filter:
a template that uses it fails until you write the table as HTML with a {% for %} loop. The report
lists every filter it does not know as an error.
API
Switch the base URL and header, or use the compatibility endpoints while you migrate:
| apitemplate.io | Formfeed |
|---|---|
/v2/create-pdf?template_id=… | POST /v1/renders with template |
/v2/create-image?template_id=… | POST /v1/renders with template and output: "png" or "jpg" |
/v2/create-pdf-from-html | POST /v1/renders with html and engine: "jinja2" |
/v2/create-pdf-from-url | POST /v1/renders with url |
/v2/list-objects | GET /v1/renders |
/v2/delete-object | DELETE /v1/renders/{id}/outputs |
X-API-KEY: … | Authorization: Bearer … (or X-API-Key) |
| Compatibility shim | POST /v1/compat/apitemplate/create-pdf, create-image, create-pdf-from-html and create-pdf-from-url answer in apitemplate.io's response shape |
Every apitemplate.io region maps to https://api-eu.formfeed.dev/v1: Formfeed renders in Frankfurt
and stores in the EU. api-us.formfeed.dev is reserved for a US region that is not open yet.
With the compatibility endpoints, a migration is three steps: import the templates with your API key, publish them, and point the integration at https://api-eu.formfeed.dev/v1/compat/apitemplate/… with a Formfeed key in the same X-API-KEY header. Templates imported that way in the app keep answering to their apitemplate.io template_id, so the request stays as it is; templates imported with formfeed import and pushed from a project answer to their slug or Formfeed ID only. See the compatibility endpoints.
apitemplate.io's S3 Storage post action has no API equivalent to import, because its credentials never leave apitemplate.io: connect your bucket once in the workspace settings instead (bring your own storage, Pro plan and above). Renders through the compatibility endpoints are then copied there like any other render, except that one answered from the deduplication cache writes no second copy, and cloud_storage is ignored, because Formfeed always keeps its own copy for the workspace's retention.