Skip to main content

Document Validation

GoRoute's full validation stack checks a document in several layers, in a fixed order. It runs on deep validation (POST /api/v1/invoices/validate/deep), the CSV import preview, the CSV import-and-send job and GoRoute's readiness assessments. Other validation calls run lighter checks โ€” see the other validation endpoints.

Validation Layersโ€‹

Document โ†’ XSD โ†’ STRUCTURE โ†’ BUSINESS โ†’ EDEC โ†’ SCHEMATRON โ†’ SCHEMATRON_CIUS

Each finding names the layer that produced it in its layer field. The first four layers stop at the first one that fails, so a document refused by the UBL 2.1 schema shows only schema findings; fix them and validate again to see the next layer. (A readiness assessment reports every layer at once.) The two Schematron layers run in full.

layerWhat it checks
XSDThe official UBL 2.1 schema: UBL-Invoice-2.1.xsd for an Invoice, UBL-CreditNote-2.1.xsd for a Credit Note. Element names, order and nesting, required elements, data types and namespaces. Every violation is fatal, with identifier UBL-XSD. A document that is not well-formed XML is XML_PARSE_ERROR; a root element that is not a UBL Invoice or CreditNote is also UBL-XSD
STRUCTUREGoRoute's own fast checks of the Peppol BIS 3.0 essentials: mandatory elements, identifiers and their schemes, code and date formats, each under the BR- code it mirrors. Every finding is fatal. Not the official rules โ€” the Schematron layer is
BUSINESSCalculation and business rules on the invoice data, under GoRoute's own codes such as BUS_034. Runs when GoRoute has the invoice data, not only its XML (deep validation and CSV import)
EDECThe seller's and buyer's Peppol ID schemes against the Peppol code lists, as EDEC-ICD-<scheme>. Runs when GoRoute has the invoice data
SCHEMATRONThe official CEN EN16931 and Peppol BIS 3.0 Schematron rules, or the PINT pack for a PINT document. The authoritative check: any error or fatal finding refuses the document; a warning does not
SCHEMATRON_CIUSNational CIUS rules for the receiver's country. Runs no rules today โ€” see Country CIUS below

A schema finding from deep validation has this shape (the two values in angle brackets are written by the schema validator for your document):

{
"layer": "XSD",
"identifier": "UBL-XSD",
"identifier_type": "rule_id",
"severity": "fatal",
"message": "<the schema validator's message, naming the element>",
"xpath": "<where in the document>",
"field": null,
"rule_id": null,
"assert_id": null
}

The CSV import preview reports the same layers per invoice as xsd_valid, structure_valid, business_valid, edec_valid, schematron_valid and cius_valid.

Schematron rules (SCHEMATRON)โ€‹

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

Country CIUS (SCHEMATRON_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โ€‹

Six endpoints check a document before it goes anywhere, and they take different inputs and give different answers. Pick by what you are holding and what you want back:

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
A canonical invoice, and you want to see the XML we would sendPOST /api/v1/invoices/preview-xmlValidates, then returns the rendered Peppol UBL itself instead of a verdict โ€” see Preview the XML before you send it
A canonical invoice, and you want a judgement rather than a rule listPOST /api/v1/intelligence/scoreA 0-to-1 confidence score, a risk level and a plain recommendation. Not a rules verdict โ€” see Score an invoice
A canonical invoice you want corrected for youPOST /api/v1/intelligence/auto-fixReturns a corrected copy and the list of what it changed. Read the currency caution first โ€” see Have an invoice corrected
POST /api/v1/invoices has no dry run

POST /api/v1/invoices always sends. Earlier revisions of the API reference showed a validate_only flag on it, at the top level and inside an options object next to send_immediately. The flag was never read, so the invoice was sent (and, for an Omani seller, reported to the Tax Authority). It is now refused: validate_only: true, in either place, answers HTTP 422 with error_code VALIDATE_ONLY_NOT_SUPPORTED, and nothing is created, stored or sent. To check an invoice without sending it, POST the same body to POST /api/v1/invoices/validate (or validate/deep, or preview-xml), which never send.

Never set validate_only expecting a dry run: use POST /api/v1/invoices/validate.

The same revisions taught two more keys in that options object, and neither was read either. Both are now refused the same way, at the top level or inside options, before anything is created, stored or sent:

  • send_immediately other than true answers HTTP 422 with error_code SEND_IMMEDIATELY_NOT_SUPPORTED. POST /api/v1/invoices always sends at once; there is no "send later" on it. To keep an invoice and send it later, create a draft with POST /api/v1/drafts and send it with POST /api/v1/drafts/{draft_id}/send (a draft is sent with totals calculated from its lines; the drafts page lists what else differs). send_immediately: true, or leaving it out, sends as before.
  • idempotency_key in the body answers HTTP 422 with error_code IDEMPOTENCY_KEY_IN_BODY_NOT_SUPPORTED. It never stopped a duplicate: the key is read only from the Idempotency-Key request header. Send it there. A body idempotency_key equal to the Idempotency-Key header is accepted, because the header deduplicates that request.

Anything else in an options object is still ignored.

Preview the XML before you send itโ€‹

POST /api/v1/invoices/preview-xml renders the exact Peppol UBL that the platform would transmit for your invoice, and hands it back to you instead of sending it. Nothing is transmitted, nothing is billed, no transaction record is created and no certificate is touched. When you are debugging what we build out of your JSON, this is the call that answers the question directly.

It takes the same request body as the send call โ€” an invoice object and the same options, including totals_authority โ€” and it requires the invoices:read permission, which is a lighter permission than the one needed to send.

curl -X POST "https://app.goroute.ai/peppol-api/api/v1/invoices/preview-xml" \
-H "X-API-Key: your_api_key" \
-H "Content-Type: application/json" \
-H "Accept: application/xml" \
-d '{
"invoice": { "...": "the same canonical invoice you would send" },
"totals_authority": "SYSTEM"
}'

What comes backโ€‹

On success the response is raw application/xml โ€” the UBL document itself, not JSON with the XML inside a field. It carries a filename for it:

HTTP/1.1 200 OK
Content-Type: application/xml
Content-Disposition: inline; filename="INV-2026-001.xml"

<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
...
</Invoice>

The filename is your own invoice number with .xml appended.

The invoice is validated first, and an invoice that does not pass is never rendered. In that case you get 422 with the field-level errors and the compliance score, in the same shape the other invoice-validation calls use:

{
"message": "Invoice validation failed",
"errors": [
{
"field": "buyer.name",
"code": "BUS_008",
"severity": "error",
"message": "Buyer name is required",
"technical_code": "BUS_008",
"suggested_fix": null,
"auto_fix_available": false
}
],
"score": 85,
"request_id": "โ€ฆ"
}

If totals_authority is SYSTEM โ€” which is the default โ€” the calculated totals are applied to the invoice before it is rendered, exactly as the send path does it. So the preview shows you the totals that would actually travel, not the ones you posted.

Two things it does not doโ€‹

  • It does not show you a buyer-specific variant. The preview renders with the default profile resolution, which follows the buyer's country. It will not show you the XRechnung customisation a particular German public-sector buyer would be sent, even if you set document_format in the body; that field is honoured by the send call, not by this one.
  • It is not the delivery decision. Seeing the XML tells you what would be built. It does not tell you whether the buyer is reachable on the network, or whether the document would go over Peppol or by email โ€” see Sending an Invoice and Routing for that.

Will this invoice go through, and what can be fixed for meโ€‹

The endpoints above answer "does this break a rule". Two further calls answer a different question: how likely is this invoice to be accepted, and can GoRoute correct the obvious problems for you. Both take a canonical invoice in GoRoute's JSON format.

Score an invoiceโ€‹

POST /api/v1/intelligence/score โ€” requires the reports:read permission. That is not the permission the other validation calls need, and it is worth knowing before you build against it: an API key with read or admin scope holds reports:read; a key scoped only to sending does not.

Send the invoice under an invoice key:

curl -X POST https://app.goroute.ai/peppol-api/api/v1/intelligence/score \
-H "X-API-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{"invoice": { "...": "the same canonical invoice you would send" }}'

What comes back:

FieldMeaning
overall_scoreA number from 0.0 to 1.0. How it is made up is below.
confidenceThe four parts the score is built from: data_completeness, tax_confidence, delivery_probability, compliance_score, each 0.0 to 1.0.
risk_levellow, medium, high or critical.
recommendationA short sentence in plain English, chosen from five by the overall_score. The text carries a leading symbol: โœ… Ready to send - high confidence (0.95 and above), โœ… Ready to send - good confidence (0.85), โš ๏ธ Review recommended before sending (0.70), โš ๏ธ Several issues need attention (0.50), โŒ Significant issues - not recommended to send (below 0.50).
is_sendabletrue or false โ€” see the caution below.
risk_factorsOne entry per problem found, each with the field, its risk, a reason, and auto_fixable saying whether the next call can correct it.

How overall_score is built. It is a fixed weighting of the four parts, so you can see why a score moved:

PartWeight
delivery_probability0.30
data_completeness0.25
tax_confidence0.25
compliance_score0.20
is_sendable is not a validation verdict

is_sendable is false in only two situations: the risk level is critical, or the invoice sits below a minimum compliance level that this call works out for itself. An invoice can be is_sendable: true and still be rejected by the official Schematron rules, because this call does not run them.

Use it as a triage signal โ€” which invoices to look at first โ€” and use POST /api/v1/invoices/validate/deep when you need to know whether a document actually passes.

Have an invoice correctedโ€‹

POST /api/v1/intelligence/auto-fix โ€” requires the invoices:send permission. It returns a corrected copy of your invoice and a list of every change it made. It does not send anything and it does not alter anything stored; the corrected invoice comes back to you to do as you like with.

The invoice goes in the body on its own โ€” not wrapped in an invoice key, unlike the scoring call above โ€” and the optional fixes parameter goes in the query string:

# Apply only the fixes you name
curl -X POST "https://app.goroute.ai/peppol-api/api/v1/intelligence/auto-fix?fixes=recalculate_totals&fixes=fix_rounding" \
-H "X-API-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{ "...": "the canonical invoice, at the top level" }'

The reply has fixed_invoice (the corrected copy), applied_fixes (one entry per change, each naming the action, the field, the old_value and the new_value) and fix_count.

Five fixes exist, and you name them by the values in the first column:

fixes valueWhat it does
recalculate_totalsRecalculates the invoice totals where they do not add up.
apply_default_currencySets currency to EUR when the invoice has none. Read the caution below.
fix_country_caseUpper-cases the seller's and buyer's country codes.
fix_roundingRounds net_amount, tax_amount and payable_amount to two decimals.
apply_default_unitSets a line's unit_code to EA (each) when the line has none.
Leave fixes out and it applies all five โ€” including the euro

When you do not pass fixes, every fix above is applied. One of them writes currency: "EUR" into any invoice that has no currency. It is a fixed value: it does not look at your organisation's country or at its own default currency.

For a seller in Oman, whose currency is the rial, that is a wrong currency applied without being asked for โ€” and because it happens silently, the only sign is the apply_default_currency entry in applied_fixes.

Two ways to stay safe: always set currency on the invoice yourself, or name the fixes you want in the query string and leave apply_default_currency out. Always read applied_fixes before you send the result.

We have asked GoRoute engineering to decide whether this default should stand.

Neither call replaces validation. Score the invoice to decide where to look, auto-fix to clear the mechanical problems, then validate before you send.

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.

Scheduled batchesโ€‹

Confirm that scheduling works on the service you call, before you depend on it

Everything in this section describes a GoRoute deployment that runs the batch scheduler and applies the guards below. Not every deployment runs it. Where it is not running, a schedule batch is still created and still answers 201 Created โ€” and then nothing sends it. It waits, indefinitely, and you are not told. If those invoices have a date you must meet, that silence is expensive.

Check it once, with two requests, before you build on scheduling.

  1. Does anything send it? Create a schedule batch a few minutes ahead, then read it back with GET /api/v1/batches/{batch_id}. Well past scheduled_at it should have moved off pending โ€” to processing, then completed, partial or failed. If it is still pending long after its time, nothing is going to send it. Send it yourself with POST /api/v1/batches/{batch_id}/process, and treat scheduling as a convenience you have confirmed rather than one you assume.
  2. Are the guards there? Ask for batch_type: "schedule" and leave scheduled_at out. A deployment with the guards refuses with 422 SCHEDULED_AT_REQUIRED and creates nothing. One without them creates the batch and returns 201; delete it with DELETE /api/v1/batches/{batch_id}. The same answer tells you whether the other refusals listed below are available to you.

A schedule batch is sent once, at its scheduled_at, without a further call:

  • scheduled_at is required, in ISO 8601 (read as UTC when it carries no offset), and must not be in the past. Each invoice is validated again just before it is sent; one that fails is skipped, as with POST /batches/{batch_id}/process.
  • The batch is picked up within about a minute of scheduled_at and moves to processing, then completed, partial or failed.
  • A batch first picked up more than 24 hours after its scheduled_at (for example after a long service interruption) is not sent. It ends failed with an error_message beginning SCHEDULE_EXPIRED, and the activity feed shows a batch.failed entry with reason_code: SCHEDULE_EXPIRED. To send it anyway, call /validate and then /process on it, or create a new batch. Never use lateness as a way of stopping a send. This expiry is part of the scheduler, so a deployment without the scheduler does not have it either, and an older one could send a batch days late. To stop a batch going out, cancel it with POST /api/v1/batches/{batch_id}/cancel or delete it with DELETE /api/v1/batches/{batch_id}.
  • scheduled_at on a send or validate batch, an unknown batch_type, and a schedule batch without a valid future scheduled_at are refused with 422 (error_code SCHEDULED_AT_NOT_ALLOWED, BATCH_TYPE_INVALID, SCHEDULED_AT_REQUIRED or SCHEDULED_AT_IN_PAST). Check 2 above tells you whether the deployment you call has these refusals; without them the same requests are accepted instead.
  • Where the scheduler runs, creating a schedule batch is the send, so it needs the invoices:send permission (an API key with the send scope), as /process does: a key with only read and write gets 403 INSUFFICIENT_PERMISSION for a schedule batch, while send and validate batches still need only invoices:create. A deployment without the scheduler asks only for invoices:create whatever the batch_type, so do not read a successful create as proof that scheduling is available โ€” use check 1.
  • Nothing is sent for an organisation that is not active (suspended). Its scheduled batches wait and are decided when it is active again, with the 24-hour limit still applying.
  • If the send of a scheduled batch stops part-way (the service sending it was restarted) and nothing moves for an hour, the batch becomes partial with an error_message beginning BATCH_INTERRUPTED. Invoices not yet sent stay pending: POST /batches/{batch_id}/process sends them. An invoice that was being sent when it stopped stays processing with error_code INTERRUPTED and is not sent again, because whether it went is unknown: check your transactions for it.
  • POST /batches/{batch_id}/cancel on a batch being sent stops it before its next invoice. /retry and /validate are refused while a batch is being sent.

Validation Best Practicesโ€‹

1. Validate Earlyโ€‹

# โœ… Validate before storing
def create_invoice(data):
xml = generate_ubl(data)
validation = validate_document(xml)

if not validation["valid"]:
errors = [i for i in validation["issues"] if i["severity"] == "error"]
raise ValueError(f"Invalid invoice: {errors}")

return save_invoice(xml)

2. Handle Warnings Appropriatelyโ€‹

# Warnings don't block sending but should be reviewed.
# There is no separate `warnings` array โ€” filter `issues` by severity.
if validation["warning_count"] > 0:
log.warning(f"Invoice has {validation['warning_count']} warnings")
for issue in validation["issues"]:
if issue["severity"] == "warning":
log.warning(f" {issue['code']}: {issue['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_document(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โ€‹