Skip to main content

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:

RuleDescription
Line totalsquantity ร— price = line amount
Tax calculationsTax amounts match tax rates
Invoice totalSum of lines + tax = payable amount
Date logicDue 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:

RuleDescriptionSeverity
BR-01Invoice must have invoice numberError
BR-02Invoice must have issue dateError
BR-16Line extension amount must be rounded to 2 decimalsError
PEPPOL-EN16931-R001BIS profile must be specifiedError
PEPPOL-EN16931-R002Customization ID must be validError

Layer 4: Country CIUS โ€” not implementedโ€‹

No national CIUS Schematron runs today

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.

CountryNational CIUSKey requirement โ€” not checked by GoRoute
๐Ÿ‡ฉ๐Ÿ‡ช GermanyXRechnungLeitweg-ID required for B2G
๐Ÿ‡ฎ๐Ÿ‡น ItalyFatturaPACodice Destinatario required
๐Ÿ‡ซ๐Ÿ‡ท FranceFactur-XChorus Pro requirements
๐Ÿ‡ณ๐Ÿ‡ฑ NetherlandsNLCIUSKVK 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โ€‹

FieldTypeRequiredMeaning
documentstringYesThe UBL document as an XML string
document_typestringNoDefaults to the Peppol BIS 3.0 Invoice document type identifier
profilestringNoValidation 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.

FieldTypeMeaning
validbooleanWhether the document passed
document_typestringThe document type identifier applied
profilestring or nullThe validation profile applied
error_countintegerNumber of error issues
warning_countintegerNumber of warning issues
issuesarrayEach with severity, code, message, and optionally location and rule_id
sender_idstring or nullParsed from the document, if it parsed
receiver_idstring or nullParsed from the document, if it parsed
document_idstring or nullParsed from the document, if it parsed
issue_datestring or nullParsed 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 haveEndpointNotes
UBL XMLPOST /api/v1/documents/validateThe endpoint above
A canonical invoice (JSON)POST /api/v1/invoices/validateFast structural and business-rule check; returns a compliance score and country findings such as COUNTRY_AU_001
A canonical invoice, and you want SchematronPOST /api/v1/invoices/validate/deepFull CEN EN16931 + Peppol BIS, or the PINT pack for a PINT document

Error Handlingโ€‹

Error Severityโ€‹

SeverityActionCan Send?
errorMust fix before sendingโŒ No
warningReview recommendedโœ… Yes
infoInformational 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"])
StepEndpointPermissionNotes
CreatePOST /api/v1/batchesinvoices:create201 Created. batch_type is send, validate or schedule. Maximum 1000 invoices.
ValidatePOST /api/v1/batches/{batch_id}/validateinvoices:sendValidates 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
Options that do not exist

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.

Next Stepsโ€‹