Skip to content

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.0 or >=22.12.0 (einvoice-medusa's own engines field).
  • 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-app has scaffolded a small turborepo monorepo since at least v2.19.0 — your actual Medusa app lives at apps/backend/, not the project root. Every command below runs from there (cd my-store/apps/backend).

1. Install

bash
npm install @normwerk/einvoice-medusa @normwerk/einvoice-model @normwerk/einvoice-commerce @normwerk/einvoice-cii

Only 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:

bash
npm install @normwerk/einvoice-pdfa

2. Configure the plugin

Edit medusa-config.ts:

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: "[email protected]",
          electronicAddressScheme: "EM",
          contact: {
            name: "Accounting",
            telephone: "+49 30 1234567",
            email: "[email protected]",
          },
        },
        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

bash
npx medusa db:migrate

This 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

ts
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:

ts
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 of defaultProfile).

Downloading a document yourself

  • Admin: the order page's own "E-Invoices" widget, or GET /admin/orders/:id/einvoice for 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

How this was verified

This exact sequence was run against a real, freshly scaffolded [email protected] 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.