jasy
ZUGFeRD & XRechnung e-invoices in pure TypeScript - generate, validate, read.
A Flutter-style PDF engine underneath. The EN-16931 maths done for you. Zero Java, zero upload.
npm i @jasy/e-invoice · npx @jasy/cli · MIT
---
## Don't believe us. Point it at your own invoice.
```bash
npx @jasy/cli validate ./your-invoice.pdf
```
That `✓` is the **same official KoSIT Schematron + veraPDF the big players run** - against the EN 16931
**and** the German XRechnung (BR-DE) rules, plus PDF/A-3. Your file, your terminal, in seconds. No
account, no upload, nothing leaves your machine.
> Java has **Mustang**. PHP has **horstoeko**. Python has **factur-x**.
> Node had nothing. **Now it has jasy.**
---
## You bring the line items. jasy does the rest.
You never compute a total, a VAT breakdown or a rounding again. You hand jasy the line items - it
**derives** the document totals and the EN-16931 VAT breakdown, spec-correct. That is _why_ the
invoices validate: the amounts are correct **by construction**, so the single biggest class of
EN-16931 failures (the BR-CO total checks) simply cannot happen.
```ts
import { renderZugferd } from "@jasy/e-invoice";
const { bytes, xml } = await renderZugferd({
number: "RE-2026-001",
issueDate: "2026-06-17",
currency: "EUR",
seller: {
name: "Northwind Studio GmbH",
vatId: "DE123456789",
address: { city: "Berlin", postCode: "10115", country: "DE" },
},
buyer: {
name: "Globex Corporation Ltd",
address: { city: "Munich", postCode: "80331", country: "DE" },
},
lines: [
{
name: "Brand identity design",
quantity: 2,
unit: "HUR",
netUnitPrice: 100,
vat: { category: "S", ratePercent: 19 },
},
],
});
// bytes -> a valid ZUGFeRD PDF/A-3 · xml -> the embedded EN-16931 XML
// totals, tax breakdown and rounding: computed for you.
```
One object in. A conformant, human-readable PDF/A-3 **and** the EN-16931 CII/UBL XML out. ZUGFeRD and
XRechnung work out of the box - no config, no template wrangling.
---
## The whole loop, from the terminal
```bash
jasy read invoice.pdf # identify + show: parties, line items, totals
jasy validate invoice.pdf # EN 16931 + XRechnung + PDF/A (exit 1 if invalid)
jasy export invoice.pdf -o out.xlsx # read it back: JSON · TXT · Excel
```
Reads **CII and UBL**, real third-party invoices included - not just our own output.
---
## A PDF engine that reads like Flutter
Underneath it all is a declarative, component-based PDF engine - written from the byte stream up, no
headless browser, no `pdf-lib`. You describe the document; it lays it out and paginates.
```ts
import { Document, Page, Column, Text, renderToBytes } from "@jasy/pdf";
const pdf = await renderToBytes(
Document([
Page({ size: "A4", margin: 48 }, [
Column({ gap: 8 }, [
Text("Invoice RE-2026-001", { size: 24, bold: true, color: "steelblue" }),
Text("Thank you for your business.", { color: "gray" }),
]),
]),
]),
);
```
Real text layout (real font metrics, kerning, ligatures, word-wrap), full flexbox (`gap` / `justify` /
`align`, wrap, `flexBasis`, `flexShrink`, `order`), boxes with per-corner radius, gradients and alpha, images and
**SVG**, a `Canvas` to draw into, custom fonts from a file, bytes or a URL, AcroForm fields,
**AES-256 password encryption**, tagged PDFs for screen readers, and **real pagination** - content that
overflows flows onto the next page, headers and footers repeat, and a block can refuse to be split.
---
## Under the hood - and every bit is real
This is not a wrapper around someone else's renderer. It is built from the byte stream up, and you can
verify all of it:
- **Hand-rolled PDF writer.** No `pdf-lib`, no PDFKit, no Java, no headless Chrome - the byte stream is ours.
- **Font subsetting.** Only the glyphs you actually use are embedded, with the PDF/A subset tag - a
740 KB font ships as a ~76 KB subset, and the text stays copy- and searchable.
- **Compression.** Content streams and images are FlateDecode-compressed; the spreadsheets `jasy export`
writes are real `.xlsx` ZIPs we deflate with our own writer and CRC32, zero dependencies.
- **AES-256 encryption, pure JS.** Lock a PDF with `renderToBytes(doc, { encrypt: { userPassword } })` - the
newest standard handler (AES-256, R6 / ISO 32000-2) via the platform WebCrypto: no native crypto, no deps,
the same code in Node **and** the browser. Owner password + permissions optional.
- **Real font metrics.** Text is laid out with the Adobe AFM metrics of the standard-14 fonts - kerning
and word-wrap are _computed_, not guessed.
- **PDF/A-3, matched not approximated.** The conformance graph is hand-built and **passes veraPDF**, the
official ISO 19005 validator.
- **Byte-exact round-trips.** Generate and parse are inverses: `generate → parse → regenerate` reproduces
the identical XML. Over 1,600 tests hold the line across the workspace.
- **Schematron, local.** The official EN-16931 + XRechnung rules run via saxon-js (the real XSLT, in pure
JS) - no Java, no upload.
- **Vectors, not bitmaps.** `Image("logo.svg")` keeps a logo a VECTOR - our own SVG reader and our own
XML parser, checked against 10,819 real files and compared pixel for pixel with headless Chrome. Need
a chart instead? `Canvas` hands you a typed pen for the same vector layer.
- **Fonts, whatever the container.** `.ttf`, `.woff` and `.woff2` are the same thing at the call site.
The WOFF2 container and its `glyf` transform are ours; only Brotli is a dependency, and it is loaded
only if a WOFF2 actually turns up.
- **Byte-stable output.** The same document rendered twice is byte-identical, PDF/A and ZUGFeRD
included, across separate processes. An archived invoice stays re-derivable and hashable years later.
- **Accessible if you ask.** `renderToBytes(doc, { accessible })` emits a full tagged structure tree,
and the output passes veraPDF's machine-verifiable PDF/UA-1 checks.
---
## Packages
| Package | What it is |
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| **[@jasy/e-invoice](https://npmx.dev/@jasy/e-invoice)** | ZUGFeRD / XRechnung: your data → PDF/A-3 + EN-16931 XML, with local validation. **The prize.** |
| **[@jasy/cli](https://npmx.dev/@jasy/cli)** | the `jasy` terminal: read · validate · export, headless **and** interactive |
| **[@jasy/pdf](https://npmx.dev/@jasy/pdf)** | the declarative, Flutter-style PDF layout engine that powers them |
| **[@jasy/vue](https://npmx.dev/@jasy/vue)** | author PDFs as Vue components - and render them in the browser |
| **[@jasy/nuxt](https://npmx.dev/@jasy/nuxt)** | the Nuxt module: zero-config PDFs, client **or** server |
---
## Why you can trust it
- **Over 1,600 tests, green** across the workspace (1,190 in the engine alone). The generator and the parser
are **byte-exact inverses**: `generate → parse → regenerate` reproduces the identical XML. Nothing is
silently lost.
- **The same rules the authorities use** - the official KoSIT EN-16931 + XRechnung Schematron, and the
official **veraPDF** for the full ISO 19005 (PDF/A) check.
- **Proven on real third-party invoices**, not only our own.
- **100% local.** No upload, no service, no account - DSGVO-safe by construction.
---
## Honest scope
jasy targets the documents that matter here: invoices, reports, quotes, datasheets - and it now sets
text properly: kerning and Latin ligatures from the font's own tables, justification, pluggable
hyphenation, right-to-left and Arabic shaping, underlines that step around descenders. Pagination is
real: text splits at line boxes, `keepTogether` moves a card whole, orphans and widows are honoured.
It is still **not** a LaTeX or WeasyPrint replacement, and does not try to be: no automatic
microtypography, no floats, no CSS engine. What it will not draw, it **names** - an SVG filter, a font
without the glyph, a word that cannot be split - rather than quietly drawing something else.
> **Status:** young and pre-1.0. The API can still shift between minor versions. Everything shown here
> works and is tested.
---
MIT · built by Florian Heuberger