Skip to main content

PDF tools

Formfeed can finish a PDF after it is rendered: append other documents, stamp a watermark across every page, and lock it with a password. There are two ways to use this.

A third endpoint makes the PDF in the first place: /pdf/convert turns a Word, Excel, PowerPoint, OpenDocument, RTF or HTML file, or the output of a Word or PowerPoint template, into a PDF render.

Sources​

Every place that reads an existing PDF (post.merge_after, and sources or source of the tools) takes two kinds of reference, mixed in any order:

  • A render id (rnd_…) of a succeeded PDF render in the same workspace whose output has not expired or been deleted. A raw UUID is read as a render id too.
  • A PDF in the file library, by its name, such as terms.pdf or legal/terms.pdf, or by its file id (fil_…). A library file does not expire, so a fixed terms-and-conditions page belongs there. A watermark image is a PNG or JPEG of the library, named the same way.

Formfeed resolves every reference inside your workspace before anything runs. A customer URL is never a source, and neither is a render of another workspace. Renders whose outputs are stored in different regions cannot be processed together, and merge_after cannot append outputs stored in another region than the one the render runs in (both 400 invalid_request). Library files have no region and combine with anything.

Cost, plans and scopes​

OperationUnits (live key)PlansScope
Each post step (merge, watermark, password)0.5 on top of the renderStarter and aboverender:create
/pdf/merge, /pdf/protect, /pdf/watermark0.5 per call, however many pagesStarter and aboverender:create
/pdf/convert1 per started 5 pages of the PDF, like a rendered PDFStarter and aboverender:create
/pdf/infofreeevery planrender:read

Test keys cost nothing, as for renders; see units. On the Free plan the tools answer 403 forbidden with "feature": "post_processing", and /pdf/convert answers it with "feature": "office", for test keys too. An e-invoice (post.einvoice) is a step of its own with its own price and plan rule; see below.

The /pdf/* endpoints count against your plan's requests per second like POST /renders (429 rate_limited above it), and a live key whose units are used up gets 402 quota_exceeded, as for a render.

Post-processing in the render​

{
"template": "invoice",
"data": { "invoice": { "number": "2026-0042" } },
"post": {
"merge_after": ["rnd_3b52891defac4cea87d80abc195d2d3f", "terms.pdf"],
"watermark": { "text": "COPY", "opacity": 0.2 },
"password": { "user": "open-sesame", "permissions": ["print"] }
}
}
KeyTypeWhat it does
merge_afterarray of 1 to 49 sourcesAppended after the rendered document, in the given order.
watermarkobjectThe fields of /pdf/watermark without source: text or image, opacity, rotation, color.
passwordobjectuser (needed to open), owner (lifts the restrictions) and permissions, as user_password, owner_password and permissions of /pdf/protect. Note the shorter names here.
einvoiceobject or falseMakes the PDF a ZUGFeRD or Factur-X e-invoice; see below.

The steps always run in the same order, whatever order you write them in:

  1. merge_after: the PDFs are appended after the rendered document (up to 49, so at most 50 documents in one file). The plan's page limit applies to the merged result, and the render's page_count counts the appended pages.
  2. watermark: drawn across the middle of every page, merged pages included.
  3. password: the finished document is encrypted with AES-256. This step comes last because an encrypted PDF cannot be edited any more.
  4. einvoice, where there is one: the e-invoice is made from the document as it will be delivered.

A render with post costs its normal units plus 0.5 per step; the units of the render itself are counted on the pages it rendered, not on the pages it appended. The example above renders one page (1 unit) and runs three steps (1.5 units), 2.5 units in total. post works only for output: "pdf", and an unknown key in post is refused with 400 invalid_request rather than ignored. Post-processed renders are never served from the deduplication cache, because their merge sources can change.

Async renders and batch items take post too. The references are resolved when you send the request, so a source that is deleted or expires before the render runs makes it fail with source_not_found.

With an e-invoice or PDF/UA-1​

  • E-invoice. post.einvoice (or a template that declares an e-invoice) runs after the other steps and costs 1 unit on top. Live e-invoices need the Starter plan; test renders make a limited number a day on every plan (plans and units). A watermark goes with it, as text or as an image. A password does not: post.password with an e-invoice answers 400 invalid_request with "field": "post.password", because PDF/A forbids encryption. PDFs you append must meet PDF/A too; one whose fonts are not embedded fails the render with pdfa_conversion_failed.
  • PDF/UA-1. merge_after cannot be combined with settings.pdf.ua (400 invalid_request, "field": "post.merge_after"): a merge drops the tags of the documents it appends. A watermark and a password do go with it; the watermark is marked as an artifact, so it does not disturb the tags, and the password is added after the file was checked.
  • The tools do not check. The results of /pdf/merge, /pdf/watermark and /pdf/protect are never validated again: their einvoice and accessibility are null. Merging PDF/UA files does not give a PDF/UA file, and a merged e-invoice carries no XML any more.

The PDF tools​

# merge an earlier render and a library PDF into one file
curl https://api-eu.formfeed.dev/v1/pdf/merge \
-H "Authorization: Bearer $FORMFEED_API_KEY" \
-H "content-type: application/json" \
-d '{"sources": ["rnd_7da09ab6cd9946b798c187c2a9350852", "terms.pdf"], "filename": "bundle.pdf"}'

# watermark the merged render (its id is in the answer above), then protect the watermarked one
curl https://api-eu.formfeed.dev/v1/pdf/watermark \
-H "Authorization: Bearer $FORMFEED_API_KEY" -H "content-type: application/json" \
-d '{"source": "rnd_745a62a800e44beeb3993bb03c8956a3", "text": "DRAFT", "color": "#cc0000"}'
curl https://api-eu.formfeed.dev/v1/pdf/protect \
-H "Authorization: Bearer $FORMFEED_API_KEY" -H "content-type: application/json" \
-d '{"source": "rnd_50136336b95245628f661fdc527bbf8a", "user_password": "open-sesame", "permissions": ["print"]}'

The SDKs accept a render object or its id, and a library PDF by its name ('terms.pdf'). They send an Idempotency-Key with every call that creates a render and reuse it on a retry, so a retry after the first answer is not charged again; see idempotency for the one case it does not cover, a retry while the first call still runs.

Merge, watermark and protect return a render like POST /v1/renders does, synchronously and with status 200: download_url, page_count, units (0.5, or 0 with a test key), template: null. It shows up in the render log and under GET /v1/renders/{id}. The sources stay as they are. The new file is stored in the region of its sources, or in your key's region when every source is a library file.

Output options​

Every tool that creates a render, /pdf/convert included, takes these optional fields beside its own:

FieldTypeDefaultMeaning
filenamestring, up to 200 charactersmerged.pdf, watermarked.pdf, protected.pdf; for convert see belowName of the download.
accesssigned or publicthe workspace's settingA signed link expires; a public one lives as long as the file. See download links.
expires_insecondsthe retention of your plan and workspaceKeeps the result for a shorter time; it never lengthens the retention. Test-key results are kept 24 hours at most.
metaobject{}Your own values, returned with the render and in its webhooks.
storagefalse or { "key": "…" }the workspace's connectionThe copy in your own bucket, as for a render.

merge​

sources takes 2 to 50 sources: render ids and library PDFs, in any mix. They are joined in the given order, and the same source may appear more than once. The merged PDF does not carry over the sources' titles or other metadata, their tags or their attachments; use filename to name the download.

watermark​

FieldTypeDefaultMeaning
sourcestringrequiredA source: render id or library PDF.
textstring, 1 to 200 charactersone of text, imageSet in Noto Sans Bold, which is embedded in the file. It covers Latin, Greek and Cyrillic letters; text with any other character answers 400 invalid_request, so use an image for other scripts.
imagestringone of text, imageThe name of a PNG or JPEG in the file library. Another type answers 422 source_invalid, an unknown name 422 source_not_found.
opacitynumber, 0 to 10.20 is invisible, 1 opaque.
rotationnumber, −360 to 360text -45, image 0Degrees clockwise as in CSS. -45 runs from bottom left to top right, 0 is horizontal.
colorhex colour #rrggbb (the # is optional)a neutral greyThe colour of the text, such as #cc0000. Ignored for an image.

The text is sized to span about 70 % of the page diagonal, but never larger than 160 pt, so a short word such as COPY stays at 160 pt. It is centred on each page, so portrait and landscape pages in one document both look right. An image is scaled to fit within 60 % of the page's width and height, keeping its aspect ratio, and centred the same way. Pass text or image: both, or neither, answer 400 invalid_request. Text and image are marked as artifacts, so a tagged PDF stays tagged.

protect​

FieldTypeDefaultMeaning
sourcestringrequiredA source: render id or library PDF.
user_passwordstring, 1 to 127 characters, no line breaksnoneNeeded to open the document. Without it anyone can open the PDF, and only the permissions apply.
owner_passwordstring, 1 to 127 characters, no line breaksa random oneLifts the restrictions in viewers that respect them. A random one, which nobody knows, is used when you leave it out.
permissionsarray of print, copy, modify, annotateall four allowedWhat readers may do. Everything you do not list is forbidden; an empty list [] allows nothing.

Send at least one of the three; an empty request answers 400 invalid_request. The encryption is AES-256. Permissions are honoured by standard PDF viewers but are not a security boundary against someone who can open the file. Use a user_password for confidentiality. A protected PDF cannot be merged, watermarked or protected again (422 source_encrypted), so protect as the last step.

info​

curl https://api-eu.formfeed.dev/v1/pdf/info \
-H "Authorization: Bearer $FORMFEED_API_KEY" -H "content-type: application/json" \
-d '{"source": "rnd_50136336b95245628f661fdc527bbf8a"}'
{
"source": "rnd_50136336b95245628f661fdc527bbf8a",
"page_count": 3,
"pages": [
{
"width_pt": 595.28,
"height_pt": 841.89,
"width_mm": 210,
"height_mm": 297
}
],
"encrypted": true,
"metadata": {
"title": null,
"author": null,
"subject": null,
"producer": null,
"creator": null
}
}

Returns the page count, the size of every page in points and millimetres (one entry per page; the example is shortened), whether the PDF is encrypted, and its metadata: title, author, subject, producer and creator. An encrypted PDF reports its pages, but every metadata field is null. source takes a render id or a library PDF. It is free on every plan, needs only render:read and creates no render.

Convert office documents​

POST /v1/pdf/convert turns a Word, Excel, PowerPoint, OpenDocument, RTF or HTML file into a PDF render. The conversion runs in LibreOffice, not in the Chromium engine that renders templates. It takes one of two sources:

  • An uploaded file. Send multipart/form-data with the document as file (up to 20 MB) and the options as form fields. The file is converted and not kept; only the PDF is stored, like any render. The conversion runs in your key's region.
  • The output of a Word or PowerPoint template. Send JSON with source, the id of a succeeded render whose output is docx or pptx (see Word and PowerPoint templates). Any other render answers 422 source_invalid. The conversion runs in the region where that output is stored.
# an uploaded file
curl https://api-eu.formfeed.dev/v1/pdf/convert \
-H "Authorization: Bearer $FORMFEED_API_KEY" \
-F file=@offer.docx \
-F page_ranges=1-3 \
-F filename=offer.pdf

# the output of a Word template render
curl https://api-eu.formfeed.dev/v1/pdf/convert \
-H "Authorization: Bearer $FORMFEED_API_KEY" -H "content-type: application/json" \
-d '{"source": "rnd_7da09ab6cd9946b798c187c2a9350852", "filename": "offer.pdf"}'

From the command line, npx formfeed pdf convert offer.docx --page-ranges 1-3 uploads the file and saves offer.pdf; a render id in place of the file converts that render's output. In the app, Office to PDF on the Renders page converts an uploaded file.

FieldTypeDefaultMeaning
filefileone of file, sourceThe document, up to 20 MB (413 file_too_large above that). Multipart only.
sourcestringone of file, sourceRender id of a succeeded render with output docx or pptx. JSON only.
page_rangesstring, up to 100 charactersall pagesPages to convert, such as 1-3,5: numbers, ranges with -, separated by ,, no spaces.
landscapebooleanfalseLandscape for spreadsheets and other documents without their own page setup.
single_page_sheetsbooleanfalseEach spreadsheet sheet on one page.
filenamestring, up to 200 charactersthe uploaded name with .pdf; for source, <render id>.pdfName of the PDF.
access, expires_in, meta, storageThe output options. In a multipart request, meta is a JSON object as text (up to 16,000 characters), storage is JSON text too (false or {"key": "…"}), and the booleans are true or false.

Accepted formats. The type is read from the file's content, never from its name:

  • Word processing: DOCX, DOC, ODT, RTF
  • Spreadsheets: XLSX, XLS, ODS
  • Presentations: PPTX, PPT, ODP
  • HTML, including Word's "save as web page" and report exports that carry a .doc name. The converter loads nothing from the web: references to images, stylesheets and other files outside the document are removed before the conversion, so they stay empty. For HTML you control, a render with an html source gives a better result.

Macro-enabled files, Office templates and slide shows (.dotx, .xltx, .potx, .ppsx), binary workbooks (.xlsb) and any other type answer 415 file_type_unsupported. Password-protected documents answer 422 source_encrypted, damaged files 422 office_document_invalid, and a document that unpacks to more than the converter takes (more than 2,000 parts, more than 200 MB in all, or a single part above 20 MB) 422 office_document_too_large.

Fonts. A font the document uses but neither embeds nor finds among the converter's fonts is replaced. Calibri, Cambria, Arial and Times New Roman have metric-compatible substitutes (Carlito, Caladea, Liberation), so lines and pages break the same; other fonts can change the layout. Embed fonts in Word (File › Options › Save › Embed fonts in the file) for an exact result.

Synchronous. The response is the finished render (output: "pdf", page_count, units); a conversion never answers 202. It may take up to 60 seconds; a document that takes longer answers 504 render_timeout. Chain /pdf/protect, /pdf/watermark or /pdf/merge on the returned id.

When the converter is busy, the API answers 503 office_unavailable with a Retry-After header, and nothing is charged. Retry after that many seconds with the same idempotency key; the SDKs do this for you.

Errors​

CodeStatusWhen
invalid_request400A wrong or missing field, too few sources, sources stored in different regions, post on a non-PDF output, text and image together (or neither), watermark text the font cannot draw, protect without a password or permissions, a password longer than 127 characters or with a line break, post.password with an e-invoice, merge_after with pdf.ua.
quota_exceeded402A live key whose units are used up, as for renders.
forbidden403The plan does not include post-processing ("feature": "post_processing") or office conversion ("feature": "office"), or the key lacks the scope.
file_too_large413The uploaded document is larger than 20 MB.
file_type_unsupported415The upload is not an accepted document, or it contains macros.
source_not_found422A render id does not exist in this workspace, its output was deleted or has expired, or no library file has that name. The problem names the field, the position in index and, for a render, its render_id.
source_invalid422The render failed or has not finished, it produced something other than a PDF, the library file is not a PDF (or, for a watermark, not a PNG or JPEG), or the file cannot be read as a PDF. For /pdf/convert: the render's output is not docx or pptx, or the converter refused the document.
source_encrypted422The source is password-protected. Protect as the last step.
page_limit_exceeded422The merged or converted document has more pages than the plan allows.
output_too_large422The result is larger than the plan's output size.
office_document_invalid422The document is damaged or fails the safety checks.
office_document_too_large422The document unpacks to more than the converter takes.
office_conversion_failed422LibreOffice could not convert the document. Open it in its application, save it again and retry.
tool_failed500The PDF could not be processed. Retry, or contact support with the request_id.
office_unavailable503The converter is busy or not reachable. Retry after Retry-After seconds.
region_unavailable503The region is busy or restarting. Retry after Retry-After seconds.
render_timeout504A conversion did not finish within 60 seconds.

A call that fails while the PDF is being processed is recorded as a failed render in the render log and costs nothing. A request refused before that (a wrong field, an unknown source, the plan) leaves no render behind.