Clark
The clerk: writes the invoice, keeps the record, sends it out.
Belgian B2B invoicing over Peppol — UBL 2.1 and EN 16931, validated against the official rule set before anything leaves the building.
- Belgian B2B mandate
- 2026 Belgian B2B mandate
- validated against
- EN 16931 validated against
- for other applications
- API for other applications
- Role
- Product, architecture, build
- Period
- 2026 — now
- Stack
- Laravel 13 Livewire 3 UBL 2.1 EN 16931 Schematron bcmath Docker
The problem
Structured e-invoicing is now the way Belgian businesses invoice each other, and a rejected document is one the customer never received. The usual way an invoicing tool goes wrong is quiet: the totals on screen and the totals on the wire drift apart, and nobody finds out until an Access Point refuses the file. That failure is designed out here rather than tested for.
How it works
Everything on screen is derived from the same object the UBL writer receives, computed in bcmath, so the two cannot disagree. Compliance is checked while you type — in plain language, with the rule code underneath — because a missing buyer reference is trivial to fix now and archaeology after a rejection. The Peppol work is a standalone package that knows nothing about any application's schema; Clark is the interface and the archive.
What is in it
Compliance while typing
The cheap PHP rules run per keystroke and read as sentences. The full Schematron needs a JVM and fifteen seconds of stylesheet compilation, so it runs once, before transmission — the same artefacts the receiving Access Point will run.
Validate, freeze, transmit — in that order
The UBL and the rendered PDF are written to storage before anything is sent. From the moment it goes out the document belongs to the recipient too; re-rendering it later from a since-edited template would quietly produce a different document from the one they hold. A test asserts the archive is byte-identical to what was transmitted.
Nothing retries by itself
A refusal leaves the invoice editable with the reason attached — the state a person can act on. Reopening it shows why it came back. A rendering failure never stops a valid invoice: the UBL is the legal original and the PDF is a rendering of it.
An API other applications invoice through
The payload describes what was sold. Layout, numbering, VAT presentation and Peppol compliance stay in Clark, so a caller does not get redeployed every time an invoice design or a rule set changes.
Honest defaults
With no transport contract configured, every motion runs except the last: validated, frozen, recorded, nothing leaves. A driver missing its credentials falls back to that rather than throwing — half-configured is the normal state of an install being set up, and the dashboard that tells you what is missing must stay reachable.
Words chosen carefully
"Past due date", never "unpaid" — nothing here tracks payment, and the wrong word sends somebody chasing a customer who settled last week. Received shows as unavailable rather than zero, because a zero would read as "none arrived".
A decision worth explaining
What gets posted to the transmission provider is our UBL, not a provider-shaped JSON. For a service priced per document that is the whole game: hand over the document model and switching provider becomes a rewrite. It costs a mapping layer once and buys the ability to leave.
Something like this on your list?
The projects differ, the way of working does not. Tell me what you are trying to solve.
Taking on new work