Skip to main content

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.ioFormfeed shimNative 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-htmlPOST …/v1/compat/apitemplate/create-pdf-from-htmlPOST /v1/renders with html and engine: jinja2
POST …/v2/create-pdf-from-urlPOST …/v1/compat/apitemplate/create-pdf-from-urlPOST /v1/renders with url

What changes​

  • Key: the X-API-KEY header stays; send a Formfeed key (ff_live_…, or ff_test_… to try it for free) in it, or use Authorization: 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_id stays 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-pdf and create-image take 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-html takes body (the HTML, rendered as a Jinja2 template), css (added as a <style> block in front of it), data and settings; create-pdf-from-url takes url and settings. Query parameters other than template_id, output_image_type and export_type are ignored.

  • Settings (create-pdf-from-html and create-pdf-from-url only; create-pdf and create-image use the settings stored with the template) are mapped to Formfeed settings:

    apitemplate.ioFormfeed
    paper_sizepaper.format: A3, A4, A5, Letter, Legal, Tabloid, Ledger; any other size renders as A4
    custom_width, custom_heightpaper.width, paper.height; a bare number is millimetres
    orientationpaper.landscape (landscape or 2)
    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 are
    header_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 in
    displayHeaderFooterfalse leaves header and footer out
    print_background, scale, page_ranges, prefer_css_page_sizeprintBackground, scale, pageRanges, preferCssPageSize

    Keys without an equivalent are ignored. See the settings reference.

  • output_image_type (create-image): 1 (the default) or jpegOnly renders a JPEG and returns it in download_url, 2 or pngOnly a PNG in download_url_png, all both. The other field is null. all makes two renders and costs both. Any other value answers 400 invalid_request and renders nothing.

  • Response: status: "success", download_url, transaction_ref (the Formfeed render id, usable with GET /v1/renders/{id}), total_pages (null for images), and on create-pdf and create-image the template_id you sent. download_url is 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=file answers with a 302 redirect 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 (404 for an unknown template, 402 when the quota is used up, 403 for a missing scope, 422 for 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 its transaction_ref, so the render log and GET /v1/renders/{id} find it. A url the API refuses, and HTML with CSS above 5 MB, answer 400 with status and message only. A missing or wrong key (401), a browser origin the key does not allow (403), the rate limit (429), a missing template_id, body or url and an unknown output_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_unavailable and 429 rate_limited carry a Retry-After header and are safe to retry after that many seconds. A render still running two minutes after it went to the background answers 504 render_timeout with its transaction_ref; do not retry that one, it would render the document again, but fetch the render with GET /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.