An open-source engine for national e-invoicing and fiscal clearance in the EU: the country-specific, real-time submission of invoices to a member state's tax authority (certificates, protective marks, gap-free sequencing, response authentication).
Document generation (EN16931 → UBL / CII / Factur-X) is handled by the upstream
library @e-invoice-eu/core, which
fiscalize builds on rather than forks. fiscalize adds the missing layer above it:
a pluggable per-country adapter interface over EN16931, with Slovenia
(e-SLOG 2.0 + FURS) as the first complete pilot adapter. The architecture is
designed to generalise across the EU as the ViDA mandate takes effect (cross-border
B2B 2030, full harmonisation 2035).
It is framework-agnostic: build an invoice, get conformant XML out, and clear it with the national authority. Host-agnostic consumers wrap it as a leaf dependency.
License: engine + adapters Apache-2.0.
Status, P1 + P2. Covers the e-invoice core (generate + validate an EN16931 core invoice as e-SLOG 2.0 / UBL / CII, with official-XSD validation) and FURS fiscal verification (ZOI/EOR, unit-verified; live test-env round-trip deferred, see below). The deployable clearance service (blagajnica) and the invoicing UI are later phases, see
ROADMAP.md.
Slovenia's e-SLOG 2.0 is EN16931-compliant but is its own XML syntax
(UN/EDIFACT INVOIC-derived, namespace urn:eslog:2.00), not UBL. So fiscalize:
- depends on
@e-invoice-eu/core(WTFPL) for the EN16931 model, validation, and UBL / CII / Peppol / Factur-X serialization, the hard, maintained part; and - owns the thin Slovenian delta: an e-SLOG 2.0 serializer.
Staying on upstream is deliberate, EN16931 / Peppol rules move (the SI B2B
mandate lands Jan 2028), and we want to inherit those updates rather than
fork away from them. Rule-change monitoring is tracked via upkeep.
Published to GitHub Packages under the @grunt-it scope. Auth follows the
house standard: a committed bunfig.toml that reads the token from the
REGISTRY_TOKEN env var (the same config app-template / utility-belt use; CI
provides REGISTRY_TOKEN as a secret). In the consuming repo:
# bunfig.toml, no secret in the file, safe to commit
[install.scopes]
"@grunt-it" = { token = "$REGISTRY_TOKEN", url = "https://npm.pkg.github.com" }export REGISTRY_TOKEN="ghp_…" # locally: a PAT with read:packages
bun add @grunt-it/fiscalizePromise-friendly, for any host (never throws):
import { createEInvoice, type Invoice } from "@grunt-it/fiscalize";
const invoice: Invoice = {
invoiceNumber: "2026-000123",
issueDate: "2026-05-25",
dueDate: "2026-06-08",
seller: {
name: "Zelena Japka d.o.o.",
vatId: "SI12345678",
address: { street: "Slovenska cesta 1", city: "Ljubljana", postalZone: "1000", countryCode: "SI" },
iban: "SI56020170014356205",
bic: "LJBASI2X",
},
buyer: {
name: "Kupec d.o.o.",
vatId: "SI87654321",
address: { city: "Ljubljana", postalZone: "1000", countryCode: "SI" },
},
lines: [
{ id: "1", name: "Detergent 1L", quantity: 10, netPrice: 5.0, lineNetAmount: 50.0, taxRate: 22 },
],
taxBreakdown: [{ taxableAmount: 50.0, taxAmount: 11.0, taxRate: 22 }],
totals: { lineExtensionAmount: 50.0, taxExclusiveAmount: 50.0, taxAmount: 11.0, taxInclusiveAmount: 61.0 },
};
const result = await createEInvoice(invoice, { format: "eslog" });
if (result.ok) {
console.log(result.data); // e-SLOG 2.0 XML
} else {
console.error(result.error.message, result.error.status);
}format is "eslog" | "ubl" | "cii". For ubl/cii, EN16931 validation runs
before generation by default (validate: false to skip).
import { Effect } from "effect";
import { parseInvoice, validateEn16931, generateEInvoice, serializeEslog } from "@grunt-it/fiscalize";
const program = Effect.gen(function* () {
const invoice = yield* parseInvoice(rawInput); // valibot, structural gate
yield* validateEn16931(invoice); // Ajv vs EN16931 schema
return yield* generateEInvoice(invoice, { format: "ubl" });
});serializeEslog(invoice) is a pure synchronous escape hatch for e-SLOG only.
import { generateEInvoice, validateEslogXml } from "@grunt-it/fiscalize";
// validate on the way out…
const xml = await Effect.runPromise(
generateEInvoice(invoice, { format: "eslog", validateOutput: true }),
);
// …or validate any e-SLOG XML string standalone
yield* validateEslogXml(someEslogXml);validateEslogXml checks the XML against the official e-SLOG 2.0 XSD
(eSLOG20_INVOIC_v200.xsd + xmldsig-core-schema.xsd, from the epos.si Aug-2020
package), using xmllint compiled to WebAssembly, no native bindings.
For Slovenian cash-register fiscalization (distinct from the e-invoice document): compute the ZOI and obtain the EOR from FURS.
import { Effect } from "effect";
import { makeFursClient } from "@grunt-it/fiscalize/furs";
const program = Effect.gen(function* () {
const furs = yield* makeFursClient({ p12, passphrase, production: false });
const { zoi, eor, printable } = yield* furs.reportInvoice({
taxNumber: 10489185,
issueDateTime: new Date(),
invoiceNumber: "11",
businessPremiseId: "BP101",
electronicDeviceId: "0001",
invoiceAmount: 19.15,
vat: [{ taxRate: 22, taxableAmount: 15.7, taxAmount: 3.45 }],
});
return { zoi, eor, printable }; // ZOI + FURS EOR + the QR/PDF417 string
});p12 is the taxpayer's certificate (a FURS test cert for the test env; the
shop's eDavki cert in production). ZOI is MD5(RSA-SHA256/PKCS#1 v1.5(…));
messages are RS256 JWS over mutual TLS.
Pass fursResponseCertPem (FURS's response-signing cert) to authenticate FURS's
replies, the client then verifies each response's JWS signature before trusting
the EOR (raising FursResponseSignatureError on a spoofed/tampered response).
Strongly recommended for production.
⚠ Runtime requirement, live FURS needs Node, not bun. Outbound mutual-TLS client certs do not work under bun 1.3.6, so the live FURS submission path (
echo/reportInvoice/registerBusinessPremise) must run under a Node runtime on a non-proxied network. The ZOI / JWS / verification crypto runs anywhere (incl. bun). This is a real deployment constraint, documented in full with the opt-in path:docs/FURS-RUNTIME.md.
Invoice is a clean, EN16931-aligned core invoice: header (BT-1/2/3/5/9/72),
seller & buyer (BG-4/BG-7, with VAT, address, bank), lines (BG-25), VAT breakdown
(BG-23), and totals (BG-22). Every field is annotated with its Business Term so
the mapping to each syntax stays auditable. Validated with valibot.
P1 maps the mandatory + common core, and produced e-SLOG XML is validated
against the official e-SLOG 2.0 XSD. Deferred (see ROADMAP.md):
business-rule (schematron-equivalent) validation, the official package ships no
.sch, so those rules are spec prose, plus document/line-level allowances,
contacts, multiple payment means, and the long tail of optional BTs. The e-SLOG
mapping is grounded in the official spec (epos.si) and cross-checked against the
MIT-licensed reference generator Media24si/eslog2.
Runnable end-to-end scripts in examples/:
bun run examples/e-invoice.ts # model → e-SLOG 2.0 (XSD-valid) + UBL
bun run examples/furs-offline.ts # FURS ZOI + request-JWS + response-verify (offline)examples/furs-offline.ts shows the FURS crypto without a live call (runs under
bun). For a live submission, see docs/FURS-RUNTIME.md.
bun install
bun test
bunx tsc --noEmitApache-2.0 (engine + country adapters). (Bundled third-party schema files keep
their own terms, see
src/lib/eslog/schema/PROVENANCE.md.)