Docs
Run the container. Then read the stylesheets.
Everything the container does is reachable from one image and one port. The stylesheets, the semantic contract and the parameter surface are Apache-2.0 and documented in the open repository, linked at the end.
Run the container
No signup, no license, no network calls out. The image is public,
multi-arch (amd64, arm64), runs as a non-root user on a read-only root
filesystem, and needs only a writable /tmp.
docker run -d --name faktorei --read-only --tmpfs /tmp -p 8080:8080 ghcr.io/faktorei/render:2026.05
Without a license file the container runs in evaluation mode:
every feature works, output carries a watermark, and the shared allowance is
500 documents per 24 hours across /render and /batch.
A license file removes both; nothing else changes.
| Check | Returns |
|---|---|
GET /health | {"status":"ok","license":"evaluation"} — license is evaluation, ok, expiring, expired-grace or refused |
GET /version | image version and the spec cycle it validates against (spec_release, currently 2026.05) |
GET /metrics | Prometheus text: renders by syntax/profile/outcome, latency, faktorei_build_info |
HTTP API
Three routes, all POST, all taking the document as the raw
request body and answering with the finished file. The syntax (UBL
invoice, UBL credit note, CII) is detected from the document; the national
profile is detected from BT-24 unless you force one.
POST /render — one document to PDF/A-3b
curl -sS --data-binary @invoice.xml "http://localhost:8080/render?lang=de" -o invoice.pdf Query parameter Values Default langen · de · fr · nl (English, German, French, Dutch); also selects the number formaten profileauto · generic · xrechnung · pint-a-nzauto (from BT-24) pdfafalse to emit plain PDF instead of PDF/A-3bPDF/A-3b
Body: the XML, any encoding declared in the prolog. Response: application/pdf. Output is byte-deterministic — the same input and options produce the same bytes on every run and every architecture, because every date in the PDF is pinned to the invoice's issue date.
POST /render/facturx — hybrid Factur-X / ZUGFeRD
curl -sS --data-binary @invoice-cii.xml http://localhost:8080/render/facturx -o invoice-facturx.pdf
Takes a CII document, renders it, and embeds the original XML
byte-exact as factur-x.xml with the Factur-X XMP extension schema.
The conformance level (MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED, XRECHNUNG)
is read from the document's guideline identifier; profile forces
the presentation profile as for /render. Always PDF/A-3b.
POST /batch — zip in, zip out
zip -j batch.zip in/*.xml
curl -sS --data-binary @batch.zip "http://localhost:8080/batch?lang=en" -o out.zip
unzip -l out.zip # one PDF per input + report.json
The body is a zip of XML documents (entry names are flattened to their
basename). The response is a zip with one PDF per input and a
report.json listing every document with ok or the
error that stopped it — a failing document never stops the others. Query
lang and profile set job-wide defaults; add
facturx=true to package every CII document as a hybrid. Put a
manifest.jsonl in the zip to set lang,
profile and the output filename per document (format:
batch manifest). Inputs are
streamed to disk, never buffered in memory, so a 2 GB batch is fine.
Status codes
Code Meaning 400Empty body, not an EN 16931 document, or an invalid manifest (the message names the line). 413Document or batch over a configured ceiling (see configuration). The message names the limit. 422Glyph coverage: the document contains characters the embedded fonts cannot draw. The engine refuses rather than print placeholder boxes (policy). 429Evaluation allowance exhausted, with Retry-After. A batch larger than the remaining allowance is refused at submission, not mid-way.
Errors are text/plain with one line that says what to change. The engine logs nothing about document content.
Install a license
A license is one signed file, faktorei.lic, verified offline
with a public key baked into the image. Mount it read-only at
/app/license/faktorei.lic (or point FAKTOREI_LICENSE
at it). The container watches the file: replacing it at renewal needs no
restart.
docker run -d --name faktorei --read-only --tmpfs /tmp -p 8080:8080 \
-v "$PWD/faktorei.lic:/app/license/faktorei.lic:ro" ghcr.io/faktorei/render:2026.05
curl -s localhost:8080/health # {"status":"ok","license":"ok"}
What happens at expiry. Thirty days before, /health
reports expiring. After expiry there is a 30-day grace period
(expired-grace, full function), then the container degrades to
evaluation mode — watermark and daily allowance — and keeps rendering. It never
hard-stops. A file that fails signature verification is refused:
at startup that is a hard error (the container exits, so a broken deploy is
visible); at runtime a hot-swapped bad file is logged and the previous valid
license stays in effect. Licenses issued since September 2026 require image
2025.11.5 or later.
Licenses are per deployment, not per instance: run as many replicas as you
like with the same file. Pricing and the license agreement →
Configuration
Everything is an environment variable with a safe default. Ceilings are enforced before any transform starts and answer with 413.
Variable Default What it does FAKTOREI_PORT8080 HTTP listen port. FAKTOREI_WORKERScores Parallel renders per process, for /batch and the CLI. Set to the replica's physical cores; rendering scales out with replicas better than up with cores (why). FAKTOREI_LICENSE/app/license/faktorei.licPath of the license file. FAKTOREI_MAX_DOC_MB10 Largest single document, in MB. FAKTOREI_MAX_LINES10000 Most invoice lines in one document. The 500-line corpus fixture renders in about 62 MB of heap; size the JVM for your own ceiling. FAKTOREI_MAX_ZIP_DOCS10000 Most documents in one /batch zip. FAKTOREI_MAX_BATCH_MB2048 Largest /batch body, inflated, in MB. FAKTOREI_MAX_BATCH_DOCS100000 Most documents in one CLI batch run. FAKTOREI_BATCH_TMP/tmpScratch directory for /batch staging. Point it at a large volume for big jobs. FAKTOREI_PRICING_URLfaktorei.dev/pricingThe URL printed in the evaluation watermark.
Command line
The same image runs as a one-shot CLI over mounted directories — the HTTP
service and the CLI share one render core, and CI asserts their output is
byte-identical. This is the path for archive migrations and nightly runs.
# same image, same core — no server needed
docker run --rm -v "$PWD:/w" ghcr.io/faktorei/render:2026.05 render /w/invoice.xml /w/invoice.pdf --lang de
docker run --rm -v "$PWD:/w" ghcr.io/faktorei/render:2026.05 facturx /w/invoice-cii.xml /w/invoice-facturx.pdf
docker run --rm -v "$PWD:/w" ghcr.io/faktorei/render:2026.05 batch /w/in /w/out --workers 4 --manifest /w/in/manifest.jsonl --resume
Command Does render <in> <out> [--lang L] [--profile P] [--pdfa] [--emit semantic|fo|pdf]One document. --emit stops at an intermediate stage for debugging. facturx <cii.xml> <out.pdf> [--lang L] [--profile P]One hybrid. batch <in-dir> <out-dir> [--workers N] [--lang L] [--profile P] [--facturx] [--manifest m.jsonl] [--resume]Every .xml in a directory, concurrently; writes each PDF atomically and a report.json. --resume continues an interrupted run from its checkpoint and refuses if the manifest changed. serveThe HTTP service (the image's default command). healthcheckExit 0 if the service answers; used by the image's own HEALTHCHECK.
Exit codes: 0 all documents rendered · 1 some failed (see report.json) · 2 usage · 4 refused before starting (ceiling, allowance, or a changed manifest on --resume).
Deploy
The container is stateless: no database, no outbound network, nothing
written outside /tmp. Run it as you run any stateless service.
Reference compose:
services:
faktorei:
image: ghcr.io/faktorei/render:2026.05
ports: ["8080:8080"]
read_only: true
tmpfs: ["/tmp"]
environment:
FAKTOREI_WORKERS: "4" # = the replica's cores; scale out, not up
volumes:
- ./faktorei.lic:/app/license/faktorei.lic:ro
healthcheck:
test: ["CMD", "/app/faktorei-engine/bin/faktorei-engine", "healthcheck"]
interval: 30s
timeout: 5s
start_period: 20s
retries: 3
- Sizing. Several 2–4 vCPU replicas behind a load balancer beat one large node; the benchmark shows the curve and the per-core figure to size from.
- Air-gapped. Nothing phones home: not the license check, not the fonts, not the stylesheets. Pull the image once, carry it in.
- Pinning.
:2026.05 is the current spec cycle and moves to each patch within it; :2026.05.N pins a build. There is deliberately no :latest, because crossing a spec cycle should be your decision. - Updates. A new cycle ships within three weeks of each Peppol / EN 16931 release and of each national profile's release; the
spec_release in /version says which rules an image validates against.
Reference, in the open repository
The stylesheets, the semantic contract, the parameter surface, and the
test corpus are all Apache-2.0 and documented in-repo.
Quickstart & overview
Render a fixture in two commands; the two-phase architecture.
Benchmark methodology
How the throughput number is measured — and reproduced.
Semantic model
The EN 16931 vocabulary every syntax normalizes to — the contract.
Parameter surface
The stable public API: language, profile, theme tokens.
National profiles
XRechnung and PINT A-NZ: what the profile parameter changes, and the BT-24 auto-detect.
Batch manifest format
JSONL per-document options, resumability, the CSV converter.
Glyph coverage policy
Why the engine refuses missing-glyph content rather than emit tofu.
Contributing
Send us an invoice that breaks a renderer. The DoD and the CLA.