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
- Official Dutch PEPPOL validator
- Tests PEPPOL BIS Billing 3.0 compliance
- Supports Dutch specifications
URL: https://ecosio.com/en/peppol-and-xml-document-validator/
- Tests EN 16931 compliance
- Belgian Schematron rules (ubl-BE-*)
- BTCC validation
URL: https://peppol-docs.agid.gov.it/docs/validator/
- General PEPPOL BIS Billing 3.0 validator
- International compliance test
- XSD schema validation
Checks XML structure and data types:
<!-- Example error -->
<cbc:InvoiceNumber>123</cbc:InvoiceNumber>
<cbc:IssueDate>invalid-date</cbc:IssueDate>Checks business rules:
- PEPPOL BIS rules (UBL): https://docs.peppol.eu/poacc/billing/3.0/files/PEPPOL-EN16931-UBL.sch
- EN 16931 rules (UBL): https://docs.peppol.eu/poacc/billing/3.0/files/CEN-EN16931-UBL.sch
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": []
}- UBL-CR-561: TaxTotal not allowed in InvoiceLine
- UBL-CR-504: Required fields present
- UBL-CR-597: Correct data formats
- ubl-BE-01: Second AdditionalDocumentReference required
- ubl-BE-10: Correct BTCC tax category names
- ubl-BE-14: TaxTotal position in InvoiceLine
- BR-CO-09: VAT number must start with ISO 3166-1 alpha-2 country code (e.g.,
NL123456789B01,BE0123456789) - BR-CO-10: Sum of invoice lines must equal LineExtensionAmount
- UBL-CR-654: PayeeFinancialAccount/ID must NOT have schemeID attribute (IBAN without schemeID)
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)- ubl-BE-01: PEPPOL AdditionalDocumentReference added
- ubl-BE-10: BTCC tax category names used
- ubl-BE-14: TaxTotal correctly positioned
- VAT number format: BE + 10 digits
- Endpoint scheme ID: 0208
- KVK/OIN number correctly detected
- VAT number format: NL + 9 digits + B + 2 digits
- Endpoint scheme ID: 0106 (KVK) or 0190 (OIN)
- Credit note has BillingReference (NL-R-001)
- NL addresses include street, city, post code (NL-R-002, NL-R-004)
- Legal entity ID uses schemeID 0106 or 0190 (NL-R-003, NL-R-005)
- Payment means present when required (NL-R-007)
- Domestic payment means code allowed (NL-R-008)
- Order line reference implies OrderReference (NL-R-009)
- No TaxTotal in InvoiceLine (UBL-CR-561)
- BIS source consulted for rule interpretation: https://docs.peppol.eu/poacc/billing/3.0/bis/
- Date format: YYYY-MM-DD
- Required fields present
- Buyer reference or order reference present (PEPPOL-EN16931-R003)
- Correct unit codes used
- XML well-formed and valid
✅ 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();
});- Generate test XML with different scenarios
- Upload to online validators
- Check all error messages
- Test with real PEPPOL endpoints (staging)
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);