pdf engine · e-invoicing · pure typescript
PDFs and e-invoices in pure TypeScript.
Write components, get paper - and XRechnung and ZUGFeRD e-invoices that pass the official EN 16931 and PDF/A checks. The engine is our own - no headless browser, no Java - and draws PDFs in Node or right in the browser.
Invoice #2026-014
Acme GmbH · Berlin
- XRechnung
- ZUGFeRD
- EN 16931
- PDF/A-3
- real pagination
- Vue · React · Angular · Nuxt
- runs in the browser
- no chromium · no jvm
- MIT
one library · every framework
One engine. Every framework.
Vue, React, Angular and Nuxt each get a binding that feels native: components where the framework thinks in components, a service and signals where Angular does. Underneath is the same engine, so a document looks the same wherever you write it.
- pure TypeScript
- no headless browser
- no Java, nothing native to install
- Node and the browser
@jasy/e-invoice · XRechnung · ZUGFeRD · Factur-X
Germany: mandatory from 2027E-invoices that pass the official checks.
One typed object in, a conformant e-invoice out: the PDF a person reads and the EN 16931 data a machine reads. Java has Mustangproject, PHP has horstoeko/zugferd, Python has factur-x - this is the TypeScript answer.
import { renderZugferd, type Invoice } from "@jasy/e-invoice";
const invoice: Invoice = {
number: "INV-2027-001",
issueDate: "2027-01-15",
currency: "EUR",
dueDate: "2027-01-29",
seller: {
name: "Northwind GmbH",
vatId: "DE265013614",
address: { line1: "Hauptstrasse 1", city: "Berlin", postCode: "10115", country: "DE" },
contact: { name: "A. Schmidt", email: "billing@northwind.de", phone: "+49 30 123456" },
},
buyer: {
name: "Globex AG",
address: { line1: "Marktplatz 5", city: "Munich", postCode: "80331", country: "DE" },
},
lines: [{ name: "Consulting", quantity: 8, unit: "HUR", netUnitPrice: 120,
vat: { category: "S", ratePercent: 19 } }],
payment: { iban: "DE89370400440532013000", terms: "Payable within 14 days." },
};
const { bytes, xml } = await renderZugferd(invoice, { locale: "en" });$ jasy validate invoice.pdf
ZUGFeRD · EN 16931 (CII) - valid
- 0 errors · 0 warnings
EN 16931 business rules
KoSIT
- 13 of 13
PDF/A-3 structure
Factur-X XMP, sRGB, embedded XML
- 0 failed rules
veraPDF
PDF/A-3b profile
Totals by construction
Line nets, totals and the VAT breakdown are computed from your data - nothing typed twice, nothing off by a cent.
ZUGFeRD, XRechnung, Factur-X
PDF/A-3 with the EN 16931 XML inside, or the XML alone - CII or UBL, one switch.
Your data stays home
No service, no upload, no Java. It runs in your Node process, next to your data.
why trust it
Every claim on this page links to its proof.
An engine of its own
Layout, text shaping and the PDF writer are ours, in TypeScript. No Chromium, no Java, no pdf-lib underneath.
How it worksMeasured against react-pdf
Faster, AES-256 encryption by default, accessible to PDF/UA - each measurement with the code to check it.
See the comparisonChecked, not claimed
E-invoices pass the KoSIT EN 16931 rules and veraPDF. Check any file yourself - ours or anyone's.
Open the validatorAccessible PDFs
One flag emits a tagged PDF/UA-1 document that passes veraPDF's machine checks.
AccessibilityByte-stable output
The same document renders to the same bytes, every time - an archived invoice stays reproducible.
Output optionsAnywhere JavaScript runs
Node or the browser, plain TypeScript, Vue, React, Angular or Nuxt - the same engine underneath.
All guides
one engine · two runtimes · same code
Runs on your server. And in the browser.
Pure TypeScript, no Node-only crutches. The same component tree produces the same byte-exact PDF whether it runs on a server or entirely in the browser - your call, not the library's.
on your server
Node, Nitro, Lambda, Workers
Generate invoices and reports in your API or backend - dependency-light, no Chromium to boot, no JVM to feed.
in the browser
100% client-side
Render the whole PDF in the user's browser - no server round-trip, works offline, and nothing ever leaves the device.
const bytes = await renderToBytes(doc) // same call, server or browser@jasy/vue · PDFs as Vue components
new · on npmYour PDF is a Vue component.
Author a document the way you build an app - components, props, slots and your reactive data - then render it to a real PDF, right in the browser. Real layout and pagination underneath, fully typed props, and a familiar feel if you have reached for @react-pdf/renderer on the React side.
pnpm add @jasy/vue vue<Document>
<Page :gap="12">
<Text :size="30" bold>{{ invoice.title }}</Text>
<Row
v-for="line in invoice.lines"
:key="line.id"
:justify="'between'"
>
<Text>{{ line.name }}</Text>
<Text bold>{{ line.total }} €</Text>
</Row>
</Page>
</Document>Components & props
Author a document the way you build an app - reusable components, props and slots.
v-for, v-if, computed
Your reactive data drives the page; loops, conditions and computed values just work.
Renders in the browser
The isomorphic engine turns the component tree into a PDF client-side - no server required.
@jasy/nuxt · the Nuxt module
new · on npmDrop it into Nuxt.
One install, zero config. Author PDFs as components and render them right in the browser, or stream them from a server route - your choice, the same components and helpers on both sides.
npx nuxi module add @jasy/nuxt<script setup>
const { open, download } = usePdf(Invoice)
</script>
<template>
<button @click="open">View PDF</button>
</template>// server/api/invoice/[id].get.ts
export default definePdfHandler((e) =>
Document([
Page([
Text(`Invoice #${getRouterParam(e, "id")}`),
]),
]),
)Zero config
Add it to modules. Components and the render helpers are auto-imported - no wiring, no imports.
Client or server
Render in the browser with usePdf, or stream from a Nitro route with definePdfHandler. Same vocabulary.
Nitro caching, lean bundle
Cache a route per path + query in one option. jimp stays server-side - the browser bundle stays small.
@jasy/react · PDFs as React components
new · on npmYour PDF is a React component.
Author a document in JSX - components, props, hooks and context - then render it to a real PDF, right in the browser. Real layout and pagination underneath: tables that carry their header across pages, newspaper columns, headers and footers that keep their space.
pnpm add @jasy/react @jasy/pdf reactComing from react-pdf? What jasy does differently <Document>
<Page gap={12}>
<Text size={30} bold>{invoice.title}</Text>
{invoice.lines.map((line) => (
<Row key={line.id} justify="between">
<Text>{line.name}</Text>
<Text bold>{line.total} €</Text>
</Row>
))}
</Page>
</Document>Components, hooks & context
Author a document the way you build an app - props, hooks, context and your own components.
Typed by the engine
Every prop is the engine's own option type, so every option autocompletes the day it ships.
Renders in the browser
The isomorphic engine turns the element tree into a PDF client-side - or on a server, the same call.
@jasy/angular · PDFs the Angular way
new · on npmPDFs that follow your signals.
A document is a function of its data. inject(JasyPdf) turns it into a real PDF, and pdfResource() renders it again whenever your signals change - right in the browser, with real layout and pagination underneath.
ng add @jasy/angularexport const invoicePdf: PdfDocumentFn<Invoice> = (invoice) =>
Document([
Page({ gap: 12 }, [
Text(invoice.title, { size: 30, bold: true }),
...invoice.lines.map((line) =>
Row({ justify: "between" }, [
Text(line.name),
Text(`${line.total} €`, { bold: true }),
]),
),
]),
]);
// in a component: renders again whenever the invoice changes
readonly preview = pdfResource(invoicePdf, () => this.invoice());Built the way Angular builds
provideJasy() in the app config, inject(JasyPdf) where you need a PDF. A document runs in an injection context, so inject() works inside it.
A preview that follows your signals
pdfResource() builds on Angular's resource(): it renders again on every change and keeps the last PDF meanwhile, so the preview does not blink.
ng add, and it is set up
One command installs and configures it. Tested on Angular 20, 21 and 22, zoneless and with zone.js, with and without SSR.
roadmap · MIT · built in the open
1.0 is built. Here's what's next.
Built in the open, MIT-licensed. Everything the 1.0 set out to do is done and shipping; this is what comes after it - in order, no dates promised.
Read and edit any PDF
Open a file, change what is on the page, and merge, split or extract. Reading a form and filling it already ships.
Digital signatures
Sign a document so a receiver can prove nobody touched it afterwards - the piece an e-invoice still misses.
The last few percent of layout
A public measure() helper, a heading across all columns, and a border that stays open where a box is split across pages.
Templates you can start from
An invoice, a report and a letter that work as they are, and come apart when you want your own.
More e-invoice profiles
Peppol and further country formats, on top of the Factur-X, ZUGFeRD and XRechnung profiles already supported.
Our own bidi
Hebrew and Arabic already order correctly. The UAX #9 library behind that seam has not been touched since 2023 and ships no types, so it is going to be ours.