xmlvalid for Python
Python client for the xmlvalid API: validate XML documents and e-invoices against XSD, DTD, RelaxNG and Schematron business rules. Peppol BIS, XRechnung, Factur-X / ZUGFeRD, EN 16931, UBL, CII and national rules (France, Italy, Croatia, Romania and more) are in the schema catalog; your own schemas work too.
- Python 3.10+, sync and async (asyncio, trio), built on httpx
- Large files are streamed, not loaded into memory
- Waits for the result for you, retries when rate limited
- Typed results and exceptions, webhook signature checks
Install
pip install xmlvalid
Create an API key under Dashboard → API keys.
Validate
from xmlvalid import Client
xmlvalid = Client("xv_…") # or set XMLVALID_API_KEY
validation = xmlvalid.validate("invoice.xml", schema="peppol-bis-3-invoice")
if validation.is_valid:
print("Valid")
else:
for error in validation.errors:
print(error) # Line 139: [BR-CO-16] Amount due for payment (BT-115) = …
The document can be:
xmlvalid.validate("invoice.xml") # a path (XML, .gz, .zip, EPUB, Factur-X PDF)
xmlvalid.validate(Path("invoice.xml")) # a Path
xmlvalid.validate(data, name="invoice.xml") # bytes
xmlvalid.validate(open("invoice.xml", "rb")) # a binary file object
xmlvalid.validate(text="<Invoice>…</Invoice>") # a string
xmlvalid.validate(url="https://example.com/feed.xml") # a public URL, downloaded by xmlvalid
The schema:
| Argument | Validates against |
|---|---|
schema="peppol-bis-3-invoice" |
A catalog entry, with its business rules |
schema="auto" |
The catalog profile an e-invoice names (BT-24), for mixed invoices |
schema="my-12" |
One of your saved schemas |
schema_file="order.xsd" |
An XSD, DTD or RelaxNG file sent with the document |
doctype=True |
The DOCTYPE the document declares |
| none | Well-formedness only |
Your own Schematron runs after the schema check: rules="rules.sch", or for rules with
includes, relative path: file: rules={"main.sch": "/path/main.sch", "parts/totals.sch": "/path/parts/totals.sch"}.
Catalog business rules move to new releases. To fail instead of getting findings from rules that changed, pin the releases a check was written against:
xmlvalid.validate(
"invoice.xml",
schema="peppol-bis-3-invoice",
rules_version=["peppol-bis-3-ubl@3.0.21", "en16931-ubl@1.3.16"], # or one string
)
When a pinned rule set runs another release, a RulesVersionMismatchError is raised before
anything runs or counts against your quota. xmlvalid.schemas() lists each entry's rule_sets
with their keys and releases.
Async
from xmlvalid import AsyncClient
async with AsyncClient() as xmlvalid:
validation = await xmlvalid.validate("invoice.xml", schema="xrechnung-3-ubl")
Every method of Client is there, awaited.
The result
validation.status # done, failed (on xmlvalid's side) or cancelled
validation.result.outcome # valid, warnings, invalid, schema, input or failed
validation.result.errors_total # all of them; the first 500 are listed
for error in validation.errors:
error.line, error.column, error.level, error.type # xsd, schematron, …
error.rule # BR-CO-16 for a business rule
error.path # where it fired: /Invoice/cac:LegalMonetaryTotal
error.message
error.rule_url # the rule's page on xmlvalid.com
error.help_url # how to fix this kind of error
validation.warnings
for rule_set in validation.rule_sets: # the business rules that ran, with their releases
rule_set.key, rule_set.version # peppol-bis-3-ubl, 3.0.21
rule_set.label # Peppol BIS Billing 3.0 rules 3.0.21
rule_set.pin # peppol-bis-3-ubl@3.0.21, for rules_version=
validation.result.sha256 # SHA-256 of the file as sent
validation.result.engine # Engine(xmlvalid='0.1.0', libxml2='2.13.9', saxonjs='2.7.0')
validation.raw # the API's JSON, as a dict
For a .zip, validation.result.items has one result per document (each with its own
rule_sets); for a PDF,
validation.result.pdf names the invoice found in it.
Long validations, reports and the account
validate() waits up to 15 minutes (timeout=); a ValidationTimeoutError carries the
validation, which goes on on the server. To queue and come back later:
validation = xmlvalid.submit("big.xml", schema="ubl-2.1-invoice")
# …
validation = xmlvalid.wait_for(validation.id) # or xmlvalid.validation(id), xmlvalid.cancel(id)
html = xmlvalid.report(validation) # the HTML report
link = xmlvalid.share([validation]) # a public link for 30 days (Pro): link.url
usage = xmlvalid.usage() # usage.used, usage.limit, usage.remaining
schemas = xmlvalid.schemas() # your saved schemas, then the catalog
Errors
Every exception extends xmlvalid.XmlvalidError. API refusals are ApiErrors with
status_code, error_code and, for invalid_request, fields:
| Exception | When |
|---|---|
AuthenticationError |
401: no key, a revoked key |
PlanLimitError |
403: not in your plan |
NotFoundError |
404: no such validation, or it expired (24 hours) |
NotFinishedError |
409: the report of a validation still running |
RulesVersionMismatchError |
409: a pinned rule set runs another release; rule_set, pinned, current |
DocumentTooLargeError |
413: over your plan's size limit |
InvalidRequestError |
422: unknown schema, a bad URL, … |
QuotaExceededError |
429: this month's validations are used up |
RateLimitedError |
429: still limited after the retries; retry_after seconds |
ServerError |
5xx |
TransportError |
The request could not be sent or its answer read |
ValidationTimeoutError |
Still running after timeout= (also a TimeoutError) |
Webhooks
On Pro, xmlvalid can call your endpoint when a validation finishes or a URL monitor changes state. Check the signature with the endpoint's secret and the raw body:
from xmlvalid import Event, webhooks
event = webhooks.verify(
request.body, # the raw bytes, as received
request.headers.get("Xmlvalid-Signature"),
os.environ["XMLVALID_WEBHOOK_SECRET"],
) # raises SignatureVerificationError
if event.type == Event.VALIDATION_COMPLETED:
validation = event.validation
A retried delivery has the same event.id. The other events are monitor.failed,
monitor.recovered (event.data["monitor"], event.data["check"]) and ping.
Options
Client(
api_key="xv_…", # default: XMLVALID_API_KEY
base_url="https://xmlvalid.com/api/v1",
timeout=900, # seconds validate() waits in all
wait=10, # seconds the API holds a new request (0-60)
max_retries=3, # retries of a 429 rate_limited
http_client=httpx.Client(...), # proxies, certificates, a mock transport in tests
)
Use it as a context manager, or call close(), to close the connection pool.
Development
uv sync
uv run pytest
uv run mypy
uv run ruff check && uv run ruff format --check
License
MIT
Metadata
Release files for xmlvalid 1.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| xmlvalid-1.2.0.tar.gz | 24.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| xmlvalid-1.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 43.7 kB
Release files / xmlvalid-1.2.0.tar.gz
| Download URL | xmlvalid-1.2.0.tar.gz |
|---|---|
| Size | 24.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8374671b27989faf59e0e14257cf1d3783073305b0e7139fb5fba58a266ffd6e
|
|
BLAKE2b-256 checksum How to use checksums |
f55009ed09644c816af562df93c228c181eee212f37f6f7d24b4f01057f6d68b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / xmlvalid-1.2.0-py3-none-any.whl
| Download URL | xmlvalid-1.2.0-py3-none-any.whl |
|---|---|
| Size | 19.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
800b746037a584d05b2087532cffeca83f706757e8091f7f093d62109180010b
|
|
BLAKE2b-256 checksum How to use checksums |
0f7df35ffa43ea81f52a41f2e47d9fc922f27074c7cad055de014a993b1a7bf6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|