openapi: 3.2.0
info:
title: Formfeed Renders API
version: 1.0.0
description: Generate PDFs and images from templates and JSON data, one API call per document.
termsOfService: https://formfeed.dev/legal/terms
contact:
name: Formfeed support
email: support@formfeed.dev
url: https://formfeed.dev
servers:
- url: https://api-eu.formfeed.dev/v1
description: EU (Frankfurt), where every workspace renders and stores today
- url: https://api.formfeed.dev/v1
description: Routes to the workspace's region
- url: https://api-us.formfeed.dev/v1
description: Reserved for a US region, which is not open yet
security:
- bearerAuth: []
tags:
- name: Renders
description: Render PDFs and images from templates, HTML or URLs.
paths:
/renders:
post:
tags:
- Renders
operationId: createRender
summary: Create a render
description: Renders a PDF or image from a stored template, ad-hoc HTML or a URL. Synchronous by default; set `mode` to `async` to receive a `202` and a webhook.
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
- name: output
in: query
description: Set to `binary` to receive the file instead of JSON.
schema:
type: string
enum:
- json
- binary
default: json
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RenderRequest'
examples:
template:
summary: From a template
value:
template: invoice-de
data:
invoice:
number: 2026-0042
output: pdf
html:
summary: Ad-hoc HTML
value:
html:
Hello {{ name }}
engine: liquid
data:
name: World
output: png
settings:
image:
width: 1200
height: 630
url:
summary: URL to PDF
value:
url: https://example.com/report
output: pdf
settings:
paper:
format: A4
responses:
'200':
description: Render succeeded (sync).
headers:
RateLimit-Limit:
schema:
type: integer
description: Requests per second allowed for the plan.
RateLimit-Remaining:
schema:
type: integer
description: Requests left in the current window.
RateLimit-Reset:
schema:
type: integer
description: Seconds until the bucket is full again.
X-Units-Charged:
schema:
type: number
description: Units metered for this render.
X-Units-Remaining:
schema:
type: number
X-Render-Region:
schema:
type: string
enum:
- eu
- us
- apac
X-Render-Engine:
schema:
type: string
X-Render-Deduplicated:
schema:
type: boolean
description: '`true` when an identical request was served from the dedup cache at zero units.'
content:
application/json:
schema:
$ref: '#/components/schemas/Render'
application/pdf:
schema:
type: string
format: binary
image/png:
schema:
type: string
format: binary
'202':
description: 'Render queued: either `mode: async` was requested, or a synchronous render outlived the 60 second deadline and was handed to the background.'
headers:
Location:
schema:
type: string
format: uri
description: URL of the render's result (`GET /renders/{id}/result`, with `output=binary` when the request asked for the file), which answers `202` until the render has finished.
Retry-After:
schema:
type: integer
description: Seconds to wait before asking `Location` again.
X-Render-Async-Fallback:
schema:
type: boolean
description: '`true` when a synchronous request fell back to async.'
content:
application/json:
schema:
$ref: '#/components/schemas/Render'
'402':
$ref: '#/components/responses/QuotaExceeded'
'422':
$ref: '#/components/responses/Unprocessable'
'429':
$ref: '#/components/responses/RateLimited'
default:
$ref: '#/components/responses/Problem'
get:
tags:
- Renders
operationId: listRenders
summary: List renders
parameters:
- name: template
in: query
schema:
type: string
- name: status
in: query
schema:
$ref: '#/components/schemas/RenderStatus'
- name: environment
in: query
schema:
type: string
enum:
- live
- test
- name: since
in: query
schema:
type: string
format: date-time
- name: until
in: query
schema:
type: string
format: date-time
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
responses:
'200':
description: Page of renders.
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Render'
next_cursor:
type:
- string
- 'null'
default:
$ref: '#/components/responses/Problem'
/renders/{id}:
get:
tags:
- Renders
operationId: getRender
summary: Get a render
parameters:
- $ref: '#/components/parameters/RenderId'
responses:
'200':
description: Render.
content:
application/json:
schema:
$ref: '#/components/schemas/Render'
'404':
$ref: '#/components/responses/NotFound'
default:
$ref: '#/components/responses/Problem'
/renders/{id}/result:
get:
tags:
- Renders
operationId: getRenderResult
summary: Wait for the result of a render
description: 'The URL in the `Location` of a `202` from `POST /renders`. While the render is queued or rendering, the answer is `202` with the render, the same `Location` and a `Retry-After`. Once it has finished, the answer is what the synchronous request would have returned: the render, the file with `output=binary`, or the render''s error as a problem with its `render_id`. Clients that follow a `202` on their own, such as Power Automate and Logic Apps, therefore wait for the document. The SDKs wait by polling `GET /renders/{id}` instead.'
parameters:
- $ref: '#/components/parameters/RenderId'
- name: output
in: query
description: Set to `binary` to receive the file instead of JSON.
schema:
type: string
enum:
- json
- binary
default: json
responses:
'200':
description: The render has succeeded.
content:
application/json:
schema:
$ref: '#/components/schemas/Render'
application/pdf:
schema:
type: string
format: binary
image/png:
schema:
type: string
format: binary
'202':
description: The render is still queued or rendering.
headers:
Location:
schema:
type: string
format: uri
description: This URL again.
Retry-After:
schema:
type: integer
description: Seconds to wait before asking again.
content:
application/json:
schema:
$ref: '#/components/schemas/Render'
'404':
$ref: '#/components/responses/NotFound'
'422':
$ref: '#/components/responses/Unprocessable'
default:
$ref: '#/components/responses/Problem'
/renders/{id}/input:
get:
tags:
- Renders
operationId: getRenderInput
summary: Get the request of a render
description: 'The stored request of a render, as what renders it again: the template and version (or the HTML and engine, or the URL), the data, the locale and the options. Requests are kept only when the workspace turns on "Keep render requests" (off by default; 24 hours from Starter, 7 days from Pro); otherwise, and after the period, the answer is 410 `render_input_expired`. Passwords are never kept. Needs the `render:input` scope, which new keys do not have by default; every read is written to the audit log.'
parameters:
- $ref: '#/components/parameters/RenderId'
responses:
'200':
description: The stored request.
content:
application/json:
schema:
$ref: '#/components/schemas/RenderInput'
'404':
$ref: '#/components/responses/NotFound'
'410':
$ref: '#/components/responses/Problem'
default:
$ref: '#/components/responses/Problem'
/renders/{id}/outputs:
delete:
tags:
- Renders
operationId: deleteRenderOutputs
summary: Delete stored outputs of a render
parameters:
- $ref: '#/components/parameters/RenderId'
responses:
'204':
description: Outputs deleted.
'404':
$ref: '#/components/responses/NotFound'
default:
$ref: '#/components/responses/Problem'
/renders/batch:
post:
tags:
- Renders
operationId: createBatch
summary: Create a batch of renders
description: Up to 1 000 renders in one job. Always asynchronous. Optional zip of all outputs.
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- items
properties:
template:
type: string
description: Default template for all items.
canary_key:
type: string
maxLength: 200
description: Default `canary_key` for all items. Without one, every item of the batch lands on the same version of a channel with a canary.
output:
allOf:
- $ref: '#/components/schemas/OutputFormat'
description: Default `output` for all items; an item's own `output` wins. Without either, each item's template decides.
items:
type: array
maxItems: 1000
items:
$ref: '#/components/schemas/RenderRequest'
zip:
type: boolean
default: false
webhook_url:
type:
- string
- 'null'
format: uri
webhook_secret:
type:
- string
- 'null'
minLength: 16
maxLength: 200
description: Signs the deliveries to `webhook_url` (same scheme as an endpoint secret). Write-only, never returned; without it those deliveries stay unsigned.
meta:
type: object
additionalProperties: true
responses:
'202':
description: Job queued.
content:
application/json:
schema:
$ref: '#/components/schemas/Job'
'422':
$ref: '#/components/responses/Unprocessable'
default:
$ref: '#/components/responses/Problem'
/jobs/{id}:
get:
tags:
- Renders
operationId: getJob
summary: Get a batch job
parameters:
- name: id
in: path
required: true
schema:
type: string
example: job_01J8ZK4M3Q
responses:
'200':
description: Job.
content:
application/json:
schema:
$ref: '#/components/schemas/Job'
'404':
$ref: '#/components/responses/NotFound'
default:
$ref: '#/components/responses/Problem'
components:
schemas:
Engine:
type: string
enum:
- jinja2
- liquid
- handlebars
RenderStatus:
type: string
enum:
- queued
- rendering
- succeeded
- failed
Render:
type: object
required:
- id
- status
- output
- region
- created_at
properties:
id:
type: string
example: rnd_01J8ZK4M3Q7X
status:
$ref: '#/components/schemas/RenderStatus'
output:
$ref: '#/components/schemas/OutputFormat'
download_url:
type:
- string
- 'null'
format: uri
expires_at:
type:
- string
- 'null'
format: date-time
bytes:
type:
- integer
- 'null'
page_count:
type:
- integer
- 'null'
units:
type: number
example: 1
region:
$ref: '#/components/schemas/Region'
environment:
type: string
enum:
- live
- test
template:
type:
- object
- 'null'
properties:
id:
type: string
slug:
type: string
version:
type: integer
channel:
type:
- string
- 'null'
description: The release channel the version came from (`published` by default); null when the request named a version number or `latest`.
canary:
type: boolean
description: True when the channel's canary share picked this version.
engine_version:
type: string
description: Chromium build and engine package version, for reproducibility.
example: chromium-140.0.7339.16/engine-1.4.0
template_checksum:
type:
- string
- 'null'
output_sha256:
type:
- string
- 'null'
deduplicated:
type: boolean
storage:
allOf:
- $ref: '#/components/schemas/StorageDelivery'
timings:
type: object
properties:
queue_ms:
type: integer
compile_ms:
type: integer
render_ms:
type: integer
upload_ms:
type: integer
total_ms:
type: integer
error:
allOf:
- $ref: '#/components/schemas/Problem'
meta:
type: object
additionalProperties: true
created_at:
type: string
format: date-time
completed_at:
type:
- string
- 'null'
format: date-time
Region:
type: string
enum:
- eu
- us
- apac
Settings:
type: object
description: Rendering settings. Deep-merged over the template's settings. See the settings reference.
additionalProperties: true
properties:
paper:
type: object
properties:
format:
type: string
example: A4
width:
type:
- string
- 'null'
height:
type:
- string
- 'null'
landscape:
type: boolean
default: false
margin:
type: object
properties:
top:
type: string
example: 20mm
right:
type: string
example: 15mm
bottom:
type: string
example: 20mm
left:
type: string
example: 15mm
header:
type: object
description: Printed across the whole page width inside the top margin.
properties:
html:
type: string
height:
type: string
padding:
type: string
description: CSS padding inside the header box; `0` for designs that run edge to edge.
default: 0 10mm
footer:
type: object
description: Printed across the whole page width inside the bottom margin.
properties:
html:
type: string
height:
type: string
padding:
type: string
description: CSS padding inside the footer box; `0` for designs that run edge to edge.
default: 0 10mm
printBackground:
type: boolean
default: true
scale:
type: number
default: 1
image:
type: object
properties:
width:
type: integer
example: 1200
height:
type: integer
example: 630
deviceScaleFactor:
type: number
default: 2
format:
type: string
enum:
- png
- jpeg
- webp
description: Output of an image template when the request names no `output`. `jpg` is accepted as well.
quality:
type: integer
minimum: 1
maximum: 100
fullPage:
type: boolean
default: false
transparent:
type: boolean
default: false
office:
type: object
description: Word and PowerPoint templates. Page setup, margins, headers and footers come from the document; the Chromium settings above do not apply.
properties:
output:
type: string
enum:
- docx
- pptx
- pdf
description: Output when the request names none; the template's own format without it.
locale:
type: string
example: de-DE
timezone:
type: string
example: Europe/Berlin
currency:
type: string
example: EUR
RenderRequest:
type: object
properties:
template:
type: string
description: Template id or slug. Omit when using `html` or `url`.
example: invoice-de
version:
type: string
description: '`published` (default), the name of a release channel such as `staging`, `latest` (the draft the editor works on, test keys only) or a version number. Channels, `published` included, are cached for up to a minute; a channel with a canary sends its share of renders to the canary version, chosen by the Idempotency-Key so a retry lands on the same version. A draft renders as it is at that moment.'
default: published
html:
type:
- string
- 'null'
description: Ad-hoc HTML, max 5 MB.
url:
type:
- string
- 'null'
format: uri
description: Public URL to render.
engine:
allOf:
- $ref: '#/components/schemas/Engine'
description: Engine for `html` sources; `null` disables templating.
data:
type: object
additionalProperties: true
description: Data bound to the template.
output:
allOf:
- $ref: '#/components/schemas/OutputFormat'
description: 'Left out, the template decides: `pdf` for a template of kind `pdf`, `settings.image.format` (the request''s, then the template''s; `png` without one) for kind `image`, `settings.office.output` (else the template''s own format) for kinds `docx` and `pptx`. Ad-hoc `html` and `url` render `pdf`. On the Free plan, test renders of `docx` and `pptx` templates may only request `pdf` (403 `test_output_unavailable`).'
settings:
$ref: '#/components/schemas/Settings'
filename:
type: string
description: May contain template expressions.
example: invoice-{{ invoice.number }}.pdf
expires_in:
type:
- integer
- 'null'
description: Seconds until the output expires. `null` uses the workspace retention.
access:
type: string
enum:
- public
- signed
default: signed
mode:
type: string
enum:
- sync
- async
default: sync
locale:
type:
- string
- 'null'
description: Overrides the template locale and selects the i18n dictionary.
webhook_url:
type:
- string
- 'null'
format: uri
webhook_secret:
type:
- string
- 'null'
minLength: 16
maxLength: 200
description: Signs the deliveries to `webhook_url` (same scheme as an endpoint secret). Write-only, never returned; without it those deliveries stay unsigned.
meta:
type: object
additionalProperties: true
description: Echoed in the response and webhooks.
region:
allOf:
- $ref: '#/components/schemas/Region'
dedupe:
type: boolean
default: true
description: Serve identical requests from the dedup cache at zero units.
canary_key:
type:
- string
- 'null'
minLength: 1
maxLength: 200
example: customer-4711
description: 'Decides the canary split of a channel: renders with the same value land on the same version, so one customer''s documents look alike. Use an opaque id; it is not stored. Without it the Idempotency-Key decides, which keeps a retry on its version but splits separate renders independently.'
validate_data:
type: boolean
default: false
description: Checks `data` against the template's stored data schema before anything is rendered or counted; a mismatch answers 422 `data_validation_error` listing every error. Needs a template with a stored schema (400 otherwise). In a batch, nothing is queued unless every item that asks for it fits.
post:
type:
- object
- 'null'
description: 'Post-processing of a PDF render, applied in the order merge, watermark, password (0.5 units
per step on live keys; Starter plan and above). An unknown step is refused, never skipped.
'
additionalProperties: false
properties:
merge_after:
type: array
minItems: 1
maxItems: 49
items:
type: string
description: 'PDFs appended after this document, in order: render ids (`rnd_…`) of earlier renders, file ids (`fil_…`) or names of PDFs in the workspace library.'
watermark:
type: object
additionalProperties: false
description: Either a text or an image from the workspace library, not both.
properties:
text:
type: string
maxLength: 200
image:
type: string
maxLength: 200
description: 'A library file: its id (`fil_…`) or its name. PNG or JPEG.'
opacity:
type: number
minimum: 0
maximum: 1
default: 0.2
rotation:
type: number
default: -45
description: Degrees clockwise as in CSS.
color:
type: string
pattern: ^#?[0-9a-fA-F]{6}$
description: Hex colour of the text; ignored for an image.
password:
type: object
additionalProperties: false
properties:
user:
type: string
maxLength: 127
description: Needed to open the document.
owner:
type: string
maxLength: 127
description: Lifts the restrictions; random when omitted.
permissions:
type: array
items:
type: string
enum:
- print
- copy
- modify
- annotate
default: []
storage:
allOf:
- $ref: '#/components/schemas/StorageOption'
OutputFormat:
type: string
enum:
- pdf
- png
- jpg
- webp
- docx
- pptx
description: PDF and image templates, ad-hoc HTML and URLs produce `pdf`, `png`, `jpg` or `webp`; a `docx` template produces `docx` or `pdf`, a `pptx` template `pptx` or `pdf`.
Job:
type: object
properties:
id:
type: string
example: job_01J8ZK4M3Q
status:
type: string
enum:
- queued
- processing
- completed
- partially_failed
- failed
total:
type: integer
succeeded:
type: integer
failed:
type: integer
zip_url:
type:
- string
- 'null'
format: uri
storage:
allOf:
- $ref: '#/components/schemas/StorageDelivery'
description: The zip's copy in the workspace's own bucket; each item carries its own.
items:
type: array
items:
$ref: '#/components/schemas/Render'
meta:
type: object
additionalProperties: true
created_at:
type: string
format: date-time
completed_at:
type:
- string
- 'null'
format: date-time
Problem:
type: object
required:
- type
- title
- status
properties:
type:
type: string
format: uri
example: https://docs.formfeed.dev/errors/template-syntax
title:
type: string
example: Template syntax error
status:
type: integer
example: 422
detail:
type: string
code:
type: string
enum:
- template_not_found
- template_syntax_error
- template_runtime_error
- data_validation_error
- navigation_failed
- render_timeout
- page_limit_exceeded
- output_too_large
- file_too_large
- source_not_found
- source_invalid
- source_encrypted
- tool_failed
- file_type_unsupported
- office_document_invalid
- office_document_too_large
- office_conversion_failed
- office_unavailable
- unsupported_in_office
- test_output_unavailable
- quota_exceeded
- rate_limited
- region_unavailable
- worker_exhausted
- invalid_request
- unauthorized
- forbidden
- not_found
- conflict
- not_implemented
- schema_breaking_change
- channel_not_found
- channel_name_invalid
- channel_in_use
- render_input_expired
- listen_session_limit
- listen_session_ended
- last_workspace
- storage_not_configured
- storage_test_failed
location:
type:
- object
- 'null'
properties:
line:
type: integer
column:
type: integer
file:
type: string
enum:
- template
- css
- head
- header
- footer
part:
type: string
example: word/document.xml
description: 'Office templates: the part of the file (document, header, footer, slide).'
paragraph:
type: integer
description: 'Office templates: the paragraph within the part, from 1.'
request_id:
type: string
errors:
type: array
description: '`data_validation_error` only: every place where the data does not match the schema.'
items:
type: object
properties:
path:
type:
- string
- 'null'
example: data.invoice.number
message:
type: string
example: 'data.invoice.number: must be string'
index:
type: integer
description: 'Batch requests only: the first item that failed.'
RenderInput:
type: object
description: The stored request of a render, as `GET /renders/{id}/input` returns it. Fields that do not apply are left out.
properties:
render_id:
type: string
example: rnd_01J8ZK4M3Q7X
template:
type: object
properties:
id:
type: string
slug:
type: string
version:
type: integer
channel:
type: string
html:
type: string
description: Ad-hoc HTML renders.
engine:
$ref: '#/components/schemas/Engine'
url:
type: string
format: uri
description: URL renders.
data:
type: object
additionalProperties: true
locale:
type: string
output:
$ref: '#/components/schemas/OutputFormat'
settings:
type: object
additionalProperties: true
filename:
type: string
post:
type: object
additionalProperties: true
description: Post-processing without passwords.
meta:
type: object
additionalProperties: true
environment:
type: string
enum:
- live
- test
created_at:
type: string
format: date-time
StorageOption:
description: 'The copy for the workspace''s own bucket (bring your own storage). Left out, every output is
copied under the connection''s key template. `false` leaves this document out. `key` names its
object below the connection''s prefix instead of the key template: relative, without `.` or `..`
segments, at most 1024 bytes; an existing object is overwritten. A render with its own key is
never answered from the dedup cache, and one without a connection answers 409
`storage_not_configured`. In a batch, each item carries its own.
'
oneOf:
- type: boolean
enum:
- false
- type: object
additionalProperties: false
properties:
key:
type: string
maxLength: 1024
example: invoices/2026/RE-1001.pdf
StorageDelivery:
type: object
description: 'The output''s copy in the workspace''s own bucket; null when none applies (no connection, a
paused one, `storage: false`, a test render the connection does not take). A synchronous
response reports `pending`: the copy is made in the background, and `storage.delivered` or
`storage.failed` follow as webhook events.
'
properties:
status:
type: string
enum:
- pending
- delivered
- failed
bucket:
type: string
example: fennlor-docs
key:
type: string
example: formfeed/2026/09/rnd_7da09ab6cd9946b798c187c2a9350852/invoice.pdf
url:
type:
- string
- 'null'
description: The object below the connection's `public_base_url`; null without one.
reason:
type:
- string
- 'null'
enum:
- permanent
- rejected
- exhausted
- source_gone
- connection_deleted
- connection_paused
- plan
- workspace_deleted
description: 'Why a delivery failed: the credentials or bucket (`permanent`), the object (`rejected`), retries used up (`exhausted`), the output expired first (`source_gone`), or the connection went away.'
error:
allOf:
- $ref: '#/components/schemas/StorageError'
StorageError:
type: object
description: 'The provider''s answer, reduced: its code, the HTTP status and the first line of its message.'
properties:
code:
type: string
example: AccessDenied
http_status:
type:
- integer
- 'null'
example: 403
message:
type: string
example: Access Denied
parameters:
Limit:
name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 50
RenderId:
name: id
in: path
required: true
schema:
type: string
example: rnd_01J8ZK4M3Q7X
Cursor:
name: cursor
in: query
schema:
type: string
IdempotencyKey:
name: Idempotency-Key
in: header
required: false
description: Unique key per logical request; replays within 24 h return the original response.
schema:
type: string
maxLength: 128
responses:
Unprocessable:
description: Validation, template or data error. Template errors include `location`.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
QuotaExceeded:
description: Monthly units exhausted and no overage available.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
Problem:
description: Error (RFC 9457 problem details).
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
RateLimited:
description: Rate limit exceeded.
headers:
Retry-After:
schema:
type: integer
RateLimit-Limit:
schema:
type: integer
RateLimit-Remaining:
schema:
type: integer
RateLimit-Reset:
schema:
type: integer
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
NotFound:
description: Not found.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: API key
description: 'API key from the workspace: `ff_live_…` or `ff_test_…`. Also accepted in the `X-API-Key` header.'
externalDocs:
description: Guides, SDKs and the CLI
url: https://docs.formfeed.dev
x-provenance:
method: harvested
publisher: Formfeed
source: https://api.formfeed.dev/v1/openapi.json
note: split by refine-openapis from the provider's own published document, verified by fetch (openapi/_original/*.fetch.json);
stamped by stamp-openapi-provenance.py (roadmap#793)