Skip to content
Rascals
Work
Own product 2026 — now

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

01

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.

02

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.

03

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.

04

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.

05

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.

06

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