← Back to all articles

Peppol BIS 3.0-facturen genereren in C#

SealDoc Team · · 6 min read

Een Peppol BIS Billing 3.0-factuur genereren in C# is conceptueel niet moeilijk. Het moeilijke deel is de naamruimtedeclaraties correct instellen, de juiste identificatoren in de juiste velden plaatsen en uitvoer produceren die Schematron-validatie doorstaat.

Dit artikel doorloopt een minimale maar volledige implementatie. Het gaat ervan uit dat je de Peppol BIS 3.0-introductie hebt gelezen en het EN16931-gegevensmodel begrijpt. Ben je onbekend met Business Terms als BT-24 of BT-131, begin dan daar.

Vereiste naamruimten

UBL 2.1 gebruikt drie XML-naamruimten voor factuurdocumenten:

static readonly XNamespace Ubl = "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2";
static readonly XNamespace Cac = "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2";
static readonly XNamespace Cbc = "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2";

Ubl is de naamruimte van het rootelement. Cac bevat aggregaat-(samengestelde) elementen zoals PartyName en InvoiceLine. Cbc bevat basis-(scalaire) elementen zoals ID, Name en Amount.

Elk element in de factuur gebruikt een van deze drie naamruimten. Een verkeerde naamruimte laat schemavalidatie mislukken voordat Schematron zelfs maar start.

De verplichte headervelden

Een minimaal geldige Peppol BIS 3.0-factuur vereist deze headerelementen, in deze volgorde:

var invoice = new XElement(Ubl + "Invoice",
    new XAttribute(XNamespace.Xmlns + "cac", Cac.NamespaceName),
    new XAttribute(XNamespace.Xmlns + "cbc", Cbc.NamespaceName),

    // BT-24: Specificatie-identifier
    new XElement(Cbc + "CustomizationID",
        "urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1"),

    // BT-23: Bedrijfsprocestype
    new XElement(Cbc + "ProfileID",
        "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0"),

    // BT-1: Factuurnummer
    new XElement(Cbc + "ID", "INV-2026-001"),

    // BT-2: Factuurdatum
    new XElement(Cbc + "IssueDate", "2026-01-14"),

    // BT-3: Type factuurcode (380 = Commerciele factuur)
    new XElement(Cbc + "InvoiceTypeCode", "380"),

    // BT-5: Valutacode
    new XElement(Cbc + "DocumentCurrencyCode", "EUR")
);

De CustomizationID-waarde is niet configureerbaar. Deze exacte tekenreeks is vereist voor Peppol BIS 3.0. Het gebruik van de EN16931-basis-URI in plaats van de Peppol BIS-URI is een veelgemaakte fout die leidt tot afwijzing op profielniveau.

Verkoper- en koperpartijen

De partijstructuur in UBL is genest: de partij bevindt zich binnen een rolelement.

// BG-4: Verkoperspartij
var seller = new XElement(Cac + "AccountingSupplierParty",
    new XElement(Cac + "Party",
        // BT-34: Elektronisch adres verkoper (Peppol endpoint-ID)
        new XElement(Cbc + "EndpointID",
            new XAttribute("schemeID", "0208"),
            "0468863455"),

        // BT-27: Naam verkoper
        new XElement(Cac + "PartyName",
            new XElement(Cbc + "Name", "Acme GmbH")),

        // BG-5: Postadres verkoper
        new XElement(Cac + "PostalAddress",
            new XElement(Cbc + "StreetName", "Hauptstrasse 1"),
            new XElement(Cbc + "CityName", "Berlin"),
            new XElement(Cbc + "PostalZone", "10115"),
            new XElement(Cac + "Country",
                new XElement(Cbc + "IdentificationCode", "DE"))),

        // BT-31: Btw-identificator verkoper
        new XElement(Cac + "PartyTaxScheme",
            new XElement(Cbc + "CompanyID", "DE123456789"),
            new XElement(Cac + "TaxScheme",
                new XElement(Cbc + "ID", "VAT"))),

        // BT-30: Wettelijk registratie-identificator verkoper
        new XElement(Cac + "PartyLegalEntity",
            new XElement(Cbc + "RegistrationName", "Acme GmbH"),
            new XElement(Cbc + "CompanyID", "HRB 12345"))
    )
);

// BG-7: Koperspartij
var buyer = new XElement(Cac + "AccountingCustomerParty",
    new XElement(Cac + "Party",
        // BT-49: Elektronisch adres koper (Peppol endpoint-ID)
        new XElement(Cbc + "EndpointID",
            new XAttribute("schemeID", "0106"),
            "123456789B01"),

        // BT-44: Naam koper
        new XElement(Cac + "PartyName",
            new XElement(Cbc + "Name", "Buyer BV")),

        new XElement(Cac + "PostalAddress",
            new XElement(Cbc + "StreetName", "Hoofdstraat 1"),
            new XElement(Cbc + "CityName", "Amsterdam"),
            new XElement(Cbc + "PostalZone", "1000 AA"),
            new XElement(Cac + "Country",
                new XElement(Cbc + "IdentificationCode", "NL"))),

        new XElement(Cac + "PartyLegalEntity",
            new XElement(Cbc + "RegistrationName", "Buyer BV"),
            new XElement(Cbc + "CompanyID", "123456789B01"))
    )
);

Het schemeID-attribuut op EndpointID is het ISO 6523-schema. 0208 is het Belgische ondernemingsnummer, 0106 is het Nederlandse KvK-nummer. Een verkeerde schemacode laat deelnemersopzoekingen mislukken in het Peppol-netwerk.

Regelitems

Elk regelitem koppelt aan een InvoiceLine-element:

static XElement BuildLine(int lineId, string description, decimal quantity,
    decimal unitPrice, decimal vatRate)
{
    var lineNetAmount = Math.Round(quantity * unitPrice, 2);

    return new XElement(Cac + "InvoiceLine",
        // BT-126: Identificator factuurregel
        new XElement(Cbc + "ID", lineId.ToString()),

        // BT-129: Gefactureerde hoeveelheid
        new XElement(Cbc + "InvoicedQuantity",
            new XAttribute("unitCode", "C62"),  // C62 = stuk/eenheid
            quantity),

        // BT-131: Nettobedrag factuurregel
        new XElement(Cbc + "LineExtensionAmount",
            new XAttribute("currencyID", "EUR"),
            lineNetAmount),

        // BG-29: Prijs
        new XElement(Cac + "Price",
            new XElement(Cbc + "PriceAmount",
                new XAttribute("currencyID", "EUR"),
                unitPrice)),

        // BG-30: Btw op regel
        new XElement(Cac + "Item",
            new XElement(Cbc + "Name", description),
            new XElement(Cac + "ClassifiedTaxCategory",
                new XElement(Cbc + "ID", "S"),
                new XElement(Cbc + "Percent", vatRate),
                new XElement(Cac + "TaxScheme",
                    new XElement(Cbc + "ID", "VAT"))))
    );
}

De unitCode is een UN/ECE-aanbeveling 20-code. C62 staat voor stuks. Andere veelgebruikte codes: HUR (uur), KGM (kilogram), MTR (meter).

Belastingtotalen

EN16931 vereist belastingbedragen gegroepeerd per categorie en tarief. Voor een eenvoudige factuur met een enkel tarief:

static XElement BuildTaxTotal(decimal taxableAmount, decimal vatRate, decimal vatAmount)
{
    return new XElement(Cac + "TaxTotal",
        new XElement(Cbc + "TaxAmount",
            new XAttribute("currencyID", "EUR"),
            vatAmount),
        new XElement(Cac + "TaxSubtotal",
            new XElement(Cbc + "TaxableAmount",
                new XAttribute("currencyID", "EUR"),
                taxableAmount),
            new XElement(Cbc + "TaxAmount",
                new XAttribute("currencyID", "EUR"),
                vatAmount),
            new XElement(Cac + "TaxCategory",
                new XElement(Cbc + "ID", "S"),
                new XElement(Cbc + "Percent", vatRate),
                new XElement(Cac + "TaxScheme",
                    new XElement(Cbc + "ID", "VAT"))))
    );
}

Documenttotalen

De LegalMonetaryTotal moet voldoen aan verschillende EN16931-rekenregels. Rond af op aggregaatniveau, niet per regel:

static XElement BuildTotals(decimal lineNetTotal, decimal vatAmount, decimal grandTotal)
{
    return new XElement(Cac + "LegalMonetaryTotal",
        // BT-106: Som nettobedragen regels
        new XElement(Cbc + "LineExtensionAmount",
            new XAttribute("currencyID", "EUR"), lineNetTotal),

        // BT-109: Factuurtotaal excl. btw
        new XElement(Cbc + "TaxExclusiveAmount",
            new XAttribute("currencyID", "EUR"), lineNetTotal),

        // BT-112: Factuurtotaal incl. btw
        new XElement(Cbc + "TaxInclusiveAmount",
            new XAttribute("currencyID", "EUR"), grandTotal),

        // BT-115: Te betalen bedrag
        new XElement(Cbc + "PayableAmount",
            new XAttribute("currencyID", "EUR"), grandTotal)
    );
}

Bereken bedragen als decimal, rond consequent af met Math.Round(value, 2, MidpointRounding.AwayFromZero). Het door elkaar gebruiken van afrondingsstrategieen voor regels en totalen is de meest voorkomende oorzaak van BR-CO-10- en BR-CO-15-fouten.

Schematron-validatie

Schemavalidatie (XSD) detecteert structurele fouten. Schematron-validatie detecteert schendingen van bedrijfsregels. Je hebt beide nodig.

Download de voorgecompileerde XSLT-bestanden van de Peppol BIS GitHub releases. Voer ze vervolgens uit met Saxon HE:

dotnet add package SaxonHE12s9apiExtensions
using net.sf.saxon.s9api;

static IEnumerable<string> ValidateWithSchematron(string invoiceXml, string xsltPath)
{
    var proc = new Processor(licensed: false);
    var exec = proc.newXsltCompiler()
                   .compile(new StreamSource(xsltPath));

    var trans  = exec.load30();
    var result = new XdmDestination();
    trans.applyTemplates(
        new StreamSource(new java.io.StringReader(invoiceXml)),
        result);

    var svrl = XDocument.Parse(result.XdmNode.ToString());
    var svrlNs = XNamespace.Get("http://purl.oclc.org/dsdl/svrl");
    return svrl.Descendants(svrlNs + "failed-assert")
               .Select(e => e.Element(svrlNs + "text")?.Value?.Trim() ?? "")
               .Where(t => !string.IsNullOrEmpty(t));
}

Voer dit uit tegen peppol-en16931-ubl.xsl uit de release-artefacten. Het XSLT-bestand past zowel de EN16931-basisregels als de Peppol BIS 3.0-extensieregels toe.

Betaalmiddelen

Als je een bankoverschrijving (SEPA) wilt opgeven, voeg dit dan toe voor TaxTotal:

new XElement(Cac + "PaymentMeans",
    // BT-81: Code betaalmiddel (30 = Overschrijving)
    new XElement(Cbc + "PaymentMeansCode", "30"),
    new XElement(Cac + "PayeeFinancialAccount",
        new XElement(Cbc + "ID", "NL91ABNA0417164300"),  // IBAN
        new XElement(Cac + "FinancialInstitutionBranch",
            new XElement(Cbc + "ID", "ABNANL2A"))))  // BIC

BT-81-code 30 (overschrijving) is de meest gebruikte. 58 (SEPA-overschrijving) is de variant wanneer SEPA-specifieke velden nodig zijn.

Volgende stap

Een geldige XML-factuur genereren is de basis. De volgende laag is transmissie: de factuur moet de koper bereiken via een Peppol Access Point. Dat vereist kennis van het endpoint van de koper, dat je ophaalt via SMP/SML-discovery. Zie hoe Peppol SMP- en SML-discovery werkt voor het opzoekproces.

Voor productiegebruik biedt SealDoc een REST-API die UBL-generatie, Schematron-validatie en Peppol-netwerkroutering in een enkele aanroep afhandelt. De invoer zijn je factuurgegevens als JSON; de uitvoer is een gevalideerd, van een tijdstempel voorzien document klaar voor bezorging.


← Back to all articles