Why is my XML valid but still rejected?

Well-formed, schema-valid, business rules, the receiver's profile: one invoice broken at each level, with the real messages and what each check can't see

· 7 min read · Oleksii Babintsev

"But it's valid XML" is the most common reply to a rejected file, and it is usually true. It just answers a different question from the one the receiver asked. "Valid" covers at least four separate checks, each stricter than the one before, and a file can pass one and fail the next.

To show where each one draws its line, I took a small UBL invoice that passes the XML Schema and all of the EN 16931 business rules, then broke it in different ways. Every message below is real output from libxml2 and the official rule sets, not a paraphrase.

Level 1: is it XML at all

The first check is whether the file is well-formed: tags closed and properly nested, one root element, attribute values in quotes, special characters escaped. No schema is involved. A browser that shows the file as a tree has only done this much.

Change the item name to Toner & drum and the parser stops:

line 53  xmlParseEntityRef: no name

An ampersand starts an entity reference in XML, so a bare one in text has to be written &. This is almost always a sign that the XML was built by joining strings rather than with an XML library, which would have escaped it. Other errors of this kind are a tag closed in the wrong order, text after the root element and a file cut off halfway.

A well-formedness error stops everything. The parser cannot tell where the damage ends, so there is no schema check and no business rule check until it is fixed. If a validator reports exactly one error and it is one of these, that does not mean the rest of the file is fine; nothing else has been looked at yet.

Level 2: does it follow its schema

A schema describes what the document may contain: which elements, in what order, how many times, and of what type. For an invoice that is an XSD, but DTD and RelaxNG do the same job for other documents. Here the parser checks the file against UBL 2.1's UBL-Invoice-2.1.xsd.

Write the issue date the way people write dates in Germany:

line 7  Element 'cbc:IssueDate': '30.10.2026' is not a valid value of the atomic type 'xs:date'.

xs:date is 2026-10-30 and nothing else, whatever the locale. Or keep every value right and swap two elements, putting the currency code before the invoice type code:

line 9  Element 'cbc:InvoiceTypeCode': This element is not expected. Expected is one of ( cbc:TaxCurrencyCode, cbc:PricingCurrencyCode, cbc:PaymentCurrencyCode, … ).

XSD sequences are ordered, so the same elements in another order are a different document. The message names the element it found and lists what it would have accepted at that point; the element you meant is somewhere further down the schema. Both messages have their own pages: not a valid value of the atomic type and this element is not expected.

This is where most people stop, and for a lot of XML it is the right place to stop: configuration files, data feeds, a DTD-based publishing format. The XSD, DTD and RelaxNG guides cover this level for each schema language.

What a schema cannot do is relate one value to another. It can say that TaxInclusiveAmount is a decimal amount. It cannot say that it must equal the amount without tax plus the tax.

Level 3: does it follow the business rules

That job belongs to business rules, usually written in Schematron: XPath assertions over the whole document, each with an id and a message. For invoices in Europe the base set is EN 16931, published by CEN: over 200 BR- rules on the invoice's content, plus syntax rules for UBL and CII.

Add 0.19 to the total with VAT (and to the amount due, so the two still agree) and the file is still schema-valid. The rules are not satisfied:

BR-CO-15  Invoice total amount with VAT (BT-112) = Invoice total amount without VAT (BT-109) + Invoice total VAT amount (BT-110).

100.00 + 19.00 is not 119.19. In real invoices this is usually a rounding difference: VAT rounded per line, then summed, against VAT computed once on the total.

Or remove the buyer's legal name. UBL makes cac:PartyLegalEntity optional, because UBL is a general format used for much more than invoices. EN 16931 decides what an invoice needs:

BR-07  An Invoice shall contain the Buyer name (BT-44).

The BT numbers are EN 16931's business terms, the same field whatever the syntax: BT-44 is cac:PartyLegalEntity/cbc:RegistrationName in UBL and ram:BuyerTradeParty/ram:Name in CII. Each rule has a page explaining what it checks and how to fix it, for example BR-CO-15 and BR-07.

One practical consequence: business rules only make sense on a document that matches its schema, so xmlvalid runs them after the schema check passes. Fix the last schema error and a new batch of findings can appear. That is not the validator moving the goalposts; those rules simply had not run yet.

Level 4: does it follow the receiver's rules

EN 16931 is the common core. Most of the time the invoice is sent under a national or network profile that adds its own rules on top: XRechnung in Germany, Peppol BIS across Europe and beyond, CIUS-RO in Romania, France's CTC rules.

I took the invoice from the start, which passes both levels above with no findings, changed only its CustomizationID to say it is an XRechnung 3.0, and ran KoSIT's XRechnung rules on it. Ten errors and one warning:

Rule What it wants
BR-DE-1 Payment instructions (BG-16)
BR-DE-2 A seller contact (BG-6)
BR-DE-3, BR-DE-4 The seller's city and post code
BR-DE-8, BR-DE-9 The buyer's city and post code
BR-DE-15 The buyer reference (BT-10), where German public buyers expect their Leitweg-ID
PEPPOL-EN16931-R001 A business process (BT-23)
PEPPOL-EN16931-R010, R020 Electronic addresses for buyer and seller
BR-DE-TMP-32 (warning) A delivery date or invoicing period

None of these are mistakes as far as EN 16931 is concerned; they are fields that Germany's public buyers require, which the European standard leaves optional. KoSIT's messages are in German ("Das Element "Buyer reference" (BT-10) muss übermittelt werden."), which is another small surprise for anyone sending an XRechnung from outside Germany.

The rule set is chosen by the CustomizationID (BT-24). A file that claims the wrong profile is checked against the wrong rules, and a validator set to plain EN 16931 will happily pass an invoice the receiver then rejects. When you open an e-invoice in the validator, it reads BT-24 and picks the matching profile from the schema catalog for that reason.

Level 5: what no file check can see

Some rejections have nothing to do with the file's structure, and no validator, ours included, can predict them:

  • Registries. Whether the Leitweg-ID, the Peppol participant id or the VAT number exists and belongs to the right party. A correctly formed id can still be unknown to the receiver.
  • State. Duplicate invoice numbers, a purchase order that has already been invoiced, a credit note for an invoice the receiver never saw.
  • The receiver's own policy. Purchase order numbers required by one company and not by another, attachments in a format their system cannot open, size limits of the platform in between.
  • Clearance systems. Italy's SdI, Spain's VERI*FACTU and Mexico's CFDI check some things against government records at submission time. A validator can only cover the checks that need nothing but the file.

If a file passes levels 1 to 4 and is still rejected, the rejection message itself, not a validator, is the place to look.

Reading a rejection

The wording of the message usually tells you which level failed:

The message mentions Level Where to look
parser error, xmlParseEntityRef, Opening and ending tag mismatch 1, well-formedness The line it names, or the code that wrote the XML
Element '…': This element is not expected, not a valid value of the atomic type, cvc-complex-type.2.4.a (Java) 2, schema The schema and the element order
A rule id such as BR-CO-15, BR-07, BR-S-08 3, EN 16931 The rule's page; usually amounts or a missing field
BR-DE-…, PEPPOL-EN16931-…, BR-FR-…, BR-RO-… 4, the profile The profile's documentation and the CustomizationID
A duplicate, an unknown id, something about the account or the order 5, the receiver The receiver

Java and .NET word the same schema problems differently from libxml2. The error guide lists their versions alongside, so cvc-complex-type.2.4.a and This element is not expected lead to the same explanation.

Try it

The broken invoices from this post are easy to recreate: open the EN 16931 example or the XRechnung one in the validator, change a total or a date, and run it. It checks the schema and the profile's business rules together, in the browser, so the invoice is not uploaded anywhere.

More from the blog