deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

How a self-hosted Flask app met Belgium's mandatory Peppol e-invoicing rules

Belgium now requires B2B invoices as structured e-invoices over Peppol. A dev.to post explains how TimeTracker's maintainer added EN 16931 support to a self-hosted Flask app.

How a self-hosted Flask app met Belgium's mandatory Peppol e-invoicing rules

A mandate turned a nice-to-have into a must-have

Since 1 January 2026, Belgian businesses have had to exchange B2B invoices as structured e-invoices over the Peppol network; a PDF attached to an email no longer counts. That change is what pushed the maintainer of TimeTracker, an open-source, self-hosted time tracker with built-in invoicing aimed largely at Belgian freelancers and teams, to add e-invoicing support. In a post on dev.to, the author documents the standards involved, the architecture that made the work manageable for a self-hosted application, and the parts that proved harder than expected. The post carries a disclaimer: it is not legal or tax advice, and readers should consult official guidance from the Belgian FPS Finance.

Untangling the standards

The post starts by separating acronyms that blur together. EN 16931 is the European semantic data model: it defines which fields an e-invoice must contain — seller, buyer, VAT breakdown, totals — but not a file format. UBL 2.1 and CII are the two XML syntaxes that can carry that data. Peppol BIS Billing 3.0 adds the Peppol network's rules on top of EN 16931, expressed in UBL. Peppol itself is transport: a network where businesses connect through certified Access Points, the so-called 4-corner model. Factur-X and ZUGFeRD are a hybrid format, a human-readable PDF with CII XML embedded inside. Because Peppol wants UBL and Factur-X wants CII, TimeTracker generates both.

Don't become an Access Point

In principle an application can speak Peppol itself: look the recipient up in the SML/SMP directory and send over AS4 with the right certificates and signatures. The author built an experimental native mode that does the lookup and sending, but it lacks WS-Security, digital signatures and receipt handling, and Access Point certification is a business in itself — the wrong layer for a self-hosted app used by freelancers.

The recommended path is a thin HTTP contract. TimeTracker builds the UBL document and POSTs it, with routing metadata such as sender and recipient endpoint IDs, document type and process ID, to an adapter URL. The adapter forwards the document to whichever certified Access Point provider the user already has. Any HTTP status of 400 or above marks an attempt as failed, and every attempt is stored so the invoice page can show a full send history.

A bridge sidecar isolates provider quirks

To avoid asking self-hosters to write their own adapter, the project ships a small peppol-bridge service that runs alongside the app in Docker Compose and exposes /health, /test and /send. Provider differences stay inside the bridge — one provider authenticates with an X-Api-Key header instead of a Bearer token — so adding a new provider means adding a preset rather than changing the invoicing code. An admin setup wizard generates the Compose snippet and points the app at the bridge.

Identifiers and one-toggle compliance

Peppol parties are identified by a scheme ID plus an endpoint ID; for Belgian companies that is typically scheme 0208 with the enterprise number. Two lessons stand out: validate both sender and recipient identifiers before anything leaves the server, since malformed IDs otherwise fail deep inside a provider's API with unhelpful errors; and store endpoints per client, with the send button appearing only when both parties are configured.

Most users do not want to think about BT-codes, so a single admin toggle adds the mandatory BIS Billing 3.0 elements — such as InvoiceTypeCode 380 and a buyer reference, falling back to the project name and then the invoice number — shows warnings when a company VAT ID or an endpoint is missing, and adds a Download UBL button for users whose accountant handles the actual sending.

Factur-X: the PDF was the hard part

Embedding XML in a PDF sounds trivial, but validators are strict about how. The export embeds factur-x.xml as a PDF Associated File with the correct relationship, MIME type and Factur-X XMP metadata, optionally normalises the file to PDF/A-3b in a single pikepdf pass, embeds Liberation fonts because PDF/A validators reject the unembedded base-14 fonts many generators default to, and can run veraPDF to verify conformance. The author's rule for anyone doing this: fail loudly. If embedding fails, the export aborts rather than quietly producing a plain PDF the user believes is compliant, and pre-export checks block downloads when a seller or buyer country is missing or reverse charge is used without a buyer VAT ID.

Why it matters

Belgium's mandate applies to any software its businesses use to bill each other, including self-hosted and open-source tools with no vendor to handle compliance on their behalf. This post is a working template for that situation: stay out of the Access Point business, push provider differences into a sidecar, validate identifiers early, surface missing data before the send button appears, and refuse to emit files that only look compliant. For maintainers of invoicing features in any jurisdiction adopting similar rules, the hard-won details — font embedding for PDF/A, fail-loud exports, per-client endpoints — are the genuinely useful part.

  • #peppol
  • #e-invoicing
  • #open-source
  • #self-hosted
  • #flask

Related posts