This guide covers PEPPOL validation, compliance rules and testing of generated UBL documents.
PEPPOL BIS Billing 3.0 release 3.0.19 is mandatory from 2025-08-25. This release updates validation artefacts and several code lists (EAS, ICD, VATEX, ISO 4217, UNCL lists). Use the latest official validators and schematron files to avoid false positives.
For rule interpretation and compliance decisions, the authoritative source in this project is:
If package behavior, local notes, or examples differ from this source, follow the BIS source and update local documentation/code accordingly.
URL: https://test.peppolautoriteit.nl/validate
URL: https://ecosio.com/en/peppol-and-xml-document-validator/
URL: https://peppol-docs.agid.gov.it/docs/validator/
Checks XML structure and data types:
<!-- Example error -->
<cbc:InvoiceNumber>123</cbc:InvoiceNumber>
<cbc:IssueDate>invalid-date</cbc:IssueDate>
Checks business rules:
Strict codelist validation requires a JSON file with the full lists you want to enforce. Only enable strict mode when these lists are complete and up-to-date.
Example structure:
{
"iso4217": ["EUR", "USD"],
"eas": ["0106", "0190", "0208", "0088"],
"icd": ["0106", "0190", "0208", "0088"],
"uncl4461": ["30", "48", "49", "57", "58", "59"],
"uncl5305": ["S", "Z", "E", "AE", "K", "G", "O"],
"uncl7143": [],
"vatex": [],
"uncl5189": [],
"uncl7161": []
}
NL123456789B01, BE0123456789)Before implementing, reviewing, or discussing PEPPOL business rules, first consult:
Use this as the primary reference for rule wording and intent, then validate generated XML with the country validators below.
use Darvis\UblPeppol\Validation\UblValidator;
use Darvis\UblPeppol\Validation\CodelistRegistry;
use Darvis\UblPeppol\UblBeBis3Service;
// VAT number validation
$error = UblValidator::validateVatNumber($vatNumber);
if ($error) {
throw new InvalidArgumentException($error);
}
// Validate VAT number
$error = UblValidator::validateVatNumber('BE0123456789');
if ($error) {
throw new InvalidArgumentException($error);
}
// Validate tax category
if (!UblValidator::isValidTaxCategory('S')) {
throw new InvalidArgumentException('Invalid tax category');
}
// Validate unit code
if (!UblValidator::isValidUnitCode('C62')) {
throw new InvalidArgumentException('Invalid unit code');
}
// Strict codelist validation (requires JSON codelists)
$registry = CodelistRegistry::fromJsonFile('/path/to/peppol-codelists.json');
$ubl = new UblBeBis3Service();
$ubl->enableStrictCodelistValidation(registry: $registry);
$ubl = new UblBeBis3Service();
$ubl->createDocument();
// ... add elements
$xml = $ubl->generateXml();
$xml = $ubl->generateXml(true); // Optional basic validation
// Strict codelist validation
$ubl->enableStrictCodelistValidation('/path/to/peppol-codelists.json');
$xml = $ubl->generateXml(true);
$ubl->addInvoiceHeader('INV-001', 'invalid-date', '2024-02-14');
// Throws: InvalidArgumentException('Invalid date format')
Upload the generated XML to a PEPPOL validator.
❌ Error: AdditionalDocumentReference missing
Solution:
$ubl->addAdditionalDocumentReference('PEPPOL', 'PEPPOLInvoice');
❌ Error: Tax category name "Standard rate" not allowed
Solution:
// Use BTCC values
'tax_category_name' => 'Taux standard' // Correct
'tax_category_name' => 'Standard rate' // Error
❌ Error: TaxTotal incorrectly positioned in InvoiceLine
Solution: UblBeBis3Service handles this automatically.
❌ Error: TaxTotal not allowed in InvoiceLine
Solution: Use UblBeBis3Service for Belgium (handles automatically).
❌ Error: Date must have YYYY-MM-DD format
Solution:
$ubl->addInvoiceHeader('INV-001', '2024-01-15', '2024-02-14'); // Correct
$ubl->addInvoiceHeader('INV-001', '15-01-2024', '14-02-2024'); // Error
❌ Error: Invalid VAT number format
Solution:
// Belgium: BE + 10 digits
'BE0123456789' // Correct
'BE123456789' // Error (9 digits)
// Netherlands: NL + 9 digits + B + 2 digits
'NL123456789B01' // Correct
'NL123456789' // Error (missing B01)
✅ XML is valid
✅ PEPPOL BIS Billing 3.0 compliant
✅ All required fields present
❌ ubl-BE-01: AdditionalDocumentReference missing
❌ ubl-BE-10: Tax category name must be BTCC value
❌ UBL-CR-561: TaxTotal not allowed in InvoiceLine
⚠️ Warning: Optional field missing
⚠️ Warning: Recommended practice not followed
The package performs automatic validation:
// Automatische validatie bij invoer
$ubl->addInvoiceHeader('', '2024-01-15', '2024-02-14');
// Throws: InvalidArgumentException('Invoice number cannot be empty')
$ubl->addInvoiceHeader('INV-001', 'invalid-date', '2024-02-14');
// Throws: InvalidArgumentException('Invalid date format')
✅ Document is valid
✅ PEPPOL BIS Billing 3.0 compliant
✅ No validation errors found
❌ Validation failed
❌ ubl-BE-10: Tax category name must be BTCC value
❌ UBL-CR-561: TaxTotal not allowed in InvoiceLine
⚠️ Warning: Optional field missing
⚠️ Warning: Recommended practice not followed
# Add to your test pipeline
php vendor/bin/pest --filter=ValidationTest
# Or use an external validator
curl -X POST https://test.peppolautoriteit.nl/validate \
-F "file=@generated_invoice.xml" \
-F "format=xml"
test('Belgian invoice validates correctly', function () {
$ubl = new UblBeBis3Service();
$ubl->createDocument();
// ... add test data
$xml = $ubl->generateXml();
expect($xml)->toContain('AdditionalDocumentReference');
expect($xml)->toContain('PEPPOL');
});
test('Generated XML passes PEPPOL validation', function () {
$xml = generateTestInvoice();
$response = Http::attach(
'file', $xml, 'test_invoice.xml'
)->post('https://test.peppolautoriteit.nl/validate');
expect($response->status())->toBe(200);
expect($response->json('valid'))->toBeTrue();
});
use Darvis\UblPeppol\Validation\UblValidator;
use Darvis\UblPeppol\UblBeBis3Service;
use Darvis\UblPeppol\UblNlBis3Service;
// Validate input data before generation (optional)
$errors = UblValidator::validateInvoiceData($invoiceData);
if (!empty($errors)) {
throw new InvalidArgumentException(implode("\n", $errors));
}
// Validate during generation (optional)
$be = new UblBeBis3Service();
$be->createDocument();
// ... add elements
$xml = $be->generateXml(true);
$nl = new UblNlBis3Service();
$nl->createDocument();
// ... add elements
$xml = $nl->generateXml(true);