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 โ not implementedโ
This layer exists in the code path and does nothing: it is a stub that finds no configured pack and returns no findings. XRechnung, FatturaPA, Factur-X and NLCIUS Schematrons are not compiled into the GoRoute validator and are not run. Documents for Germany, Italy, France and the Netherlands are validated against CEN EN16931 + Peppol BIS 3.0 only.
The national requirements below are real requirements of those jurisdictions โ they are listed so you know what your document must satisfy โ but GoRoute will not catch a breach of them for you. Validate against national tooling as well if you need that assurance.
| Country | National CIUS | Key requirement โ not checked by GoRoute |
|---|---|---|
| ๐ฉ๐ช Germany | XRechnung | Leitweg-ID required for B2G |
| ๐ฎ๐น Italy | FatturaPA | Codice Destinatario required |
| ๐ซ๐ท France | Factur-X | Chorus Pro requirements |
| ๐ณ๐ฑ Netherlands | NLCIUS | KVK number format |
What is selected per jurisdiction is the PINT pack, chosen from the document's
cbc:CustomizationID โ see
Schematron packs GoRoute actually runs.
Pre-Validation APIโ
POST /api/v1/documents/validate validates a UBL document without sending it. It takes the
same JSON shape as the send endpoint โ the XML travels as a string in the document
field โ and requires the invoices:read permission.
import requests
def validate_document(xml_content: str) -> dict:
"""Validate a UBL document without sending it."""
response = requests.post(
"https://app.goroute.ai/peppol-api/api/v1/documents/validate",
headers={
"X-API-Key": "your_api_key",
"Content-Type": "application/json",
},
json={"document": xml_content},
)
return response.json()
result = validate_document(invoice_xml)
if result["valid"]:
print("Document is valid")
else:
print(f"Validation failed: {result['error_count']} error(s)")
for issue in result["issues"]:
print(f" [{issue['severity']}] {issue['code']}: {issue['message']}")
Request bodyโ
| Field | Type | Required | Meaning |
|---|---|---|---|
document | string | Yes | The UBL document as an XML string |
document_type | string | No | Defaults to the Peppol BIS 3.0 Invoice document type identifier |
profile | string | No | Validation profile, e.g. peppol-bis3 or en16931 |
Validation responseโ
{
"valid": false,
"document_type": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice",
"profile": "peppol-bis3",
"error_count": 1,
"warning_count": 1,
"issues": [
{
"severity": "error",
"code": "BR-16",
"message": "Amount MUST be rounded to maximum 2 decimals",
"location": "/Invoice/cac:InvoiceLine[1]/cbc:LineExtensionAmount",
"rule_id": "BR-16"
}
],
"sender_id": "9959:123456789",
"receiver_id": "9959:987654321",
"document_id": "INV-2026-001",
"issue_date": "2026-08-05"
}
Errors and warnings arrive in a single flat issues array, distinguished by
severity. There is no nested validation_layers object and no per-layer breakdown on
this endpoint.
| Field | Type | Meaning |
|---|---|---|
valid | boolean | Whether the document passed |
document_type | string | The document type identifier applied |
profile | string or null | The validation profile applied |
error_count | integer | Number of error issues |
warning_count | integer | Number of warning issues |
issues | array | Each with severity, code, message, and optionally location and rule_id |
sender_id | string or null | Parsed from the document, if it parsed |
receiver_id | string or null | Parsed from the document, if it parsed |
document_id | string or null | Parsed from the document, if it parsed |
issue_date | string or null | Parsed from the document, if it parsed |
The other validation endpointsโ
Three validation endpoints exist and they take different inputs. Pick by what you are holding:
| You have | Endpoint | Notes |
|---|---|---|
| UBL XML | POST /api/v1/documents/validate | The endpoint above |
| A canonical invoice (JSON) | POST /api/v1/invoices/validate | Fast structural and business-rule check; returns a compliance score and country findings such as COUNTRY_AU_001 |
| A canonical invoice, and you want Schematron | POST /api/v1/invoices/validate/deep | Full CEN EN16931 + Peppol BIS, or the PINT pack for a PINT document |
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โ
There is no endpoint that takes a list of documents and validates them in one call. Batch validation works on a batch resource: you create the batch first, then validate it by its ID.
import requests
BASE = "https://app.goroute.ai/peppol-api"
HEADERS = {
"X-API-Key": "your_api_key",
"Content-Type": "application/json",
}
def validate_batch(invoices: list) -> dict:
"""Create a validate-only batch, then validate it."""
# 1. Create the batch โ 201 Created. Maximum 1000 invoices.
batch = requests.post(
f"{BASE}/api/v1/batches",
headers=HEADERS,
json={
"name": "Nightly validation",
"batch_type": "validate",
"invoices": invoices,
},
).json()
# 2. Validate it by ID
batch_id = batch["id"]
return requests.post(
f"{BASE}/api/v1/batches/{batch_id}/validate",
headers=HEADERS,
).json()
result = validate_batch(invoice_list)
print(result["status"], result["progress"])
| Step | Endpoint | Permission | Notes |
|---|---|---|---|
| Create | POST /api/v1/batches | invoices:create | 201 Created. batch_type is send, validate or schedule. Maximum 1000 invoices. |
| Validate | POST /api/v1/batches/{batch_id}/validate | invoices:send | Validates every invoice in the batch without sending |
Both return a batch object. Progress is reported as total, pending, processing,
success, failed, skipped and percent โ not as valid_count/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โ
All paths are relative to https://app.goroute.ai/peppol-api.
Validate a UBL documentโ
POST /api/v1/documents/validate
Content-Type: application/json
{
"document": "<?xml version=\"1.0\"?><Invoice>...</Invoice>",
"document_type": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1",
"profile": "peppol-bis3"
}
Validate a canonical invoiceโ
POST /api/v1/invoices/validate
POST /api/v1/invoices/validate/deep
Content-Type: application/json
validate/deep accepts invoice, totals_authority, include_schematron (default true)
and receiver_country.
Validate a batchโ
POST /api/v1/batches
POST /api/v1/batches/{batch_id}/validate
Content-Type: application/json
Earlier revisions of this page documented skip_cius, include_warnings and a cius
query parameter for forcing a CIUS such as xrechnung. None of them exists. There is no
way to force a CIUS pack, because the national CIUS layer is not implemented โ see
How the Schematron pack is selected.
The only comparable real control is include_schematron on validate/deep, which turns
Schematron off entirely rather than selecting a pack.