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 country | Invoice type | Required format | API endpoint |
|---|---|---|---|
| 🇧🇪 Belgium | B2B | Peppol BIS 3.0 (UBL) | /v1/create/ubl |
| 🇳🇱 Netherlands | B2G | Peppol BIS 3.0 (UBL) | /v1/create/ubl |
| 🇳🇴 Norway | Any | EHF Billing 3.0 (UBL) | /v1/create/ubl |
| 🇩🇰 Denmark | Any | Peppol BIS 3.0 (UBL) | /v1/create/ubl |
| 🇩🇪 Germany | B2B | ZUGFeRD | /v1/create/zugferd |
| 🇩🇪 Germany | B2G | XRechnung | /v1/create/xrechnung |
| 🇫🇷 France | B2B | Factur-X | /v1/create/facturx |
| 🇸🇬 Singapore | Any | Peppol PINT (UBL) | /v1/create/ubl |
| 🇦🇺 Australia | Any | Peppol BIS (UBL) | /v1/create/ubl |
| 🌍 Cross-border EU | B2B | Peppol BIS 3.0 (UBL) | /v1/create/ubl |
For UBL, add the profile parameter to set the correct Peppol CIUS:
| Market | profile value |
|---|---|
| Belgium, Denmark, Sweden, cross-border EU | peppol-bis-3 |
| Netherlands public sector | nlcius |
| Norway | ehf |
| Singapore | pint-sg |
| Australia | pint-au |
| Germany B2G | xrechnung |
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 parameter | Salesforce object | Field API name |
|---|---|---|
InvoiceNumber | Invoice / Order | InvoiceNumber, OrderNumber |
IssueDate | Invoice | InvoiceDate__c |
PaymentDueDate | Invoice | DueDate__c |
BuyerName | Account | Name |
BuyerTaxId | Account | VAT_Number__c (custom) |
BuyerStreet | Account | BillingStreet |
BuyerPostcode | Account | BillingPostalCode |
BuyerCity | Account | BillingCity |
BuyerCountry | Account | BillingCountryCode |
Currency | Invoice / Opportunity | CurrencyIsoCode |
TaxBasisTotal | Invoice | TotalPrice__c (custom) |
TaxTotalAmount | Invoice | TotalTax__c (custom) |
GrandTotalAmount | Invoice | GrandTotal__c (custom) |
IBAN | Account | IBAN__c (custom) |
BuyerReference | Account | Leitweg_ID__c (custom, Germany B2G) |
Lines[n][description] | Invoice Line Item | Description__c |
Lines[n][quantity] | Invoice Line Item | Quantity__c |
Lines[n][unitPrice] | Invoice Line Item | UnitPrice__c |
Lines[n][lineTotal] | Invoice Line Item | TotalPrice |
Lines[n][taxPercentage] | Invoice Line Item | TaxRate__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:
- E-invoicing in Microsoft Power Automate and Dynamics 365
- UBL API: the complete toolkit
- ZUGFeRD API: the complete toolkit
- Factur-X API: the complete toolkit
- XRechnung API: the complete toolkit
- EN 16931 API: the complete compliance toolkit
- E-invoicing mandate deadlines by country
- Full API documentation
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.