apitemplate.io compatibility endpoints
The compatibility shim lets an existing apitemplate.io integration switch by changing the base URL
and the key. It covers the four endpoints most integrations use and answers in apitemplate.io's
shape (status, download_url, transaction_ref). It is meant for migration: new features land
only on the native endpoints, and the shim has no rate-limit or quota exceptions. Every call is a
normal render: it needs the render:create scope, counts against the
rate limit, costs units like a native
render and appears in the render log.
| apitemplate.io | Formfeed shim | Native equivalent |
|---|---|---|
POST https://rest.apitemplate.io/v2/create-pdf?template_id=… | POST https://api-eu.formfeed.dev/v1/compat/apitemplate/create-pdf?template_id=<slug> | POST /v1/renders with template |
POST …/v2/create-image?template_id=…&output_image_type=… | POST …/v1/compat/apitemplate/create-image?template_id=<slug>&output_image_type=1 (or 2, all) | POST /v1/renders with output: jpg or png |
POST …/v2/create-pdf-from-html | POST …/v1/compat/apitemplate/create-pdf-from-html | POST /v1/renders with html and engine: jinja2 |
POST …/v2/create-pdf-from-url | POST …/v1/compat/apitemplate/create-pdf-from-url | POST /v1/renders with url |
What changes
-
Key: the
X-API-KEYheader stays; send a Formfeed key (ff_live_…, orff_test_…to try it for free) in it, or useAuthorization: Bearer ff_live_…. -
Template ids: templates imported with your apitemplate.io API key (Templates → Import → apitemplate.io → With API key) remember their apitemplate.io id, so
template_idstays as it is. Formfeed looks the value up as such an id first, then as the template's slug,tpl_…id or UUID; templates you created in Formfeed need one of those. A lookup is cached for a minute, so a template imported or archived a moment ago answers to its old id within that minute. An archived template no longer answers to its old id; imported again, the new copy does. -
Published version: the endpoints render the published version, so publish each imported draft first. A template without a published version answers
404. -
Body:
create-pdfandcreate-imagetake the template data as the JSON body, exactly as before; a body that is not a JSON object counts as no data.create-pdf-from-htmltakesbody(the HTML, rendered as a Jinja2 template),css(added as a<style>block in front of it),dataandsettings;create-pdf-from-urltakesurlandsettings. Query parameters other thantemplate_id,output_image_typeandexport_typeare ignored. -
Settings (
create-pdf-from-htmlandcreate-pdf-from-urlonly;create-pdfandcreate-imageuse the settings stored with the template) are mapped to Formfeed settings:apitemplate.io Formfeed paper_sizepaper.format:A3,A4,A5,Letter,Legal,Tabloid,Ledger; any other size renders as A4custom_width,custom_heightpaper.width,paper.height; a bare number is millimetresorientationpaper.landscape(landscapeor2)margin_top,margin_right,margin_bottom,margin_leftmargin; a bare number is CSS pixels, as Chromium reads it, and lengths with a unit (10mm) stay as they areheader_template,footer_templateheader.html,footer.html;{{pageNumber}},{{totalPages}},{{date}},{{title}}and{{url}}become Chromium's page fields, and the markup is scaled by 4/3, the size apitemplate.io draws it indisplayHeaderFooterfalseleaves header and footer outprint_background,scale,page_ranges,prefer_css_page_sizeprintBackground,scale,pageRanges,preferCssPageSizeKeys without an equivalent are ignored. See the settings reference.
-
output_image_type(create-image):1(the default) orjpegOnlyrenders a JPEG and returns it indownload_url,2orpngOnlya PNG indownload_url_png,allboth. The other field isnull.allmakes two renders and costs both. Any other value answers400 invalid_requestand renders nothing. -
Response:
status: "success",download_url,transaction_ref(the Formfeed render id, usable withGET /v1/renders/{id}),total_pages(nullfor images), and oncreate-pdfandcreate-imagethetemplate_idyou sent.download_urlis a signed link valid for seven days at most and never longer than the file is kept, which the plan's retention decides (one day on Free);GET /v1/renders/{id}issues a fresh one. Identical live requests within 24 hours are deduplicated like native renders and return the earlier render. -
export_type=fileanswers with a302redirect to the output instead of JSON. -
Errors of the render, the scope, the plan and the quota come back as
{ "status": "error", "message": …, "code": … }with the HTTP status of the underlying problem (404for an unknown template,402when the quota is used up,403for a missing scope,422for a template error). An error of a render that was recorded before it failed, such as a template error or a page limit, also carries itstransaction_ref, so the render log andGET /v1/renders/{id}find it. Aurlthe API refuses, and HTML with CSS above 5 MB, answer400withstatusandmessageonly. A missing or wrong key (401), a browser origin the key does not allow (403), the rate limit (429), a missingtemplate_id,bodyorurland an unknownoutput_image_type(400) answer like the native API, as an RFC 9457 problem. -
Busy regions make the request wait: the endpoints always answer with the finished document, also when the render went to the queue because every render slot was busy.
503 region_unavailableand429 rate_limitedcarry aRetry-Afterheader and are safe to retry after that many seconds. A render still running two minutes after it went to the background answers504 render_timeoutwith itstransaction_ref; do not retry that one, it would render the document again, but fetch the render withGET /renders/{id}.
Example
curl "https://api-eu.formfeed.dev/v1/compat/apitemplate/create-pdf?template_id=invoice-de" \
-H "X-API-KEY: $FORMFEED_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "invoice": { "number": "2026-0042", "lines": [] } }'
{
"status": "success",
"download_url": "https://cdn.formfeed.dev/o/eu/…?exp=…&sig=…",
"transaction_ref": "rnd_0f3c9a2e7b1d4e5f8a6c3b2d1e0f9a8b",
"total_pages": 1,
"template_id": "invoice-de"
}
Not covered
list-objects, delete-object, list-templates, the account endpoints and merge/watermark
helpers are not shimmed; any other path under /v1/compat/apitemplate/ answers 404 not_found.
Use the native endpoints instead: GET /v1/renders for list-objects,
DELETE /v1/renders/{id}/outputs for delete-object, GET /v1/templates for list-templates,
GET /v1/account and GET /v1/usage for the account, and the PDF tools
(POST /v1/pdf/merge, /v1/pdf/watermark, Starter and above) for merging and watermarks. The
importer moves the templates.