Overview
If you already have UBL (Universal Business Language) XML files, you can send them directly via e-invoice.be without converting from JSON. This is useful when:
You have an ERP system that generates UBL XML
You’re migrating from another Peppol Access Point
You have existing UBL files to send
You want full control over the UBL structure
Prerequisites
Your UBL XML must:
Be valid UBL BIS Billing 3.0 format
Comply with Peppol specifications
Be either an Invoice or Credit Note
Use the /api/validate/ubl endpoint to validate your UBL XML before creating documents.
Workflow
Validate your UBL XML (recommended)
Create document from UBL
Send via Peppol
Step 1: Validate UBL XML
Before creating a document, validate your UBL file:
curl -X POST "https://api.e-invoice.be/api/validate/ubl" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/xml" \
--data-binary @invoice.xml
Validation Response
Valid UBL :
{
"valid" : true ,
"message" : "UBL document is valid and complies with Peppol BIS Billing 3.0" ,
"document_type" : "Invoice" ,
"invoice_number" : "INV-2024-001"
}
Invalid UBL :
{
"valid" : false ,
"errors" : [
{
"field" : "cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:CompanyID" ,
"message" : "Missing required Peppol ID"
},
{
"field" : "cbc:ProfileID" ,
"message" : "Invalid BIS profile. Expected: urn:fdc:peppol.eu:2017:poacc:billing:01:1.0"
}
]
}
Step 2: Create Document from UBL
Once validation passes, create the document:
curl -X POST "https://api.e-invoice.be/api/documents/ubl" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/xml" \
--data-binary @invoice.xml
Response
{
"id" : "doc_abc123" ,
"type" : "INVOICE" ,
"state" : "DRAFT" ,
"invoice_number" : "INV-2024-001" ,
"issue_date" : "2024-10-24" ,
"total_amount" : 1210.00 ,
"tax_amount" : 210.00 ,
"created_at" : 1729468923 ,
"updated_at" : 1729468923
}
Step 3: Send via Peppol
Send the document using the document ID:
curl -X POST "https://api.e-invoice.be/api/documents/doc_abc123/send" \
-H "Authorization: Bearer YOUR_API_KEY"
Response
{
"id" : "doc_abc123" ,
"state" : "TRANSIT" ,
"message" : "Document queued for transmission via Peppol"
}
Example UBL Invoice
Here’s a minimal valid UBL invoice:
<? xml version = "1.0" encoding = "UTF-8" ?>
< Invoice xmlns = "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:fdc:peppol.eu:2017:poacc:billing:3.0 </ cbc:CustomizationID >
< cbc:ProfileID > urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 </ cbc:ProfileID >
< cbc:ID > INV-2024-001 </ cbc:ID >
< cbc:IssueDate > 2024-10-24 </ cbc:IssueDate >
< cbc:DueDate > 2024-11-24 </ cbc:DueDate >
< cbc:InvoiceTypeCode > 380 </ cbc:InvoiceTypeCode >
< cbc:DocumentCurrencyCode > EUR </ cbc:DocumentCurrencyCode >
<!-- Supplier -->
< cac:AccountingSupplierParty >
< cac:Party >
< cbc:EndpointID schemeID = "0208" > 0123456789 </ cbc:EndpointID >
< cac:PartyName >
< cbc:Name > Your Company </ cbc:Name >
</ cac:PartyName >
< cac:PostalAddress >
< cbc:StreetName > Main Street </ cbc:StreetName >
< cbc:BuildingNumber > 123 </ cbc:BuildingNumber >
< cbc:CityName > Brussels </ cbc:CityName >
< cbc:PostalZone > 1000 </ cbc:PostalZone >
< cac:Country >
< cbc:IdentificationCode > BE </ cbc:IdentificationCode >
</ cac:Country >
</ cac:PostalAddress >
< cac:PartyLegalEntity >
< cbc:RegistrationName > Your Company BVBA </ cbc:RegistrationName >
< cbc:CompanyID schemeID = "0208" > 0123456789 </ cbc:CompanyID >
</ cac:PartyLegalEntity >
</ cac:Party >
</ cac:AccountingSupplierParty >
<!-- Customer -->
< cac:AccountingCustomerParty >
< cac:Party >
< cbc:EndpointID schemeID = "0208" > 0987654321 </ cbc:EndpointID >
< cac:PartyName >
< cbc:Name > Customer Company </ cbc:Name >
</ cac:PartyName >
< cac:PostalAddress >
< cbc:StreetName > Customer Lane </ cbc:StreetName >
< cbc:BuildingNumber > 456 </ cbc:BuildingNumber >
< cbc:CityName > Antwerp </ cbc:CityName >
< cbc:PostalZone > 2000 </ cbc:PostalZone >
< cac:Country >
< cbc:IdentificationCode > BE </ cbc:IdentificationCode >
</ cac:Country >
</ cac:PostalAddress >
< cac:PartyLegalEntity >
< cbc:RegistrationName > Customer Company NV </ cbc:RegistrationName >
< cbc:CompanyID schemeID = "0208" > 0987654321 </ cbc:CompanyID >
</ cac:PartyLegalEntity >
</ cac:Party >
</ cac:AccountingCustomerParty >
<!-- Totals -->
< cac:LegalMonetaryTotal >
< cbc:LineExtensionAmount currencyID = "EUR" > 1000.00 </ cbc:LineExtensionAmount >
< cbc:TaxExclusiveAmount currencyID = "EUR" > 1000.00 </ cbc:TaxExclusiveAmount >
< cbc:TaxInclusiveAmount currencyID = "EUR" > 1210.00 </ cbc:TaxInclusiveAmount >
< cbc:PayableAmount currencyID = "EUR" > 1210.00 </ cbc:PayableAmount >
</ cac:LegalMonetaryTotal >
<!-- Tax Total -->
< cac:TaxTotal >
< cbc:TaxAmount currencyID = "EUR" > 210.00 </ cbc:TaxAmount >
< cac:TaxSubtotal >
< cbc:TaxableAmount currencyID = "EUR" > 1000.00 </ cbc:TaxableAmount >
< cbc:TaxAmount currencyID = "EUR" > 210.00 </ cbc:TaxAmount >
< cac:TaxCategory >
< cbc:ID > S </ cbc:ID >
< cbc:Percent > 21.0 </ cbc:Percent >
< cac:TaxScheme >
< cbc:ID > VAT </ cbc:ID >
</ cac:TaxScheme >
</ cac:TaxCategory >
</ cac:TaxSubtotal >
</ cac:TaxTotal >
<!-- Line Items -->
< cac:InvoiceLine >
< cbc:ID > 1 </ cbc:ID >
< cbc:InvoicedQuantity unitCode = "C62" > 10 </ cbc:InvoicedQuantity >
< cbc:LineExtensionAmount currencyID = "EUR" > 1000.00 </ cbc:LineExtensionAmount >
< cac:Item >
< cbc:Description > Professional Services </ cbc:Description >
< cbc:Name > Professional Services </ cbc:Name >
< cac:ClassifiedTaxCategory >
< cbc:ID > S </ cbc:ID >
< cbc:Percent > 21.0 </ cbc:Percent >
< cac:TaxScheme >
< cbc:ID > VAT </ cbc:ID >
</ cac:TaxScheme >
</ cac:ClassifiedTaxCategory >
</ cac:Item >
< cac:Price >
< cbc:PriceAmount currencyID = "EUR" > 100.00 </ cbc:PriceAmount >
</ cac:Price >
</ cac:InvoiceLine >
</ Invoice >
Code Examples
Node.js
const axios = require ( 'axios' );
const fs = require ( 'fs' );
const api = axios . create ({
baseURL: 'https://api.e-invoice.be' ,
headers: {
'Authorization' : `Bearer ${ process . env . E_INVOICE_API_KEY } `
}
});
async function sendUBLInvoice ( ublFilePath ) {
try {
// Read UBL file
const ublXml = fs . readFileSync ( ublFilePath , 'utf8' );
// 1. Validate UBL
console . log ( 'Validating UBL...' );
const validation = await api . post ( '/api/validate/ubl' , ublXml , {
headers: { 'Content-Type' : 'application/xml' }
});
if ( ! validation . data . valid ) {
console . error ( 'UBL validation failed:' , validation . data . errors );
return ;
}
console . log ( '✓ UBL is valid' );
// 2. Create document from UBL
console . log ( 'Creating document...' );
const document = await api . post ( '/api/documents/ubl' , ublXml , {
headers: { 'Content-Type' : 'application/xml' }
});
console . log ( '✓ Document created:' , document . data . id );
// 3. Send via Peppol
console . log ( 'Sending document...' );
const result = await api . post ( `/api/documents/ ${ document . data . id } /send` );
console . log ( '✓ Document sent:' , result . data . state );
} catch ( error ) {
console . error ( 'Error:' , error . response ?. data || error . message );
}
}
// Usage
sendUBLInvoice ( './invoices/invoice.xml' );
Python
import requests
import os
API_KEY = os.environ.get( 'E_INVOICE_API_KEY' )
BASE_URL = 'https://api.e-invoice.be'
headers = {
'Authorization' : f 'Bearer { API_KEY } '
}
def send_ubl_invoice ( ubl_file_path ):
try :
# Read UBL file
with open (ubl_file_path, 'r' , encoding = 'utf-8' ) as file :
ubl_xml = file .read()
# 1. Validate UBL
print ( 'Validating UBL...' )
validation = requests.post(
f ' { BASE_URL } /api/validate/ubl' ,
data = ubl_xml,
headers = { ** headers, 'Content-Type' : 'application/xml' }
)
if not validation.json().get( 'valid' ):
print ( 'UBL validation failed:' , validation.json().get( 'errors' ))
return
print ( '✓ UBL is valid' )
# 2. Create document
print ( 'Creating document...' )
document = requests.post(
f ' { BASE_URL } /api/documents/ubl' ,
data = ubl_xml,
headers = { ** headers, 'Content-Type' : 'application/xml' }
)
doc_id = document.json()[ 'id' ]
print ( f '✓ Document created: { doc_id } ' )
# 3. Send via Peppol
print ( 'Sending document...' )
result = requests.post(
f ' { BASE_URL } /api/documents/ { doc_id } /send' ,
headers = headers
)
print ( f '✓ Document sent: { result.json()[ "state" ] } ' )
except Exception as error:
print ( f 'Error: { error } ' )
# Usage
send_ubl_invoice( './invoices/invoice.xml' )
Retrieving UBL from Created Documents
If you created a document via JSON and want to retrieve the generated UBL:
curl -X GET "https://api.e-invoice.be/api/documents/{document_id}/ubl" \
-H "Authorization: Bearer YOUR_API_KEY"
This returns the UBL XML that was generated from your JSON payload.
Common UBL Validation Errors
Missing Peppol Profile
Error :
{
"field" : "cbc:ProfileID" ,
"message" : "Missing or invalid ProfileID"
}
Fix : Add the correct profile ID:
< cbc:ProfileID > urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 </ cbc:ProfileID >
Invalid Customization ID
Error :
{
"field" : "cbc:CustomizationID" ,
"message" : "Invalid CustomizationID for Peppol BIS Billing 3.0"
}
Fix : Use the correct customization ID:
< cbc:CustomizationID > urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 </ cbc:CustomizationID >
Missing Endpoint ID
Error :
{
"field" : "cac:AccountingSupplierParty/cac:Party/cbc:EndpointID" ,
"message" : "Missing Peppol endpoint ID"
}
Fix : Add endpoint IDs for both parties:
< cbc:EndpointID schemeID = "0208" > 0123456789 </ cbc:EndpointID >
Invalid Tax Category
Error :
{
"field" : "cac:TaxCategory/cbc:ID" ,
"message" : "Invalid tax category code"
}
Fix : Use valid UNCL5305 codes (S, E, Z, etc.):
< cac:TaxCategory >
< cbc:ID > S </ cbc:ID >
< cbc:Percent > 21.0 </ cbc:Percent >
</ cac:TaxCategory >
Best Practices
Always Validate Before Creating
Use /api/validate/ubl to catch errors early: # Validate first
curl -X POST "https://api.e-invoice.be/api/validate/ubl" \
--data-binary @invoice.xml
# Only create if validation passes
curl -X POST "https://api.e-invoice.be/api/documents/ubl" \
--data-binary @invoice.xml
Ensure all UBL namespaces are correctly declared: < Invoice xmlns = "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" >
Before sending, verify customer Peppol IDs: curl "https://api.e-invoice.be/api/validate/peppol-id?peppol_id=0208:0987654321"
Handle Large Files Efficiently
For large UBL files:
Use streaming when reading files
Consider compressing before transmission
Check file size limits in your HTTP client
Keep a copy of the original UBL for audit purposes: // Before sending
const backup = `./backup/ ${ invoiceNumber } .xml` ;
fs . copyFileSync ( ublFilePath , backup );
UBL Credit Notes
Credit notes follow the same process but use the CreditNote element:
<? xml version = "1.0" encoding = "UTF-8" ?>
< CreditNote xmlns = "urn:oasis:names:specification:ubl:schema:xsd:CreditNote-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:fdc:peppol.eu:2017:poacc:billing:3.0 </ cbc:CustomizationID >
< cbc:ProfileID > urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 </ cbc:ProfileID >
< cbc:ID > CN-2024-001 </ cbc:ID >
< cbc:IssueDate > 2024-10-24 </ cbc:IssueDate >
< cbc:CreditNoteTypeCode > 381 </ cbc:CreditNoteTypeCode >
< cbc:DocumentCurrencyCode > EUR </ cbc:DocumentCurrencyCode >
<!-- Reference to original invoice -->
< cac:BillingReference >
< cac:InvoiceDocumentReference >
< cbc:ID > INV-2024-001 </ cbc:ID >
</ cac:InvoiceDocumentReference >
</ cac:BillingReference >
<!-- Rest of credit note structure -->
</ CreditNote >
Resources
Next Steps