Skip to main content
Version: 9.x

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:9 is the new image. The latest tag moves to 9 with the release: pin :8 until 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_PATH replaces UNOCONVERTER_BIN_PATH. PDFCPU_BIN_PATH, QPDF_BIN_PATH, PDFTK_BIN_PATH and EXIFTOOL_BIN_PATH are 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 8Gotenberg 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/metricsRemoved: 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:

Gotenberg 8
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
Response
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 fieldsGotenberg 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, imageQualityNo 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.

RouteGotenberg 8Gotenberg 9
ChromiumwaitWindowStatuswaitForWindowStatus, which now waits as documented
ChromiumnativePageRangesnativePages
ChromiumgenerateDocumentOutlinenativeBookmarks
ChromiumgenerateTaggedPdfnativeTaggedPdf
Screenshotsquality, 0 to 100quality, 1 to 100
LibreOfficenativePageRangesnativePages
LibreOfficeexportBookmarksnativeBookmarks
LibreOfficegenerateTaggedPdfnativeTaggedPdf
LibreOfficesinglePageSheetssinglePage
LibreOfficepdfanativePdfa: PDF/A-1b, PDF/A-2b, PDF/A-3b or PDF/A-4
LibreOfficepdfua, a booleannativePdfua, the string PDF/UA-1
LibreOfficequalitynativeQuality
LibreOfficelosslessImageCompressionnativeLosslessImageCompression
LibreOfficereduceImageResolutionnativeReduceImageResolution
LibreOfficemaxImageResolutionnativeMaxImageResolution
LibreOfficenativeWatermarkColor#rrggbb, no longer a decimal number
LibreOfficenativeWatermarkFontHeightnativeWatermarkFontSize
LibreOfficenativeWatermarkFontNamenativeWatermarkFont
LibreOfficenativeWatermarkRotateAnglenativeWatermarkRotation, in degrees, counter-clockwise
LibreOfficeinitialView, a numberdefault, outline or thumbnails
LibreOfficemagnification, a numberdefault, fitWindow, fitWidth, fitVisible or zoom
LibreOfficepageLayout, a numberdefault, singlePage, oneColumn or twoColumnRight
LibreOfficefirstPageOnLeftRemoved: LibreOffice ignores it
LibreOfficenativePdfFormatsRemoved: nativePdfa and nativePdfua always apply natively
BothnonepaperSize: A3, A4, A5, B4, B5, Letter, Legal or Tabloid
AlldownloadFrom[].embeddedRemoved: 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. optimize keeps the quality of every image. Lower it where the PDF is made instead: LibreOffice's nativeQuality and nativeReduceImageResolution, or smaller images in the HTML you send Chromium.
  • Merge bookmark options (bookmarks, autoIndexBookmarks, titleBookmarks). merge keeps the bookmarks of each PDF and points them at the merged pages. For a bookmark per file, add a set-bookmarks stage after merge.
  • 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 an attach stage, by the name its server's Content-Disposition header gives.

Metadata and bookmarks​

  • set-metadata writes the eight standard fields: title, author, subject, keywords, creator, producer, creationDate and modDate. Gotenberg 8 wrote any ExifTool tag.
  • /forms/pdf/metadata answers with the same eight fields, not ExifTool's tags.
  • Write dates in the PDF format: D:20260101120000Z.
  • Bookmarks take pageNumber instead of page, and nest with children.

Factur-X​

  • remediate with profile=Factur-X embeds the invoice: xml names the uploaded XML, level its conformance level.
  • Without xml, it repairs the invoice the PDF already carries.
  • facturxVersion is gone. The MINIMUM and BASIC WL levels now embed with the relationship Data, 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-Filename still overrides both.

Webhooks​

  • Gotenberg-Webhook-Error-Url and Gotenberg-Webhook-Error-Method are gone.
  • Gotenberg-Webhook-Events-Url is required with Gotenberg-Webhook-Url. Errors reach it as webhook.error events.

Flags​

Gotenberg 8Gotenberg 9
--pdfengines-*-engines, --pdfengines-enginesRemoved: 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-incognitoRemoved
--webhook-error-allow-list, --webhook-error-deny-listRemoved, 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.x9.x
gotenberg_<engine>_requests_queue_sizegotenberg_<engine>_queue_size
gotenberg_<engine>_restarts_countgotenberg_<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.x9.x
<engine>.requests.activegotenberg.<engine>.request.active
<engine>.requests.queue_sizegotenberg.<engine>.queue.size
chromium.network.bytesgotenberg.chromium.network.response.size
reason, unknown valueerror.type, _OTHER value
outcomegotenberg.chromium.network.request.outcome
gotenberg.queue.depth_at_arrivalgotenberg.queue.size
gotenberg.conversions_since_last_restartgotenberg.process.conversion.count
gotenberg.conversion.input.bytesgotenberg.conversion.input.size
gotenberg.conversion.input.html.bytesgotenberg.conversion.input.html.size
gotenberg.conversion.input.files.countgotenberg.conversion.input.file.count
gotenberg.conversion.output.bytesgotenberg.conversion.output.size
gotenberg.chromium.print.landscapegotenberg.conversion.landscape
gotenberg.chromium.print.has_page_rangesgotenberg.conversion.has_page_ranges
gotenberg.chromium.resources.countgotenberg.chromium.network.request.count
gotenberg.chromium.resources.failed_countgotenberg.chromium.network.request.failed.count
gotenberg.chromium.resources.bytes_totalgotenberg.chromium.network.response.size
gotenberg.chromium.resources.unique_originsgotenberg.chromium.network.origin.count
chromium.heaviest_resource event: url, bytesgotenberg.chromium.heaviest_resource: url.full, gotenberg.chromium.heaviest_resource.size
conversion.retry event: attemptgotenberg.conversion.retry: gotenberg.conversion.retry.attempt
chromium.Pdf, chromium.Screenshot, libreoffice.Pdf spanschromium.pdf, chromium.screenshot, libreoffice.pdf
webhook.Async spanwebhook.async
<method> Webhook, POST Webhook Event spanswebhook.send, webhook.send_event
GET Download From spanapi.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.