Upgrade from 8.x
Gotenberg 9 hands its PDF work to litho, one PDF engine, and turns post-processing into stages you list in order. Most upgrades rename fields and move options into stages.
A Gotenberg 8 request that sends a field Gotenberg 9 moved, renamed or removed answers 400, and the message says where the field went:
Invalid form data: form field 'merge' is unknown to this route; Gotenberg 9 moved it to a stage: add {"op":"merge"} to 'stages'
Invalid form data: form field 'generateDocumentOutline' is unknown to this route; Gotenberg 9 renamed it 'nativeBookmarks'
The image
gotenberg/gotenberg:9is the new image. Thelatesttag moves to 9 with the release: pin:8until you upgrade.- litho takes over from pdfcpu, QPDF, pdftk and ExifTool, the tools behind Gotenberg 8's PDF engines. They leave the image, along with Java, Python and unoconverter. A custom image that calls them must install them itself.
- LibreOffice now runs through lok:
LOK_BIN_PATHreplacesUNOCONVERTER_BIN_PATH.PDFCPU_BIN_PATH,QPDF_BIN_PATH,PDFTK_BIN_PATHandEXIFTOOL_BIN_PATHare gone. - The LibreOffice route no longer accepts
.epub, which LibreOffice never converted. - The variants, architectures, user (UID 1001), port and command don't change.
Routes
The /forms/pdfengines routes become /forms/pdf routes. The old paths answer 404, with the route that replaced each.
| Gotenberg 8 | Gotenberg 9 |
|---|---|
/forms/pdfengines/merge | /forms/pdf/merge |
/forms/pdfengines/split | /forms/pdf/split, or /forms/pdf/select for splitUnify |
/forms/pdfengines/flatten | /forms/pdf/flatten-form |
/forms/pdfengines/optimize | /forms/pdf/optimize |
/forms/pdfengines/convert | /forms/pdf/remediate |
/forms/pdfengines/metadata/read | /forms/pdf/metadata |
/forms/pdfengines/metadata/write | /forms/pdf/set-metadata |
/forms/pdfengines/bookmarks/read | /forms/pdf/bookmarks |
/forms/pdfengines/bookmarks/write | /forms/pdf/set-bookmarks |
/forms/pdfengines/encrypt | /forms/pdf/encrypt |
/forms/pdfengines/embed | /forms/pdf/attach |
/forms/pdfengines/watermark, /stamp | /forms/pdf/stamp-text, /stamp-image, /stamp-pdf |
/forms/pdfengines/rotate | /forms/pdf/rotate |
/forms/pdfengines/factur-x | /forms/pdf/remediate with profile=Factur-X |
/prometheus/metrics | Removed: see Metrics |
The Chromium and LibreOffice routes, /health, /version and /debug keep their paths.
Post-processing becomes stages
Gotenberg 8 took post-processing as flat fields and ran them in a fixed order:
curl \
--request POST http://localhost:3000/forms/libreoffice/convert \
--form files=@annex.docx \
--form files=@report.docx \
--form merge=true \
--form pdfa=PDF/A-2b \
-o report.pdf
Gotenberg 9 takes them as stages, and runs them in the order you list them:
curl \
--request POST http://localhost:3000/forms/libreoffice/convert \
--form files=@annex.docx \
--form files=@report.docx \
--form 'stages=[
{ "op": "merge" },
{ "op": "remediate", "profile": "PDF/A-2b" }
]' \
-o report.pdf
HTTP/1.1 200 OK
Content-Type: application/pdf
Gotenberg-Remediation-Residual: 0
Gotenberg-Remediation-Rolled-Back: 0
The Chromium conversion routes, the LibreOffice route and /forms/pdf/pipe take stages; the screenshot routes don't. Each stage also has its own route, /forms/pdf/<op>, which takes the stage's options as form fields.
| Gotenberg 8 fields | Gotenberg 9 stage |
|---|---|
merge (LibreOffice) | {"op": "merge"} |
splitMode=intervals, splitSpan | {"op": "split", "every": 2} |
splitMode=pages, splitSpan | {"op": "split", "ranges": ["1", "2-3"]}: one PDF per range |
splitMode=pages, splitUnify=true | {"op": "select", "pages": "1-3"} |
pdfa | {"op": "remediate", "profile": "<the level you sent>"} |
pdfua | {"op": "remediate", "profile": "PDF/UA-1"} |
pdfa and pdfua together | {"op": "remediate", "profile": ["<the level you sent>", "PDF/UA-1"]} |
metadata | {"op": "set-metadata", "title": "…"}: see Metadata |
userPassword, ownerPassword, allow* | {"op": "encrypt", "userPassword": "…", "permissions": {"print": false}} |
embeds files, embedsMetadata | {"op": "attach", "file": "data.xml"}, one stage per file |
watermark*, stamp* | stamp-text, stamp-image or stamp-pdf, always over the page |
rotateAngle, rotatePages | {"op": "rotate", "angle": 90, "pages": "1"} |
flatten | {"op": "flatten-form"} |
optimizeImages, imageQuality | No direct equivalent: see below |
facturxXml file and the facturx* fields | {"op": "remediate", "profile": "Factur-X", "xml": "invoice.xml", "level": "…"} |
A stage names the files it needs, such as an image to stamp or an XML to attach, by their uploaded names. Upload them under files: the embeds, watermark, stamp and facturxXml fields are gone.
Gotenberg checks the order of the stages before it runs them, and answers 400 when a list breaks one of the order rules.
Gotenberg 8 converted to PDF/A again after its other steps. Gotenberg 9 doesn't: LibreOffice's nativePdfa applies at export, and the stages after it may break conformance. End the list with a remediate stage instead.
Renamed fields
A field that has a twin among the stages now starts with native. Some values change format too.
| Route | Gotenberg 8 | Gotenberg 9 |
|---|---|---|
| Chromium | waitWindowStatus | waitForWindowStatus, which now waits as documented |
| Chromium | nativePageRanges | nativePages |
| Chromium | generateDocumentOutline | nativeBookmarks |
| Chromium | generateTaggedPdf | nativeTaggedPdf |
| Screenshots | quality, 0 to 100 | quality, 1 to 100 |
| LibreOffice | nativePageRanges | nativePages |
| LibreOffice | exportBookmarks | nativeBookmarks |
| LibreOffice | generateTaggedPdf | nativeTaggedPdf |
| LibreOffice | singlePageSheets | singlePage |
| LibreOffice | pdfa | nativePdfa: PDF/A-1b, PDF/A-2b, PDF/A-3b or PDF/A-4 |
| LibreOffice | pdfua, a boolean | nativePdfua, the string PDF/UA-1 |
| LibreOffice | quality | nativeQuality |
| LibreOffice | losslessImageCompression | nativeLosslessImageCompression |
| LibreOffice | reduceImageResolution | nativeReduceImageResolution |
| LibreOffice | maxImageResolution | nativeMaxImageResolution |
| LibreOffice | nativeWatermarkColor | #rrggbb, no longer a decimal number |
| LibreOffice | nativeWatermarkFontHeight | nativeWatermarkFontSize |
| LibreOffice | nativeWatermarkFontName | nativeWatermarkFont |
| LibreOffice | nativeWatermarkRotateAngle | nativeWatermarkRotation, in degrees, counter-clockwise |
| LibreOffice | initialView, a number | default, outline or thumbnails |
| LibreOffice | magnification, a number | default, fitWindow, fitWidth, fitVisible or zoom |
| LibreOffice | pageLayout, a number | default, singlePage, oneColumn or twoColumnRight |
| LibreOffice | firstPageOnLeft | Removed: LibreOffice ignores it |
| LibreOffice | nativePdfFormats | Removed: nativePdfa and nativePdfua always apply natively |
| Both | none | paperSize: A3, A4, A5, B4, B5, Letter, Legal or Tabloid |
| All | downloadFrom[].embedded | Removed: downloaded files are inputs; see below |
Chromium no longer makes PDF/A on its own: add a remediate stage.
No direct equivalent
Some Gotenberg 8 options have no field of their own in Gotenberg 9:
- Lower image quality on a PDF.
optimizekeeps the quality of every image. Lower it where the PDF is made instead: LibreOffice'snativeQualityandnativeReduceImageResolution, or smaller images in the HTML you send Chromium. - Merge bookmark options (
bookmarks,autoIndexBookmarks,titleBookmarks).mergekeeps the bookmarks of each PDF and points them at the merged pages. For a bookmark per file, add aset-bookmarksstage aftermerge. - Stamp anchors, relative sizes, page numbers and fonts. A stamp takes a position in points from the bottom-left corner, one of the 14 standard PDF fonts, and draws over the page. Number pages with a Chromium footer.
- Embed a downloaded file (
downloadFrom[].embedded). Name it in anattachstage, by the name its server'sContent-Dispositionheader gives.
Metadata and bookmarks
set-metadatawrites the eight standard fields:title,author,subject,keywords,creator,producer,creationDateandmodDate. Gotenberg 8 wrote any ExifTool tag./forms/pdf/metadataanswers with the same eight fields, not ExifTool's tags.- Write dates in the PDF format:
D:20260101120000Z. - Bookmarks take
pageNumberinstead ofpage, and nest withchildren.
Factur-X
remediatewithprofile=Factur-Xembeds the invoice:xmlnames the uploaded XML,levelits conformance level.- Without
xml, it repairs the invoice the PDF already carries. facturxVersionis gone. The MINIMUM and BASIC WL levels now embed with the relationshipData, as Factur-X requires.
Output names
- A merged PDF takes the name of its first file, in alphanumeric order. Gotenberg 8 named it at random.
- LibreOffice names its PDFs after their documents,
report.docx.pdf, even for a single document. Gotenberg-Output-Filenamestill overrides both.
Webhooks
Gotenberg-Webhook-Error-UrlandGotenberg-Webhook-Error-Methodare gone.Gotenberg-Webhook-Events-Urlis required withGotenberg-Webhook-Url. Errors reach it aswebhook.errorevents.
Flags
| Gotenberg 8 | Gotenberg 9 |
|---|---|
--pdfengines-*-engines, --pdfengines-engines | Removed: litho does all the PDF work |
--pdfengines-disable-routes | --pdf-disable-routes |
--pdfengines-max-concurrency | --pdf-max-concurrency |
--prometheus-* | Removed: see Metrics |
--api-trace-header | --api-correlation-id-header |
--api-disable-health-check-logging | --api-disable-health-check-route-telemetry |
--log-format | --log-std-format |
--log-enable-gcp-fields | --log-std-enable-gcp-fields |
--chromium-incognito | Removed |
--webhook-error-allow-list, --webhook-error-deny-list | Removed, with the error URL |
New flags: --pdf-max-stream-bytes, --pdf-max-embedded-file-bytes and --pdf-max-image-pixels bound what litho reads. --pdf-allow-list, --pdf-deny-list and their siblings filter the timestamp authorities litho calls. --api-archive-max-entries and --api-archive-max-ratio bound the ZIPs sent under archives. --libreoffice-enable-macros lets documents run macros, which stay off by default. --log-level=debug no longer logs the messages between Gotenberg and Chromium: --chromium-debug-protocol brings them back.
The other flags keep their names and defaults.
Metrics
The Prometheus module is gone. Gotenberg exports its metrics with OpenTelemetry, which can still serve them to Prometheus:
docker run --rm -p "3000:3000" -p "9464:9464" \
-e OTEL_METRICS_EXPORTER=prometheus \
-e OTEL_EXPORTER_PROMETHEUS_HOST=0.0.0.0 \
gotenberg/gotenberg:9
Prometheus then scrapes http://localhost:9464/metrics. The module's metrics change names:
| 8.x | 9.x |
|---|---|
gotenberg_<engine>_requests_queue_size | gotenberg_<engine>_queue_size |
gotenberg_<engine>_restarts_count | gotenberg_<engine>_process_restarts_total, a counter |
The OpenTelemetry names follow the OpenTelemetry naming rules. They start with gotenberg. and drop .total: chromium.requests.total becomes gotenberg.chromium.requests. <engine>.errors.total and the status attribute are gone: a failed request carries an error.type on gotenberg.<engine>.requests and the durations. Some names change further, and so do attributes and spans:
| 8.x | 9.x |
|---|---|
<engine>.requests.active | gotenberg.<engine>.request.active |
<engine>.requests.queue_size | gotenberg.<engine>.queue.size |
chromium.network.bytes | gotenberg.chromium.network.response.size |
reason, unknown value | error.type, _OTHER value |
outcome | gotenberg.chromium.network.request.outcome |
gotenberg.queue.depth_at_arrival | gotenberg.queue.size |
gotenberg.conversions_since_last_restart | gotenberg.process.conversion.count |
gotenberg.conversion.input.bytes | gotenberg.conversion.input.size |
gotenberg.conversion.input.html.bytes | gotenberg.conversion.input.html.size |
gotenberg.conversion.input.files.count | gotenberg.conversion.input.file.count |
gotenberg.conversion.output.bytes | gotenberg.conversion.output.size |
gotenberg.chromium.print.landscape | gotenberg.conversion.landscape |
gotenberg.chromium.print.has_page_ranges | gotenberg.conversion.has_page_ranges |
gotenberg.chromium.resources.count | gotenberg.chromium.network.request.count |
gotenberg.chromium.resources.failed_count | gotenberg.chromium.network.request.failed.count |
gotenberg.chromium.resources.bytes_total | gotenberg.chromium.network.response.size |
gotenberg.chromium.resources.unique_origins | gotenberg.chromium.network.origin.count |
chromium.heaviest_resource event: url, bytes | gotenberg.chromium.heaviest_resource: url.full, gotenberg.chromium.heaviest_resource.size |
conversion.retry event: attempt | gotenberg.conversion.retry: gotenberg.conversion.retry.attempt |
chromium.Pdf, chromium.Screenshot, libreoffice.Pdf spans | chromium.pdf, chromium.screenshot, libreoffice.pdf |
webhook.Async span | webhook.async |
<method> Webhook, POST Webhook Event spans | webhook.send, webhook.send_event |
GET Download From span | api.download |
Spans leave their status unset on a success. The server span also leaves it unset on a 4xx, and sets Error on a 5xx, with the status code as error.type and the error message as the description, in place of its error attribute. Each webhook and download attempt gets its own HTTP client span, POST or GET, under the span of the whole call. It carries the host as server.address, and url.full without the URL's credentials, query and fragment. The logs name the URLs of webhooks, downloads and Chromium's requests the same way, and so does the 409 that lists the resources failing failOnResourceHttpStatusCodes.
Custom builds
A Go program that builds its own Gotenberg imports github.com/gotenberg/gotenberg/v9. The PdfEngine, PdfEngineProvider and MetricsProvider interfaces are gone. ErrorTypeUnknown is now ErrorTypeOther.