openapi: 3.2.0 info: title: Formfeed PDF tools 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: PDF tools description: Merge, protect, watermark and inspect PDFs. paths: /pdf/merge: post: tags: - PDF tools operationId: mergePdf summary: Merge PDFs description: 'Joins the PDF outputs of earlier renders, in the order given, into a new PDF. The result is a render of its own (it appears in the render log and `GET /renders/{id}`) and costs 0.5 units on live keys. Sources are render ids of the same workspace whose output still exists; uploaded files come with the file library. Part of the Starter plan and above.' requestBody: required: true content: application/json: schema: type: object required: - sources properties: sources: type: array minItems: 2 maxItems: 50 items: type: string description: Render id (`rnd_…`) of a succeeded PDF render, or a PDF of the workspace library by id (`fil_…`) or name. example: - rnd_7da09ab6cd9946b798c187c2a9350852 - terms.pdf filename: type: string example: bundle.pdf expires_in: type: - integer - 'null' access: type: string enum: - public - signed meta: type: object additionalProperties: true storage: allOf: - $ref: '#/components/schemas/StorageOption' responses: '200': description: Merged render. content: application/json: schema: $ref: '#/components/schemas/Render' default: $ref: '#/components/responses/Problem' /pdf/protect: post: tags: - PDF tools operationId: protectPdf summary: Password-protect a PDF description: 'Encrypts the PDF output of an earlier render with AES-256 into a new render (0.5 units on live keys). `user_password` is needed to open the document; without it anyone can open it, but the permissions still apply. `permissions` lists what readers may do; everything not listed is forbidden. Protect last: an encrypted PDF cannot be merged or watermarked afterwards.' requestBody: required: true content: application/json: schema: type: object required: - source properties: source: type: string description: Render id (`rnd_…`) of a succeeded PDF render. user_password: type: string maxLength: 127 description: Needed to open the document. owner_password: type: string maxLength: 127 description: Lifts the restrictions. A random one is used when omitted. permissions: type: array items: type: string enum: - print - copy - modify - annotate default: [] filename: type: string expires_in: type: - integer - 'null' access: type: string enum: - public - signed meta: type: object additionalProperties: true storage: allOf: - $ref: '#/components/schemas/StorageOption' responses: '200': description: Protected render. content: application/json: schema: $ref: '#/components/schemas/Render' default: $ref: '#/components/responses/Problem' /pdf/convert: post: tags: - PDF tools operationId: convertOfficeToPdf summary: Convert an office document to PDF description: 'Converts a Word, Excel, PowerPoint, OpenDocument or RTF file into a PDF render (Starter plan and above). Upload the file as `multipart/form-data`, or name the render of a Word or PowerPoint template (output `docx` or `pptx`) as `source` in a JSON body. Accepted are DOCX, XLSX, PPTX, ODT, ODS, ODP, DOC, XLS, PPT, RTF and HTML, up to 20 MB; the type is read from the file''s content, not its name, so a "save as web page" export that carries a `.doc` name converts as the HTML it is. Macro-enabled and password-protected documents are refused. The file is converted and not kept; the PDF is stored like any render. HTML is converted by LibreOffice, not by the render engine, and the converter loads nothing from the web: images and stylesheets the document links to stay empty. For HTML you control, a render with an `html` source gives a better result. Costs follow the PDF rule: one unit per started five pages on live keys. Chain `/pdf/protect`, `/pdf/watermark` or `/pdf/merge` on the returned render id. Fonts the document uses but neither embeds nor finds among the converter''s fonts are replaced by metric-compatible ones (Carlito for Calibri, Caladea for Cambria, Liberation for Arial and Times New Roman). Embed fonts in Word for an exact result.' requestBody: required: true content: application/json: schema: type: object required: - source properties: source: type: string description: Render id (`rnd_…`) of a succeeded render whose output is `docx` or `pptx`. page_ranges: type: string maxLength: 100 pattern: ^\d+(-\d+)?(,\d+(-\d+)?)*$ example: 1-3,5 landscape: type: boolean default: false single_page_sheets: type: boolean default: false filename: type: string expires_in: type: integer access: type: string enum: - public - signed meta: type: object additionalProperties: true storage: allOf: - $ref: '#/components/schemas/StorageOption' multipart/form-data: schema: type: object required: - file properties: file: type: string format: binary description: The document, up to 20 MB. page_ranges: type: string maxLength: 100 pattern: ^\d+(-\d+)?(,\d+(-\d+)?)*$ example: 1-3,5 description: Pages to convert; all when omitted. landscape: type: boolean default: false description: Landscape for spreadsheets and documents without their own page setup. single_page_sheets: type: boolean default: false description: Each spreadsheet sheet on one page. filename: type: string description: Name of the PDF; the uploaded name with `.pdf` when omitted. expires_in: type: integer access: type: string enum: - public - signed meta: type: string description: A JSON object as text; returned with the render. storage: type: string description: 'The `storage` option as JSON text: `false`, or `{"key": "…"}`.' responses: '200': description: Converted render. content: application/json: schema: $ref: '#/components/schemas/Render' '413': description: File too large. content: application/problem+json: schema: $ref: '#/components/schemas/Problem' '415': description: Not an accepted office document. content: application/problem+json: schema: $ref: '#/components/schemas/Problem' '503': description: The converter is busy; retry after `Retry-After` seconds. content: application/problem+json: schema: $ref: '#/components/schemas/Problem' default: $ref: '#/components/responses/Problem' /pdf/watermark: post: tags: - PDF tools operationId: watermarkPdf summary: Watermark a PDF description: 'Draws a text or an image diagonally across every page of the PDF output of an earlier render, into a new render (0.5 units on live keys). The text is set in Helvetica Bold, which covers Western European languages; an image is a PNG or JPEG of the workspace file library, scaled to 60 % of the page width. Pass `text` or `image`, not both.' requestBody: required: true content: application/json: schema: type: object required: - source properties: source: type: string description: Render id (`rnd_…`) of a succeeded PDF render. text: type: string maxLength: 200 example: COPY image: type: string maxLength: 200 description: 'Instead of `text`: a PNG or JPEG of the workspace library, by id (`fil_…`) or name.' opacity: type: number minimum: 0 maximum: 1 default: 0.2 rotation: type: number default: -45 description: Degrees clockwise as in CSS; the default runs from bottom left to top right. color: type: string pattern: ^#?[0-9a-fA-F]{6}$ example: '#cc0000' description: Hex colour; grey by default. filename: type: string expires_in: type: - integer - 'null' access: type: string enum: - public - signed meta: type: object additionalProperties: true storage: allOf: - $ref: '#/components/schemas/StorageOption' responses: '200': description: Watermarked render. content: application/json: schema: $ref: '#/components/schemas/Render' default: $ref: '#/components/responses/Problem' /pdf/info: post: tags: - PDF tools operationId: pdfInfo summary: Inspect a PDF description: Page count, page sizes, encryption and metadata of the PDF output of an earlier render. Free on every plan; creates no render. requestBody: required: true content: application/json: schema: type: object required: - source properties: source: type: string description: Render id (`rnd_…`). responses: '200': description: PDF info. content: application/json: schema: type: object properties: source: type: string page_count: type: integer pages: type: array items: type: object properties: width_pt: type: number height_pt: type: number width_mm: type: number height_mm: type: number encrypted: type: boolean metadata: type: object additionalProperties: true default: $ref: '#/components/responses/Problem' components: schemas: 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 RenderStatus: type: string enum: - queued - rendering - succeeded - failed Region: type: string enum: - eu - us - apac 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`. 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 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.' responses: Problem: description: Error (RFC 9457 problem details). 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)