Document Validation
GoRoute validates all documents through multiple layers before transmission to ensure compliance with Peppol, EU, and national requirements.
Validation Layers
Document → Layer 1 → Layer 2 → Layer 3 → Layer 4 → Transmission
│ │ │ │
▼ ▼ ▼ ▼
UBL XSD Business Schematron Country
Schema Rules (EN16931) CIUS
Layer 1: XSD Validation
Validates document structure against UBL 2.1 XML Schema:
- Element names and hierarchy
- Required elements present
- Data type correctness
- Namespace compliance
<!-- ✅ Valid structure -->
<cbc:IssueDate>2024-01-15</cbc:IssueDate>
<!-- ❌ Invalid: wrong date format -->
<cbc:IssueDate>15/01/2024</cbc:IssueDate>
<!-- ❌ Invalid: missing required element -->
<Invoice>
<!-- IssueDate is required but missing -->
</Invoice>
Layer 2: Business Rules
Validates mathematical and logical consistency:
| Rule | Description |
|---|---|
| Line totals | quantity × price = line amount |
| Tax calculations | Tax amounts match tax rates |
| Invoice total | Sum of lines + tax = payable amount |
| Date logic | Due date ≥ issue date |
# Example business rule validation
def validate_totals(invoice):
calculated_total = sum(line.amount for line in invoice.lines)
if calculated_total != invoice.total:
raise ValidationError(
f"Line total {calculated_total} does not match "
f"invoice total {invoice.total}"
)
Layer 3: Schematron Validation
Validates against EN16931 European standard and Peppol BIS 3.0:
EN16931 (Base)
│
└── Peppol BIS 3.0 (Peppol-specific rules)
│
└── Country CIUS (National requirements)
Common Schematron Errors:
| Rule | Description | Severity |
|---|---|---|
| BR-01 | Invoice must have invoice number | Error |
| BR-02 | Invoice must have issue date | Error |
| BR-16 | Line extension amount must be rounded to 2 decimals | Error |
| PEPPOL-EN16931-R001 | BIS profile must be specified | Error |
| PEPPOL-EN16931-R002 | Customization ID must be valid | Error |
Layer 4: Country CIUS
Additional rules for specific countries:
| Country | CIUS | Key Requirements |
|---|---|---|
| 🇩🇪 Germany | XRechnung | Leitweg-ID required for B2G |
| 🇮🇹 Italy | FatturaPA | Codice Destinatario required |
| 🇫🇷 France | Factur-X | Chorus Pro requirements |
| 🇳🇱 Netherlands | NLCIUS | KVK number format |
Pre-Validation API
Validate documents before sending:
import requests
def validate_invoice(xml_content: str) -> dict:
"""Validate an invoice without sending."""
response = requests.post(
"https://app.goroute.ai/peppol-api/api/v1/validate",
headers={
"X-API-Key": "your_api_key",
"Content-Type": "application/xml"
},
data=xml_content
)
return response.json()
# Example usage
result = validate_invoice(invoice_xml)
if result["valid"]:
print("✅ Document is valid")
else:
print("❌ Validation failed:")
for error in result["errors"]:
print(f" - {error['rule']}: {error['message']}")
Validation Response
{
"valid": false,
"document_type": "Invoice",
"validation_layers": {
"xsd": {"passed": true, "errors": []},
"business_rules": {"passed": true, "errors": []},
"schematron": {
"passed": false,
"errors": [
{
"rule": "BR-16",
"severity": "error",
"location": "/Invoice/cac:InvoiceLine[1]/cbc:LineExtensionAmount",
"message": "Amount MUST be rounded to maximum 2 decimals",
"value": "100.123"
}
],
"warnings": [
{
"rule": "PEPPOL-EN16931-R120",
"severity": "warning",
"location": "/Invoice/cac:AccountingSupplierParty",
"message": "Supplier VAT identifier SHOULD be provided"
}
]
},
"cius": {"passed": true, "errors": []}
},
"error_count": 1,
"warning_count": 1
}
Error Handling
Error Severity
| Severity | Action | Can Send? |
|---|---|---|
error | Must fix before sending | ❌ No |
warning | Review recommended | ✅ Yes |
info | Informational only | ✅ Yes |
Common Errors and Fixes
BR-16: Rounding Error
# ❌ Wrong: Too many decimals
<cbc:LineExtensionAmount currencyID="EUR">100.123</cbc:LineExtensionAmount>
# ✅ Correct: Maximum 2 decimals
<cbc:LineExtensionAmount currencyID="EUR">100.12</cbc:LineExtensionAmount>
BR-CO-10: Tax Total Mismatch
# ❌ Wrong: Tax total doesn't match calculated
line_tax = 21.00 # (100.00 * 21%)
invoice_tax_total = 20.00 # Incorrect
# ✅ Correct: Values match
line_tax = 21.00
invoice_tax_total = 21.00
PEPPOL-EN16931-R001: Missing Profile
<!-- ❌ Wrong: Missing ProfileID -->
<Invoice>
<cbc:CustomizationID>urn:cen.eu:en16931:2017...</cbc:CustomizationID>
<!-- ProfileID missing -->
</Invoice>
<!-- ✅ Correct: Both IDs present -->
<Invoice>
<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>
</Invoice>
Batch Validation
Validate multiple documents at once:
import requests
def validate_batch(invoices: list) -> dict:
"""Validate multiple invoices in one call."""
response = requests.post(
"https://app.goroute.ai/peppol-api/api/v1/validate/batch",
headers={
"X-API-Key": "your_api_key",
"Content-Type": "application/json"
},
json={
"documents": [
{"id": "inv-001", "xml": invoices[0]},
{"id": "inv-002", "xml": invoices[1]},
]
}
)
return response.json()
result = validate_batch(invoice_list)
print(f"Valid: {result['valid_count']}/{result['total_count']}")
Validation Best Practices
1. Validate Early
# ✅ Validate before storing
def create_invoice(data):
xml = generate_ubl(data)
validation = validate_invoice(xml)
if not validation["valid"]:
raise ValueError(f"Invalid invoice: {validation['errors']}")
return save_invoice(xml)
2. Handle Warnings Appropriately
# Warnings don't block sending but should be reviewed
if validation["warning_count"] > 0:
log.warning(f"Invoice has {validation['warning_count']} warnings")
for warning in validation["warnings"]:
log.warning(f" {warning['rule']}: {warning['message']}")
3. Cache Validation Results
import hashlib
def get_cached_validation(xml: str) -> dict:
"""Cache validation results by document hash."""
doc_hash = hashlib.sha256(xml.encode()).hexdigest()
cached = redis.get(f"validation:{doc_hash}")
if cached:
return json.loads(cached)
result = validate_invoice(xml)
redis.setex(f"validation:{doc_hash}", 3600, json.dumps(result))
return result
API Reference
Validate Single Document
POST /api/v1/validate
Content-Type: application/xml
<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="...">
...
</Invoice>
Validate Batch
POST /api/v1/validate/batch
Content-Type: application/json
{
"documents": [
{"id": "doc-1", "xml": "<Invoice>...</Invoice>"},
{"id": "doc-2", "xml": "<Invoice>...</Invoice>"}
]
}
Validate with Options
POST /api/v1/validate?skip_cius=true&include_warnings=false
Content-Type: application/xml
| Parameter | Type | Description |
|---|---|---|
skip_cius | boolean | Skip country-specific validation |
include_warnings | boolean | Include warnings in response |
cius | string | Force specific CIUS (e.g., xrechnung) |