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. Both /api/validate/ubl and /api/documents/ubl accept multipart/form-data with a file field — not a raw XML body.
curl -X POST "https://api.e-invoice.be/api/validate/ubl" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@invoice.xml"
Do not use -H "Content-Type: application/xml" --data-binary @invoice.xml — that returns 422 Field required: body.file. See the validation guide for the full request contract and copy-paste examples in Node.js, Python, C#, and PHP.
Validation Response
Valid UBL :
{
"id" : "a1b2c3d4-5678-90ab-cdef-1234567890ab" ,
"file_name" : "invoice.xml" ,
"is_valid" : true ,
"issues" : []
}
Invalid UBL :
{
"id" : "a1b2c3d4-5678-90ab-cdef-1234567890ab" ,
"file_name" : "invoice.xml" ,
"is_valid" : false ,
"issues" : [
{
"message" : "Belgian enterprise number MUST be stated in the correct format." ,
"type" : "error" ,
"rule_id" : "PEPPOL-COMMON-R043" ,
"flag" : "fatal" ,
"schematron" : "PEPPOL-EN16931"
}
]
}
Step 2: Create Document from UBL
Once validation passes, create the document. Same contract: multipart/form-data with a file field.
curl -X POST "https://api.e-invoice.be/api/documents/ubl" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@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"
}
Peppol ID Routing
By default, sender and receiver Peppol IDs are automatically derived from the company identifiers in your document, regardless of any endpoint IDs specified in the UBL XML :
Derived from company tax IDs (vendor_tax_id / customer_tax_id) OR company IDs (vendor_id / customer_id)
For Belgian companies: 0208 scheme is always used (Belgian government requirement)
Example: Tax ID BE1018265814 → Peppol ID 0208:1018265814
UBL Endpoint IDs are ignored : Even if your UBL document contains specific endpoint IDs (e.g., <cbc:EndpointID schemeID="0088">1234567890123</cbc:EndpointID>), these are not used for routing . The API derives Peppol IDs from the company identifiers in the document metadata instead.
Explicitly Specifying Peppol IDs (Recommended)
Best Practice : Always explicitly specify sender and receiver Peppol IDs using query parameters to ensure documents are routed to the correct endpoints. This is especially important when sending UBL documents, as endpoint IDs within the UBL XML are ignored.
To route to a specific Peppol endpoint, explicitly provide the Peppol IDs via query parameters:
curl -X POST "https://api.e-invoice.be/api/documents/doc_abc123/send?sender_peppol_scheme=0208&sender_peppol_id=1018265814&receiver_peppol_scheme=0088&receiver_peppol_id=1234567890123" \
-H "Authorization: Bearer YOUR_API_KEY"
Available query parameters :
Parameter Description Example sender_peppol_schemeSender’s Peppol scheme ID 0208sender_peppol_idSender’s Peppol identifier 1018265814receiver_peppol_schemeReceiver’s Peppol scheme ID 0088receiver_peppol_idReceiver’s Peppol identifier 1234567890123
Common Peppol schemes :
0208 - Belgian enterprise number (BE)
0088 - Global Location Number (GLN)
0106 - Dutch KVK
Always verify the recipient is registered at the specified Peppol endpoint before sending. Use /api/validate/peppol-id?peppol_id=scheme:identifier to confirm registration and prevent delivery failures.
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 FormData = require ( 'form-data' );
const BASE_URL = 'https://api.e-invoice.be' ;
const authHeader = { Authorization: `Bearer ${ process . env . E_INVOICE_API_KEY } ` };
// Both /api/validate/ubl and /api/documents/ubl take multipart/form-data
// with a single "file" field. Always create a fresh FormData per request —
// the underlying stream is consumed on send.
function uploadXml ( path ) {
const form = new FormData ();
form . append ( 'file' , fs . createReadStream ( path ));
return form ;
}
async function sendUBLInvoice ( ublFilePath ) {
try {
// 1. Validate UBL
console . log ( 'Validating UBL...' );
const validateForm = uploadXml ( ublFilePath );
const validation = await axios . post (
` ${ BASE_URL } /api/validate/ubl` ,
validateForm ,
{ headers: { ... authHeader , ... validateForm . getHeaders () } }
);
if ( ! validation . data . is_valid ) {
console . error ( 'UBL validation failed:' , validation . data . issues );
return ;
}
console . log ( '✓ UBL is valid' );
// 2. Create document from UBL
console . log ( 'Creating document...' );
const createForm = uploadXml ( ublFilePath );
const document = await axios . post (
` ${ BASE_URL } /api/documents/ubl` ,
createForm ,
{ headers: { ... authHeader , ... createForm . getHeaders () } }
);
console . log ( '✓ Document created:' , document . data . id );
// 3. Send via Peppol
console . log ( 'Sending document...' );
const result = await axios . post (
` ${ BASE_URL } /api/documents/ ${ document . data . id } /send` ,
null ,
{ headers: authHeader }
);
console . log ( '✓ Document sent:' , result . data . state );
} catch ( error ) {
console . error ( 'Error:' , error . response ?. data || error . message );
}
}
// Usage
sendUBLInvoice ( './invoices/invoice.xml' );
Python
import os
import requests
API_KEY = os.environ[ 'E_INVOICE_API_KEY' ]
BASE_URL = 'https://api.e-invoice.be'
HEADERS = { 'Authorization' : f 'Bearer { API_KEY } ' }
# Both /api/validate/ubl and /api/documents/ubl take multipart/form-data
# with a single "file" field. requests builds the multipart envelope when
# you pass `files=`; do NOT also set Content-Type — requests sets it
# (with the boundary) for you.
def send_ubl_invoice ( ubl_file_path ):
try :
# 1. Validate UBL
print ( 'Validating UBL...' )
with open (ubl_file_path, 'rb' ) as f:
validation = requests.post(
f ' { BASE_URL } /api/validate/ubl' ,
headers = HEADERS ,
files = { 'file' : (os.path.basename(ubl_file_path), f, 'application/xml' )},
)
if not validation.json().get( 'is_valid' ):
print ( 'UBL validation failed:' , validation.json().get( 'issues' ))
return
print ( '✓ UBL is valid' )
# 2. Create document
print ( 'Creating document...' )
with open (ubl_file_path, 'rb' ) as f:
document = requests.post(
f ' { BASE_URL } /api/documents/ubl' ,
headers = HEADERS ,
files = { 'file' : (os.path.basename(ubl_file_path), f, '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. Both endpoints take multipart/form-data with a file field — use -F (curl) / files= (requests) / FormData (axios), not raw XML bodies. # Validate first
curl -X POST "https://api.e-invoice.be/api/validate/ubl" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@invoice.xml"
# Only create if validation passes
curl -X POST "https://api.e-invoice.be/api/documents/ubl" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@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
Creating from PDF Learn how to create documents from PDF files
Creating Invoices Create invoices from JSON
Validation Guide Test invoice data
API Reference Explore all endpoints