Formfeed PDF tools API
Merge, protect, watermark and inspect PDFs.
Merge, protect, watermark and inspect PDFs.
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.curl "https://apis.io/api/v1/apis/formfeed:formfeed-pdf-tools-api"
curl "https://apis.io/api/v1/apis?limit=25"
Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.
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)