core-invoice
Offline library for what an e-invoice means, not for sending it.
Europe’s semantic model is CEN EN 16931. Peppol PINT is the international sibling: tax is VAT, GST, SST, or consumption — not VAT only. This library validates and converts UBL 2.1 and UN/CEFACT CII D16B against those rules, with no network.
Other software can call it. It is not a Peppol Access Point, not LHDN / MyInvois submit, and not an accounting product.
How it works
XML is not the model. The library reads UBL or CII into a typed Invoice, runs profile rules on that value, and writes XML back from the same value.
UBL 2.1 or CII D16B
│
▼
Invoice (parties, lines, tax, totals)
│
├── validate(profile) → Report (rule ids such as BR-05, IBR-CL-01-MY)
└── convert → UBL or CII
| Term | Meaning here |
|---|---|
| EN 16931 | CEN core: the business terms (BT-*) and groups (BG-*) an invoice must carry. |
| UBL 2.1 | OASIS XML syntax. Invoice and CreditNote. |
| CII D16B | UN/CEFACT XML syntax. A mapped subset for EN / Peppol; not for PINT-MY. |
| Profile | Which rule set to apply. Four shipped; they are siblings, not a ladder. |
| BT-24 | CustomizationID — the URN that says which profile the document claims. CLI --profile auto reads this. |
| CIUS | A Core Invoice Usage Specification: extra rules on top of EN 16931. Peppol BIS 3.0 is the only CIUS shipped here. PINT is not a CIUS. |
Fatal rule ids are comparable to pinned ConnectingEurope EN 16931 validation-1.3.16 and PINT-MY 1.3.0 (see Evidence). validate().ok() is still not a legal attestation: not OpenPEPPOL Valid, not IRBM Valid.
Profiles
--profile auto (the default) picks the rule set from BT-24. A named profile forces that set — “would this pass as Peppol?” even if BT-24 says otherwise.
You do not widen() PINT or PINT-MY into EN or Peppol BIS.
| Profile | CLI slug | What it is | Syntax |
|---|---|---|---|
| EN 16931 | en16931 |
Core semantic model (2017+A1). | UBL and CII |
| Peppol BIS Billing 3.0 | peppol |
CIUS of EN. VAT-only. Pin v3.0.20 (Schematron .sch only in this pin). |
UBL and CII |
| PINT | pint |
International. Tax is not only VAT. Not a CIUS. Pin Billing 1.1.2. | UBL; CII subset |
| PINT-MY | pint-my |
Specialises PINT (SST / TTx). Pin 1.3.0. Wire TaxScheme is VAT / AAL, never SST. |
UBL only |
A German XRechnung BT-24 is ingested as EN 16931. There is no Profile::XRechnung. Optional Cargo feature xrechnung adds CIUS BR-DE-* rules (including Skonto BR-DE-18 and warnings BR-DE-21/26/27/28); it is off by default and is not CORE. Not KoSIT Valid. Extension / CVD are not registered.
Crates
One version across the workspace.
| Crate | Role |
|---|---|
core-invoice |
Semantic model and rules. No XML, no I/O. |
core-invoice-formats |
UBL 2.1 tree walk (Invoice and CreditNote). CII D16B subset for EN/Peppol. PINT-MY is UBL-only (CiiNotForProfile). |
core-invoice-cli |
Binary core-invoice: validate, convert, diff, explain, rules, inspect. |
core-invoice-fixtures |
In-memory constructors: PINT-MY SST, authored Pint GST (pint_gst_sr, not a 1.1.2 oracle), Peppol VAT. Official XML is testdata/, not this crate. |
core-invoice-sys |
C ABI: validate / convert / diff / version, exit 0/1/2. |
Library
Validate XML. None as profile means “read BT-24”.
use validate_xml;
let xml = read_to_string?;
let report = validate_xml?;
if report.ok else
Build an invoice in memory and validate the model (no XML):
use ;
let mut invoice = blank;
// set lines, tax category, totals, issue date, …
let report = validate;
if !report.ok
convert proves the document first, then writes. Production write is write_validated (stamps BT-24 / BT-23 from the proved profile). CII write of a PINT-MY invoice is refused.
Command line
From this repo, after clone:
| Exit | Meaning |
|---|---|
| 0 | Valid, no semantic difference, explained, or listed |
| 1 | Invalid (findings on stdout) or diff found a difference |
| 2 | Unreadable XML, I/O, unknown explain id, or CII refused for PINT-MY |
inspect prints fields. It does not give a valid/invalid verdict. Validity is never colour-only.
C, Python, WebAssembly
- C —
crates/core-invoice-sys:core_invoice_validate/_ubl, convert, diff, version. Same 0/1/2 contract. Header:include/core_invoice.h. - Python —
python/core_invoice.pyvia ctypes. Surface isvalidate_xmlonly (no PyPI wheel). - wasm — the model crate:
cargo build -p core-invoice --target wasm32-unknown-unknown. Not a browser Access Point. Formats/sys are not that job.
Not this library
- Peppol Access Point, AS4, SMP, SML
- LHDN / MyInvois submit, IRBM Valid, digital certificates
- Accounting UI, general ledger, PDF letterhead
- XRechnung as a default / CORE profile
- ZUGFeRD / Factur-X PDF write
- EN 16931-1:2026 rules (
Edition::En2026stays unimplemented until its artefacts exist) - CII write of a PINT-MY invoice
- Compiling Peppol
.schourselves and calling that OpenPEPPOL Valid
Evidence
Pins, fetch, and licence fences: docs/spec.md. Expected unmatched ids: docs/UNCOVERED.md.
| Corpus | Pin | Role |
|---|---|---|
| ConnectingEurope EN 16931 | validation-1.3.16 |
Fatal-id compare (task svrl) |
| Peppol BIS Billing 3.0 | v3.0.20 |
.sch only — no compiled OpenPEPPOL XSLT in this pin |
| Peppol PINT Billing | 1.1.2 |
International profile. Schematron + genericode; no official example XML in the zip. GST IBR families are jurisdiction specialisations, not this pin. |
| PINT-MY Billing | 1.3.0 |
Malaysian specialisation (UBL) |
task svrl runs pinned Schematron (Saxon-HE, or Docker Compose Java) and diffs Fatal Finding.id against SVRL @id. That is an oracle, not a crate dependency.
Test XML lives in git-tracked testdata/ (~2 MB): CEN UBL unit tests and official EN / Peppol / PINT-MY samples, so cargo test on a fresh clone does not skip. EUPL / Peppol terms — testdata/NOTICE. Not copied into crates/, so crates.io packages do not include it.
Full artefacts (Schematron, XSLT, XSD zips, clones) stay in gitignored refers/. Fetch with task spec.
Development
MSRV 1.88.0, pinned by rust-toolchain.toml. Orchestrator: brew install go-task.
License and versions
MIT OR Apache-2.0. Releases: CHANGELOG.md.
crates.io: core-invoice 2.0.3. Git tag v2.0.3. crates.io 2.0.2 / 2.0.1 / 2.0.0 / 1.0.0 remain (not yanked). 2.x may add APIs; breaking changes are 3.0. The C ABI is the 0/1/2 verbs, not Invoice layout.