Automation Blog Contact
Back to Blog
Automation Apr 28, 2026 18 min read

E-Invoicing in Salesforce: Generate Peppol UBL, ZUGFeRD, Factur-X and XRechnung from Flow and Apex

Salesforce generates PDFs. European e-invoicing mandates require structured XML. This guide shows how to bridge the gap using InvoiceXML from Salesforce Flow and Apex — covering Peppol UBL, ZUGFeRD, Factur-X, and XRechnung with Named Credentials, field mapping, and full Apex examples.

Salesforce generates invoices. European and global e-invoicing mandates require structured XML. These two facts create a gap that every Salesforce customer operating in Belgium, Germany, France, the Netherlands, Norway, or any Peppol-connected market is currently trying to bridge.

Salesforce does not natively produce Peppol BIS 3.0 UBL, ZUGFeRD, Factur-X, or XRechnung. It produces PDFs. Since January 2026 in Belgium, a PDF invoice is not legally sufficient for B2B transactions. Since January 2025 in Germany, trading partners must be able to receive structured e-invoices. France follows in September 2026. The mandate wave is moving faster than most Salesforce implementations have adapted.

This post shows you how to close that gap using InvoiceXML's REST API from both Salesforce Flow (no-code) and Apex (code), covering every format your European and Peppol-market customers require.


Which format does your Salesforce customer need?

Before building anything, identify the correct format for each market you operate in. Use this as your decision table — you can implement it as a Flow decision element or an Apex switch statement:

Customer countryInvoice typeRequired formatAPI endpoint
🇧🇪 BelgiumB2BPeppol BIS 3.0 (UBL)/v1/create/ubl
🇳🇱 NetherlandsB2GPeppol BIS 3.0 (UBL)/v1/create/ubl
🇳🇴 NorwayAnyEHF Billing 3.0 (UBL)/v1/create/ubl
🇩🇰 DenmarkAnyPeppol BIS 3.0 (UBL)/v1/create/ubl
🇩🇪 GermanyB2BZUGFeRD/v1/create/zugferd
🇩🇪 GermanyB2GXRechnung/v1/create/xrechnung
🇫🇷 FranceB2BFactur-X/v1/create/facturx
🇸🇬 SingaporeAnyPeppol PINT (UBL)/v1/create/ubl
🇦🇺 AustraliaAnyPeppol BIS (UBL)/v1/create/ubl
🌍 Cross-border EUB2BPeppol BIS 3.0 (UBL)/v1/create/ubl

For UBL, add the profile parameter to set the correct Peppol CIUS:

Marketprofile value
Belgium, Denmark, Sweden, cross-border EUpeppol-bis-3
Netherlands public sectornlcius
Norwayehf
Singaporepint-sg
Australiapint-au
Germany B2Gxrechnung

Store your API key as a Named Credential

Never hardcode the InvoiceXML API key in Flow or Apex. Use a Named Credential — Salesforce's secure credential store — so the key is managed centrally and never appears in code.

Setup → Security → Named Credentials → New Legacy

Label:                   InvoiceXML
Name:                    InvoiceXML
URL:                     https://api.invoicexml.com
Identity Type:           Named Principal
Authentication Protocol: Password Authentication
Username:                apikey
Password:                YOUR_INVOICEXML_API_KEY

In your HTTP callout, reference the credential as:

calloutName: InvoiceXML

The Authorization: Bearer YOUR_API_KEY header is constructed from the Named Credential automatically — you never handle the raw key in Flow nodes or Apex classes.


Salesforce Flow: no-code HTTP callout

Salesforce Flow supports HTTP callouts via the HTTP Callout action (available from Summer '23). This is the no-code path — no Apex required. Suitable for Salesforce admins building invoice generation into order or billing workflows.

Flow structure

Record-Triggered Flow
  → Trigger: Invoice record created / status updated to "Approved"
  → Get Records: fetch Invoice Line Items
  → Decision: route by customer BillingCountry
      → Belgium / cross-border: HTTP Callout → create UBL
      → Germany B2B:            HTTP Callout → create ZUGFeRD
      → Germany B2G:            HTTP Callout → create XRechnung
      → France:                 HTTP Callout → create Factur-X
  → Decision: check callout response valid = true
      → Success: Create File record, attach to Invoice
      → Failure: Create Task for review, send notification

HTTP Callout action configuration (Peppol UBL example)

Action:      HTTP Callout
Label:       Create Peppol UBL Invoice
URL:         {!$Credential.InvoiceXML.Endpoint}/v1/create/ubl
Method:      POST
Headers:
  Authorization:  Bearer {!$Credential.InvoiceXML.Password}
  Content-Type:   multipart/form-data
Body (Form Data):
  InvoiceNumber:    {!$Record.InvoiceNumber}
  IssueDate:        {!$Record.InvoiceDate}
  PaymentDueDate:   {!$Record.DueDate}
  SellerName:       {!$Setup.CompanyInfo.Name}
  SellerTaxId:      {!$Setup.CompanyInfo.TaxId}
  SellerStreet:     {!$Setup.CompanyInfo.Street}
  SellerPostcode:   {!$Setup.CompanyInfo.PostalCode}
  SellerCity:       {!$Setup.CompanyInfo.City}
  SellerCountry:    {!$Setup.CompanyInfo.Country}
  BuyerName:        {!$Record.Account.Name}
  BuyerTaxId:       {!$Record.Account.VAT_Number__c}
  BuyerStreet:      {!$Record.Account.BillingStreet}
  BuyerPostcode:    {!$Record.Account.BillingPostalCode}
  BuyerCity:        {!$Record.Account.BillingCity}
  BuyerCountry:     {!$Record.Account.BillingCountryCode}
  Currency:         {!$Record.CurrencyIsoCode}
  TaxBasisTotal:    {!$Record.TotalPrice}
  TaxTotalAmount:   {!$Record.TotalTax}
  GrandTotalAmount: {!$Record.GrandTotal}
  PaymentMeansCode: 58
  IBAN:             {!$Record.Account.IBAN__c}
  profile:          peppol-bis-3

For line items, use a Loop element over the related Invoice Line Items before the HTTP Callout, building an Apex-defined variable collection that maps to Lines[0][description], Lines[0][quantity], etc.

Handling the response

The HTTP Callout returns the invoice file as binary (application/xml for UBL/XRechnung, application/pdf for ZUGFeRD/Factur-X). In Flow:

Create Records: ContentVersion
  Title:           {!$Record.InvoiceNumber}
  PathOnClient:    {!$Record.InvoiceNumber}.pdf
  VersionData:     {!HTTPCalloutResponse.Body}

Create Records: ContentDocumentLink
  ContentDocumentId: {!NewContentVersion.ContentDocumentId}
  LinkedEntityId:    {!$Record.Id}
  ShareType:         V

Salesforce Apex: full control with HttpRequest

For more complex scenarios — bulk invoice generation, retry logic, validation before storage, custom error handling — use Apex with the standard HttpRequest / HttpResponse classes.

Named Credential callout from Apex

public class InvoiceXMLService {

    public static Blob createUblInvoice(Invoice__c invoice,
                                        List<InvoiceLineItem__c> lines) {

        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:InvoiceXML/v1/create/ubl');
        req.setMethod('POST');
        req.setTimeout(30000);

        // Build multipart/form-data body
        String boundary = 'InvoiceXML' + String.valueOf(DateTime.now().getTime());
        req.setHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);

        String body = buildMultipartBody(boundary, invoice, lines);
        req.setBody(body);

        Http http = new Http();
        HttpResponse res = http.send(req);

        if (res.getStatusCode() == 200) {
            return res.getBodyAsBlob();
        } else {
            Map<String, Object> errorBody =
                (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
            String detail = (String) errorBody.get('detail');
            throw new InvoiceGenerationException('InvoiceXML error: ' + detail);
        }
    }

    private static String buildMultipartBody(String boundary,
                                             Invoice__c invoice,
                                             List<InvoiceLineItem__c> lines) {
        String CRLF = '\r\n';
        String body = '';

        Map<String, String> fields = new Map<String, String>{
            'InvoiceNumber'    => invoice.InvoiceNumber__c,
            'IssueDate'        => String.valueOf(invoice.InvoiceDate__c),
            'PaymentDueDate'   => String.valueOf(invoice.DueDate__c),
            'SellerName'       => UserInfo.getOrganizationName(),
            'SellerTaxId'      => invoice.SellerVatId__c,
            'SellerCountry'    => invoice.SellerCountry__c,
            'BuyerName'        => invoice.Account__r.Name,
            'BuyerTaxId'       => invoice.Account__r.VAT_Number__c,
            'BuyerStreet'      => invoice.Account__r.BillingStreet,
            'BuyerPostcode'    => invoice.Account__r.BillingPostalCode,
            'BuyerCity'        => invoice.Account__r.BillingCity,
            'BuyerCountry'     => invoice.Account__r.BillingCountryCode,
            'Currency'         => invoice.CurrencyIsoCode,
            'TaxBasisTotal'    => String.valueOf(invoice.TotalPrice__c),
            'TaxTotalAmount'   => String.valueOf(invoice.TotalTax__c),
            'GrandTotalAmount' => String.valueOf(invoice.GrandTotal__c),
            'PaymentMeansCode' => '58',
            'IBAN'             => invoice.Account__r.IBAN__c,
            'profile'          => 'peppol-bis-3'
        };

        for (String key : fields.keySet()) {
            body += '--' + boundary + CRLF;
            body += 'Content-Disposition: form-data; name="' + key + '"' + CRLF;
            body += CRLF;
            body += fields.get(key) + CRLF;
        }

        // Add line items
        Integer i = 0;
        for (InvoiceLineItem__c line : lines) {
            Map<String, String> lineFields = new Map<String, String>{
                'Lines[' + i + '][description]'    => line.Description__c,
                'Lines[' + i + '][quantity]'        => String.valueOf(line.Quantity__c),
                'Lines[' + i + '][unitPrice]'       => String.valueOf(line.UnitPrice__c),
                'Lines[' + i + '][lineTotal]'       => String.valueOf(line.LineTotal__c),
                'Lines[' + i + '][unitCode]'        => 'HUR',
                'Lines[' + i + '][taxPercentage]'   => String.valueOf(line.TaxRate__c),
                'Lines[' + i + '][taxCategoryCode]' => 'S'
            };
            for (String key : lineFields.keySet()) {
                body += '--' + boundary + CRLF;
                body += 'Content-Disposition: form-data; name="' + key + '"' + CRLF;
                body += CRLF;
                body += lineFields.get(key) + CRLF;
            }
            i++;
        }

        body += '--' + boundary + '--';
        return body;
    }

    public class InvoiceGenerationException extends Exception {}
}

Saving the result as a Salesforce File

public static void attachInvoiceToRecord(Id recordId,
                                         Blob invoiceBlob,
                                         String invoiceNumber,
                                         String fileExtension) {
    ContentVersion cv = new ContentVersion();
    cv.Title         = 'Invoice-' + invoiceNumber;
    cv.PathOnClient  = 'Invoice-' + invoiceNumber + '.' + fileExtension;
    cv.VersionData   = invoiceBlob;
    cv.IsMajorVersion = true;
    insert cv;

    Id contentDocId = [
        SELECT ContentDocumentId FROM ContentVersion WHERE Id = :cv.Id
    ].ContentDocumentId;

    ContentDocumentLink cdl = new ContentDocumentLink();
    cdl.ContentDocumentId = contentDocId;
    cdl.LinkedEntityId    = recordId;
    cdl.ShareType         = 'V';
    insert cdl;
}

Validation before storage

Add a pre-storage validation call to catch generation errors before attaching the file:

public static ValidationResult validateUbl(Blob ublXml) {
    HttpRequest req = new HttpRequest();
    req.setEndpoint('callout:InvoiceXML/v1/validate/ubl');
    req.setMethod('POST');
    req.setTimeout(30000);

    String boundary = 'InvoiceXMLVal' + String.valueOf(DateTime.now().getTime());
    req.setHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);

    String CRLF = '\r\n';
    String bodyStart = '--' + boundary + CRLF
        + 'Content-Disposition: form-data; name="file"; filename="invoice.xml"' + CRLF
        + 'Content-Type: application/xml' + CRLF + CRLF;
    String bodyEnd = CRLF + '--' + boundary + '--';

    Blob fullBody = Blob.valueOf(bodyStart);
    fullBody = Blob.valueOf(
        EncodingUtil.base64Encode(fullBody) +
        EncodingUtil.base64Encode(ublXml) +
        EncodingUtil.base64Encode(Blob.valueOf(bodyEnd))
    );
    req.setBodyAsBlob(fullBody);

    Http http = new Http();
    HttpResponse res = http.send(req);

    Map<String, Object> result =
        (Map<String, Object>) JSON.deserializeUntyped(res.getBody());

    ValidationResult vr = new ValidationResult();
    vr.valid = (Boolean) result.get('valid');

    if (!vr.valid) {
        Map<String, Object> errors  = (Map<String, Object>) result.get('errors');
        List<Object> friendly       = (List<Object>) errors.get('friendly');
        vr.errorMessages = new List<String>();
        for (Object err : friendly) {
            Map<String, Object> errMap = (Map<String, Object>) err;
            vr.errorMessages.add((String) errMap.get('message'));
        }
    }
    return vr;
}

public class ValidationResult {
    public Boolean valid;
    public List<String> errorMessages;
}

ZUGFeRD and Factur-X from Salesforce

For German B2B (ZUGFeRD) and French B2B (Factur-X) the implementation is identical to the UBL examples above — change the endpoint URL and remove the profile parameter:

// ZUGFeRD — Germany B2B
req.setEndpoint('callout:InvoiceXML/v1/create/zugferd');
// No profile parameter needed — ZUGFeRD defaults to EN 16931 profile

// Factur-X — France B2B
req.setEndpoint('callout:InvoiceXML/v1/create/facturx');
// French standard VAT rate
lineFields.put('Lines[0][taxPercentage]', '20');

Both endpoints return application/pdf — a PDF/A-3b file with embedded XML. Store it with the .pdf extension. Your French and German customers receive a file that opens in any PDF viewer and imports cleanly into DATEV, SAP, Lexware, or any EN 16931-compatible accounting system.


XRechnung for German public sector

XRechnung requires one additional field that ZUGFeRD does not: the Leitweg-ID — a routing identifier provided by the German government buyer. Add a custom field to your Salesforce Account object to store it: Leitweg_ID__c.

// XRechnung — Germany B2G
req.setEndpoint('callout:InvoiceXML/v1/create/xrechnung');

// Add to your fields map:
fields.put('BuyerReference', invoice.Account__r.Leitweg_ID__c);
fields.put('syntax', 'cii'); // or 'ubl'

Add a validation rule on your Invoice object to prevent submission to German public sector accounts without a Leitweg-ID:

AND(
  Account__r.BillingCountryCode = 'DE',
  Account__r.Is_Public_Sector__c = TRUE,
  ISBLANK(Account__r.Leitweg_ID__c)
)

Error message: "XRechnung invoices require a Leitweg-ID on the account record. Please add the buyer's routing identifier before generating the invoice."


Salesforce object field mapping reference

Standard and common custom Salesforce fields mapped to InvoiceXML parameters:

InvoiceXML parameterSalesforce objectField API name
InvoiceNumberInvoice / OrderInvoiceNumber, OrderNumber
IssueDateInvoiceInvoiceDate__c
PaymentDueDateInvoiceDueDate__c
BuyerNameAccountName
BuyerTaxIdAccountVAT_Number__c (custom)
BuyerStreetAccountBillingStreet
BuyerPostcodeAccountBillingPostalCode
BuyerCityAccountBillingCity
BuyerCountryAccountBillingCountryCode
CurrencyInvoice / OpportunityCurrencyIsoCode
TaxBasisTotalInvoiceTotalPrice__c (custom)
TaxTotalAmountInvoiceTotalTax__c (custom)
GrandTotalAmountInvoiceGrandTotal__c (custom)
IBANAccountIBAN__c (custom)
BuyerReferenceAccountLeitweg_ID__c (custom, Germany B2G)
Lines[n][description]Invoice Line ItemDescription__c
Lines[n][quantity]Invoice Line ItemQuantity__c
Lines[n][unitPrice]Invoice Line ItemUnitPrice__c
Lines[n][lineTotal]Invoice Line ItemTotalPrice
Lines[n][taxPercentage]Invoice Line ItemTaxRate__c (custom)

Your seller details (name, VAT number, address, IBAN) are static — store them in Custom Metadata Types rather than fetching from records:

InvoiceXML_Config__mdt config = [
    SELECT SellerName__c, SellerTaxId__c, SellerIBAN__c, SellerCountry__c
    FROM InvoiceXML_Config__mdt
    WHERE DeveloperName = 'Default'
    LIMIT 1
];

Remote Site Settings

Before Apex HTTP callouts work, add the InvoiceXML API domain to your Remote Site Settings:

Setup → Security → Remote Site Settings → New

Remote Site Name: InvoiceXML
Remote Site URL:  https://api.invoicexml.com
Active:           ✓

Callout permissions

Apex callouts from triggers require @future(callout=true) methods or Queueable Apex with Database.AllowsCallouts. From Flow, HTTP Callout actions handle this automatically. For bulk invoice generation (more than 200 records), use Batch Apex with Database.AllowsCallouts:

global class BulkInvoiceGenerationBatch
    implements Database.Batchable<SObject>, Database.AllowsCallouts {

    global Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator([
            SELECT Id, InvoiceNumber__c, Account__r.BillingCountryCode, ...
            FROM Invoice__c
            WHERE Status__c = 'Approved'
            AND E_Invoice_Generated__c = false
        ]);
    }

    global void execute(Database.BatchableContext bc, List<Invoice__c> invoices) {
        for (Invoice__c invoice : invoices) {
            // One callout per record — batch size of 1 recommended for callouts
            Blob result = InvoiceXMLService.createUblInvoice(
                invoice, getLineItems(invoice.Id));
            InvoiceXMLService.attachInvoiceToRecord(
                invoice.Id, result, invoice.InvoiceNumber__c, 'xml');
        }
    }

    global void finish(Database.BatchableContext bc) {}
}

Set batch size to 1 when making HTTP callouts from Batch Apex to stay within Salesforce governor limits on concurrent callouts per transaction.


Why not build this natively in Salesforce

The alternative is generating ZUGFeRD, Factur-X, or Peppol UBL natively — either in an Apex class or a connected external service you maintain.

For Peppol UBL you need: UBL 2.1 XML generation, EN 16931 Schematron validation (requires XSLT 2.0 — not available natively in Apex), Peppol BIS 3.0 overlay validation (updates quarterly from OpenPeppol), and CustomizationID profile routing across multiple national CIUS variants.

For ZUGFeRD and Factur-X you need: CII XML generation, EN 16931 Schematron, PDF/A-3b container creation with embedded fonts, ICC colour profiles, XMP metadata, and correct AFRelationship declarations. Apex cannot generate PDF/A-3b files natively.

None of this is impossible — you can build an external microservice and call it from Apex the same way you call InvoiceXML. But you then own the infrastructure, the Schematron maintenance, the format version updates, and the security compliance for processing financial documents through your own service.

InvoiceXML is stateless by design — every document is processed in memory and purged on response delivery. Nothing is stored, nothing is logged, GDPR and HIPAA compliant by architecture. Your Salesforce invoice data never persists outside the API call.


Get started

Create a free InvoiceXML account → — 100 free credits, no credit card required.

Related resources:


InvoiceXML is a REST API for Peppol-ready and EN 16931-compliant e-invoice generation, validation, and conversion. It covers Peppol BIS 3.0 UBL, ZUGFeRD, Factur-X, XRechnung, and CII — accessible from Salesforce Flow, Apex, and any HTTP client.


Ready to automate your invoices?

Start your 30-day free trial. No credit card required.

Get Started