Skip to main content

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

  1. Validate your UBL XML (recommended)
  2. Create document from UBL
  3. 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.
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:
ParameterDescriptionExample
sender_peppol_schemeSender’s Peppol scheme ID0208
sender_peppol_idSender’s Peppol identifier1018265814
receiver_peppol_schemeReceiver’s Peppol scheme ID0088
receiver_peppol_idReceiver’s Peppol identifier1234567890123
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

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"
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