diff --git a/lab/faktura/customer.ts b/lab/faktura/customer.ts
index 71a0a6afbc646ea9a1db044e03ce98204d3e426f..de0b7f45cae5f2e3c63b9f4e429286bc3eb295a5 100644
--- a/lab/faktura/customer.ts
+++ b/lab/faktura/customer.ts
@@ -15,5 +15,6 @@ export class Customer extends MongoEntity {
   /** may contain \n */
   @Field(type => String) namensZusatz: Maybe<string>;
   @Field(type => Address) address: Maybe<Address>;
+  @Field(type => String) email: Maybe<string>;
   @Field(type => String) ustID: Maybe<string>;
 }
diff --git a/lab/faktura/faktura-document.ts b/lab/faktura/faktura-document.ts
index 5b7b7e108c0e548972c3c69cfb6250c7d7addbdc..99045667ba26482b2c15d7e1326fe565143df959 100644
--- a/lab/faktura/faktura-document.ts
+++ b/lab/faktura/faktura-document.ts
@@ -9,7 +9,7 @@ import type { PartialEntityData } from '@adornis/baseql/entities/types.js';
 import { baseqlMetaData } from '@adornis/baseql/metadata/metaDataStore.js';
 import { registerMutation } from '@adornis/baseql/metadata/register.js';
 import { getByID } from '@adornis/baseql/operations/mongo.js';
-import { type BaseQLSelectionSet } from '@adornis/baseql/utils/queryGeneration.js';
+import { selectionSet, type BaseQLSelectionSet } from '@adornis/baseql/utils/queryGeneration.js';
 import { Counter } from '@adornis/counter/counter.js';
 import { AdornisFile } from '@adornis/file-utils/db/files.js';
 import { createPDFBufferFromComponent } from '@adornis/print/server/print-internals.js';
@@ -22,6 +22,7 @@ import { readFile } from 'fs/promises';
 import { PDFDocument } from 'pdf-lib';
 import { Customer } from './customer.js';
 import { Tenant } from './tenant.js';
+import { generateXInvoiceXML } from './xrechnung.js';
 
 export const STEUERSATZ = 19;
 
@@ -264,7 +265,13 @@ export const printDocumentWithAttachments = registerMutation({
     const fakDoc = await getByID(
       FakturaDocument,
       fakDocID,
-    )({ readableID: 1, documentDate: 1, identifierNumber: 1, attachments: { file: AdornisFile.allFields } });
+    )({
+      ...selectionSet(() => FakturaDocument, 4),
+      readableID: 1,
+      documentDate: 1,
+      identifierNumber: 1,
+      attachments: { file: AdornisFile.allFields },
+    });
     if (!fakDoc) throw new Error('no faktura document found with id ' + fakDocID);
     const name = `${fakDoc.readableID}.pdf`;
     const pdfBuffer = await createPDFBufferFromComponent({
@@ -280,6 +287,17 @@ export const printDocumentWithAttachments = registerMutation({
       for (const page of await pdflib.copyPages(pdflibAttachment, pdflibAttachment.getPageIndices()))
         pdflib.addPage(page);
     }
+
+    // Embed the ZUGFeRD XML as a file attachment
+    const xmlAttachmentName = 'xrechnung.xml';
+    const xmlBytes = Buffer.from(generateXInvoiceXML(fakDoc), 'utf8');
+
+    await pdflib.attach(xmlBytes, xmlAttachmentName, {
+      mimeType: 'application/xml',
+      creationDate: new Date(),
+      modificationDate: new Date(),
+    });
+
     // TODO we create an orphaned adornisfile here, we'll need to remove this at some point. Best solution would be for baseQL to allow buffer serving through some integrated means
     const afile = await AdornisFile.createFromBuffer(Buffer.from(await pdflib.save()), name);
     return afile;
diff --git a/lab/faktura/package.json b/lab/faktura/package.json
index b631ae32303af4fb2cb26d8dd6c64678c014bcb3..c09878e648698011690436f83d817c5eae98b572 100644
--- a/lab/faktura/package.json
+++ b/lab/faktura/package.json
@@ -11,6 +11,7 @@
   "dependencies": {
     "@adornis/accounting": "workspace:^",
     "@adornis/address": "workspace:^",
+    "@adornis/assert": "workspace:^",
     "@adornis/base": "workspace:^",
     "@adornis/baseql": "workspace:^",
     "@adornis/chemistry": "workspace:^",
diff --git a/lab/faktura/tenant.ts b/lab/faktura/tenant.ts
index 6c9bcb06d42782b6102298701d5f2de3aff82023..a6116b57375a38eea693a3f1f94ef96d5e03a714 100644
--- a/lab/faktura/tenant.ts
+++ b/lab/faktura/tenant.ts
@@ -18,6 +18,7 @@ export class Tenant extends MongoEntity {
   @DataLoadedField(() => AdornisFile, ({ logoID }) => logoID) logo: Maybe<AdornisFile>;
   @Field(() => Address) address: Maybe<Address>;
   @Field(() => String) email: Maybe<string>;
+  @Field(() => String) phone: Maybe<string>;
   @Field(() => String) website: Maybe<string>;
   @Field(() => String) steuerNr: Maybe<string>;
   @Field(() => String) umsatzsteuerNr: Maybe<string>;
diff --git a/lab/faktura/xrechnung.ts b/lab/faktura/xrechnung.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b9bfa78dc65f64cd6a8a47b02a173889157cc8b0
--- /dev/null
+++ b/lab/faktura/xrechnung.ts
@@ -0,0 +1,240 @@
+/*!
+ * xrechnung-generator
+ * Copyright(c) 2024 Benedikt Cleff (info@pixelpal.io)
+ * MIT Licensed
+ *
+ * Changes made by Adornis
+ * 
+ * hiermit validieren wir: https://erechnungsvalidator.service-bw.de/
+ */
+
+import { assertNotEmpty } from '@adornis/assert/not-empty.js';
+import type { NarrowedEntity } from '@adornis/baseql/utils/queryGeneration.js';
+import { add, format } from 'date-fns';
+import type { FakturaDocument } from './faktura-document.js';
+
+function escapeXML(value: string) {
+  if (!value) return '';
+  return value
+    .replace(/&/g, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+    .replace(/"/g, '&quot;')
+    .replace(/'/g, '&apos;');
+}
+
+export function generateXInvoiceXML(
+  invoice: NarrowedEntity<
+    FakturaDocument,
+    {
+      documentDate: 1;
+      readableID: 1;
+      identifierNumber: 1;
+      tenant: {
+        name: 1;
+        email: 1;
+        phone: 1;
+        address: { streetAndHousenumber: 1; zipCity: { zip: 1; city: 1 } };
+        bankName: 1;
+        handelsregisterNr: 1;
+        steuerNr: 1;
+        umsatzsteuerNr: 1;
+        iban: 1;
+      };
+      customer: {
+        displayName: 1;
+        firmenName: 1;
+        namensZusatz: 1;
+        ustID: 1;
+        email: 1;
+        address: { name: 1; streetAndHousenumber: 1; zipCity: { zip: 1; city: 1 } };
+      };
+      totalTax: 1;
+      totalBrutto: 1;
+      totalNetto: 1;
+      products: {
+        name: 1;
+        description: 1;
+        nettoTotal: 1;
+        discountPercent: 1;
+        amount: 1;
+        durationInMonths: 1;
+      };
+    }
+  >,
+) {
+  return `<?xml version="1.0" encoding="UTF-8"?>
+<ubl:Invoice xmlns:ubl="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
+             xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
+             xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
+  <cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0</cbc:CustomizationID>
+  <cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
+  <cbc:ID>${escapeXML(invoice.readableID)}</cbc:ID>
+  <cbc:IssueDate>${format(invoice.documentDate, 'yyyy-MM-dd')}</cbc:IssueDate>
+  <cbc:DueDate>${format(add(invoice.documentDate, { days: 14 }), 'yyyy-MM-dd')}</cbc:DueDate>
+  <cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
+  <cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
+  <cbc:BuyerReference>${escapeXML(invoice.readableID)}</cbc:BuyerReference>
+
+  <cac:AccountingSupplierParty>
+    <cac:Party>
+      <cbc:EndpointID schemeID="EM">${escapeXML(assertNotEmpty(invoice.tenant?.email))}</cbc:EndpointID>
+      <cac:PartyName>
+        <cbc:Name>${escapeXML(assertNotEmpty(invoice.tenant?.name))}</cbc:Name>
+      </cac:PartyName>
+      <cac:PostalAddress>
+        ${
+          invoice.tenant?.address?.streetAndHousenumber
+            ? `<cbc:StreetName>${escapeXML(invoice.tenant.address.streetAndHousenumber)}</cbc:StreetName>`
+            : ''
+        }
+        ${
+          invoice.tenant?.address?.zipCity?.city
+            ? `<cbc:CityName>${escapeXML(invoice.tenant.address.zipCity.city)}</cbc:CityName>`
+            : ''
+        }
+        ${
+          invoice.tenant?.address?.zipCity?.zip
+            ? `<cbc:PostalZone>${escapeXML(invoice.tenant.address.zipCity.zip)}</cbc:PostalZone>`
+            : ''
+        }
+        <cac:Country>
+          <cbc:IdentificationCode>DE</cbc:IdentificationCode>
+        </cac:Country>
+      </cac:PostalAddress>
+      ${
+        invoice.tenant?.umsatzsteuerNr
+          ? `<cac:PartyTaxScheme>
+        <cbc:CompanyID>${escapeXML(invoice.tenant.umsatzsteuerNr)}</cbc:CompanyID>
+        <cac:TaxScheme>
+          <cbc:ID>VAT</cbc:ID>
+        </cac:TaxScheme>
+      </cac:PartyTaxScheme>`
+          : ''
+      }
+      ${
+        invoice.tenant?.steuerNr
+          ? `<cac:PartyTaxScheme>
+        <cbc:CompanyID>${escapeXML(invoice.tenant.steuerNr)}</cbc:CompanyID>
+        <cac:TaxScheme>
+          <cbc:ID>FC</cbc:ID>
+        </cac:TaxScheme>
+      </cac:PartyTaxScheme>`
+          : ''
+      }
+      <cac:PartyLegalEntity>
+        <cbc:RegistrationName>${escapeXML(assertNotEmpty(invoice.tenant?.name))}</cbc:RegistrationName>
+        ${
+          invoice.tenant?.handelsregisterNr
+            ? `<cbc:CompanyID>${escapeXML(invoice.tenant.handelsregisterNr)}</cbc:CompanyID>`
+            : ''
+        }
+      </cac:PartyLegalEntity>
+      <cac:Contact>
+        <cbc:Name>${escapeXML(assertNotEmpty(invoice.tenant?.name))}</cbc:Name>
+        ${invoice.tenant?.phone ? `<cbc:Telephone>${escapeXML(invoice.tenant.phone)}</cbc:Telephone>` : ''}
+        ${invoice.tenant?.email ? `<cbc:ElectronicMail>${escapeXML(invoice.tenant.email)}</cbc:ElectronicMail>` : ''}
+      </cac:Contact>
+    </cac:Party>
+  </cac:AccountingSupplierParty>
+
+  <cac:AccountingCustomerParty>
+    <cac:Party>
+      <cbc:EndpointID schemeID="EM">${escapeXML(assertNotEmpty(invoice.customer?.email))}</cbc:EndpointID>
+      <cac:PartyName>
+        <cbc:Name>${escapeXML(assertNotEmpty(invoice.customer?.displayName))}</cbc:Name>
+      </cac:PartyName>
+      <cac:PostalAddress>
+        ${
+          invoice.customer?.address?.streetAndHousenumber
+            ? `<cbc:StreetName>${escapeXML(invoice.customer.address.streetAndHousenumber)}</cbc:StreetName>`
+            : ''
+        }
+        ${
+          invoice.customer?.address?.zipCity?.city
+            ? `<cbc:CityName>${escapeXML(invoice.customer.address.zipCity.city)}</cbc:CityName>`
+            : ''
+        }
+        ${
+          invoice.customer?.address?.zipCity?.zip
+            ? `<cbc:PostalZone>${escapeXML(invoice.customer.address.zipCity.zip)}</cbc:PostalZone>`
+            : ''
+        }
+        <cac:Country>
+          <cbc:IdentificationCode>DE</cbc:IdentificationCode>
+        </cac:Country>
+      </cac:PostalAddress>
+      <cac:PartyLegalEntity>
+        <cbc:RegistrationName>${escapeXML(assertNotEmpty(invoice.customer?.firmenName))}</cbc:RegistrationName>
+      </cac:PartyLegalEntity>
+    </cac:Party>
+  </cac:AccountingCustomerParty>
+
+  <cac:PaymentMeans>
+    <cbc:PaymentMeansCode>31</cbc:PaymentMeansCode>
+    <cbc:PaymentID>${escapeXML(invoice.readableID)}</cbc:PaymentID>
+    ${
+      invoice.tenant?.iban || invoice.tenant?.bankName
+        ? `
+    <cac:PayeeFinancialAccount>
+      ${invoice.tenant.iban ? `<cbc:ID>${escapeXML(invoice.tenant.iban)}</cbc:ID>` : ''}
+      ${invoice.tenant.bankName ? `<cbc:Name>${escapeXML(invoice.tenant.bankName)}</cbc:Name>` : ''}
+    </cac:PayeeFinancialAccount>`
+        : ''
+    }
+  </cac:PaymentMeans>
+
+  <cac:TaxTotal>
+    <cbc:TaxAmount currencyID="EUR">${invoice.totalTax.toFixed(2)}</cbc:TaxAmount>
+    <cac:TaxSubtotal>
+      <cbc:TaxableAmount currencyID="EUR">${invoice.totalNetto.toFixed(2)}</cbc:TaxableAmount>
+      <cbc:TaxAmount currencyID="EUR">${invoice.totalTax.toFixed(2)}</cbc:TaxAmount>
+      <cac:TaxCategory>
+        <cbc:ID>S</cbc:ID>
+        <cbc:Percent>19</cbc:Percent>
+        <cac:TaxScheme>
+          <cbc:ID>VAT</cbc:ID>
+        </cac:TaxScheme>
+      </cac:TaxCategory>
+    </cac:TaxSubtotal>
+  </cac:TaxTotal>
+
+  <cac:LegalMonetaryTotal>
+    <cbc:LineExtensionAmount currencyID="EUR">${invoice.totalNetto.toFixed(2)}</cbc:LineExtensionAmount>
+    <cbc:TaxExclusiveAmount currencyID="EUR">${invoice.totalNetto.toFixed(2)}</cbc:TaxExclusiveAmount>
+    <cbc:TaxInclusiveAmount currencyID="EUR">${invoice.totalBrutto.toFixed(2)}</cbc:TaxInclusiveAmount>
+    <cbc:PayableAmount currencyID="EUR">${invoice.totalBrutto.toFixed(2)}</cbc:PayableAmount>
+  </cac:LegalMonetaryTotal>
+
+  ${invoice.products
+    .map(
+      (item, index) => `
+  <cac:InvoiceLine>
+    <cbc:ID>${index + 1}</cbc:ID>
+    <cbc:InvoicedQuantity unitCode="H87">1</cbc:InvoicedQuantity>
+    <cbc:LineExtensionAmount currencyID="EUR">${item.nettoTotal.toFixed(2)}</cbc:LineExtensionAmount>
+    <cac:Item>
+      <cbc:Description>${escapeXML(item.description)}</cbc:Description>
+      <cbc:Name>${escapeXML(item.name)}</cbc:Name>
+      <cac:ClassifiedTaxCategory>
+        <cbc:ID>S</cbc:ID>
+        <cbc:Percent>19</cbc:Percent>
+        <cac:TaxScheme>
+          <cbc:ID>VAT</cbc:ID>
+        </cac:TaxScheme>
+      </cac:ClassifiedTaxCategory>
+    </cac:Item>
+    <cac:Price>
+      <cbc:PriceAmount currencyID="EUR">${item.nettoTotal.toFixed(2)}</cbc:PriceAmount>
+      <cbc:BaseQuantity unitCode="H87">1</cbc:BaseQuantity>
+      <cac:AllowanceCharge>
+        <cbc:ChargeIndicator>false</cbc:ChargeIndicator>
+        <cbc:Amount currencyID="EUR">0.00</cbc:Amount>
+        <cbc:BaseAmount currencyID="EUR">${item.nettoTotal.toFixed(2)}</cbc:BaseAmount>
+      </cac:AllowanceCharge>
+    </cac:Price>
+  </cac:InvoiceLine>`,
+    )
+    .join('\n')}
+</ubl:Invoice>`;
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d1849bef9f893e51f29de7e9fb4a0357e48e7b76..ec3349450f7b7c00913ae9c27ccb32c294b2e84a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -470,6 +470,9 @@ importers:
       '@adornis/address':
         specifier: workspace:^
         version: link:../../modules/address
+      '@adornis/assert':
+        specifier: workspace:^
+        version: link:../assert
       '@adornis/base':
         specifier: workspace:^
         version: link:../../modules/base