Source
This page is generated from docs/quickstart-medusa.md at the pinned revision ea6b031ccb33. Edit it there, not here.
Quickstart: einvoice-medusa
Back to docs/README.md. Goal: a fresh Medusa v2 project fulfilling its first order into a real, KoSIT-passing XRechnung, in about 30 minutes — verified by actually timing this exact sequence end to end (see "How this was verified" at the bottom), not written from memory of how the plugin is supposed to work.
Prerequisites
- Node
^20.19.0or>=22.12.0(einvoice-medusa's ownenginesfield). - A Medusa v2 project with a real Postgres database. If you don't have one yet:bash
npx create-medusa-app@latest my-store --db-url "postgres://user:pass@localhost:5432/my_store_db"create-medusa-apphas scaffolded a small turborepo monorepo since at least v2.19.0 — your actual Medusa app lives atapps/backend/, not the project root. Every command below runs from there (cd my-store/apps/backend).
1. Install
npm install @normwerk/einvoice-medusa @normwerk/einvoice-model @normwerk/einvoice-commerce @normwerk/einvoice-ciiOnly add @normwerk/einvoice-pdfa if you plan to use standalone.basePdf with its bundled renderInvoicePdf (step 5) rather than your own PDF renderer or a supported PDF plugin:
npm install @normwerk/einvoice-pdfa2. Configure the plugin
Edit medusa-config.ts:
export default defineConfig({
// ...your existing projectConfig...
plugins: [
{
resolve: "@normwerk/einvoice-medusa",
options: {
seller: {
name: "Your Company GmbH",
countryCode: "DE",
city: "Berlin",
postCode: "10115",
vatIdentifier: "DE123456789",
electronicAddress: "invoicing@yourcompany.example",
electronicAddressScheme: "EM",
contact: {
name: "Accounting",
telephone: "+49 30 1234567",
email: "invoicing@yourcompany.example",
},
},
payment: {
means: "58", // SEPA credit transfer
iban: "DE89370400440532013000",
},
},
},
],
});seller.contact and payment both look like they could be optional from their TypeScript types alone — they aren't. Every document this plugin builds targets the full XRechnung 3.0 CIUS regardless of who the buyer is, and that CIUS makes seller contact (BR-DE-2) and payment instructions (BR-DE-1) mandatory. Leave either out and the plugin refuses to start at all (InvalidEinvoiceModuleOptionsError, thrown from the module's own constructor) — a loud failure at boot, not a document that silently fails validation later.
3. Run migrations
npx medusa db:migrateThis creates einvoice_document and einvoice_counter (this plugin's own two tables — no schema changes to any core Medusa table).
4. Try it
Start the dev server (npx medusa develop), then fulfill any real order (admin dashboard, or POST /admin/orders/:id/fulfillments). Within about a second, open that order's page: a new "E-Invoices" side-panel section lists the invoice, with XML (and PDF, once you've done step 5 or 6) download links. Refund a captured payment and a credit note appears the same way.
5. Optional: attach a PDF without your own renderer
standalone: {
basePdf: async (invoice) => {
const { renderInvoicePdf } = await import("@normwerk/einvoice-pdfa");
return renderInvoicePdf(invoice);
},
},invoice here is the fully built EN 16931 Invoice — the exact same data the XML carries, so the PDF and XML can never disagree on numbers. renderInvoicePdf is a real, font-embedded, PDF/A-eligible layout that ships with @normwerk/einvoice-pdfa; use your own renderer instead if you have one (same option, any function returning Uint8Array | undefined).
6. Optional: integrate with @webbers/invoices-medusa
If you already run @webbers/invoices-medusa for PDF invoices, don't configure standalone at all — set:
integration: { kind: "webbers" },This plugin then reuses their invoice's own number instead of allocating its own, and embeds this plugin's XML into their PDF. See docs/domain-glossary.md's own T-072 section for real, tested caveats with their package (some genuine bugs in their 1.0.6 release, worked around or documented there).
Buyer VAT-ID and B2G references
Neither has a first-class Medusa field — set them on the order's customer, under metadata:
customer.metadata.vat_id— the buyer's VAT-ID (BT-48).customer.metadata.buyer_reference— a real Leitweg-ID for a B2G buyer (BT-10). Setting this also makes the plugin choose the XRechnung profile automatically for that order (a Leitweg-ID buyer always gets XRECHNUNG, regardless ofdefaultProfile).
Downloading a document yourself
- Admin: the order page's own "E-Invoices" widget, or
GET /admin/orders/:id/einvoicefor the raw list. - Storefront:
GET /store/orders/:id/einvoice— requires a logged-in customer who owns the order (a guest order has no way to authenticate as its own "customer" today, a known v0.1 limitation).
Where to go next
docs/mapping-reference-medusa.md— exactly which Medusa field feeds which BT/BG number, and which ones this plugin can't source from Medusa at all yet.docs/features/einvoice-medusa.md— the storage/widget/API design, and what was and wasn't verified.docs/domain-glossary.md's Medusa v2 section — every real gotcha found building this (exact event names/payloads,query.graphquirks, migration pitfalls).
How this was verified
This exact sequence was run against a real, freshly scaffolded create-medusa-app@2.19.0 project (Docker Postgres), start to finish, and timed: environment setup (steps 1–3) took under 10 minutes, and a fulfilled order produced a real, KoSIT-validated invoice within seconds of step 4. @normwerk/einvoice-* aren't published to npm yet (that's T-076's own job) — this run substituted yalc for step 1's npm install, medusa-config.ts was edited exactly as shown, and every other step ran unmodified. Once T-076 publishes these packages for real, this quickstart should be re-run once against the actual npm registry before being called final — a yalc-substituted install is a faithful stand-in for module resolution, but not for npm's own package resolution/version constraints.