Skip to content

Understanding EN 16931 Validation Errors

EN 16931 validation errors look cryptic until you know what the rule IDs, business terms and VAT rule families actually mean. Here's how to read them and work out what your invoice is doing wrong.

Richard Adnams12 min read

Illustration of a UBL invoice being checked against EN 16931 validation rules, with XML code, rule results, and error indicators.
On this page

If you've run a UBL invoice through an EN 16931 validator you've probably seen errors that look something like this:

BR-CO-10
BR-S-08
BR-CL-04
BR-DEC-13

Which isn't exactly the friendliest introduction to electronic invoicing.

The rule IDs do start to make sense though, and once you know roughly what each family is checking you can normally get from the error to the broken bit of your invoice without randomly poking values until the validator goes green.

That's what we're going to look at here.

This isn't another explanation of the whole Peppol validation stack, I've already covered that in How Peppol Invoice Validation Actually Works.

This time we're staying inside EN 16931 and looking at the errors themselves.

First, don't ignore the rule ID

A validator might give you something like:

BR-CO-10

Sum of Invoice line net amount (BT-106) =
Σ Invoice line net amount (BT-131).

That sentence is useful, but the bit I care about first is:

BR-CO-10

Treat the rule ID as the stable bit.

Human-readable messages can be displayed differently by different validators, translated, reformatted or wrapped in some lovely generic API error object.

The identifier tells you exactly which rule failed.

That's useful for debugging, but it's also useful if you're building software around validation because you can link it to documentation, count how often it happens, write automated tests against it or give your users a better explanation than whatever came directly out of Schematron.

It's basically the error code you actually want to keep.

What does BR mean?

Most of the EN 16931 rules you'll run into start with:

BR-

That's a Business Rule.

The basic numbered rules cover requirements such as mandatory invoice information.

For example:

BR-02 requires an invoice number.

BR-03 requires an invoice issue date.

BR-05 requires an invoice currency code.

BR-06 requires the seller name.

BR-07 requires the buyer name.

They're the sort of rules where the failure is usually pretty literal.

If you get:

BR-02

there's a good chance your debugging session isn't going to be particularly exciting.

You forgot the invoice number.

The more interesting errors tend to come from the specialised rule families.

BR-CO-* is where the numbers start fighting back

You'll see BR-CO-* a lot when invoice totals don't reconcile or related values disagree with each other.

A good example is the one from the first article:

BR-CO-10

This checks that the invoice-level sum of line net amounts matches the actual sum of all the individual invoice line net amounts.

Imagine your invoice contains two lines:

Line 1: 100.00
Line 2: 50.00

but declares:

<cbc:LineExtensionAmount currencyID="GBP">140.00</cbc:LineExtensionAmount>

The lines add up to 150.00, so declaring 140.00 isn't going to get very far.

There are similar rules for other totals.

BR-CO-13 checks the invoice total amount without VAT.

BR-CO-14 checks the total VAT amount against the VAT breakdown.

BR-CO-15 checks that the total including VAT is the total without VAT plus VAT.

BR-CO-16 checks the amount due after things like paid amounts and rounding are taken into account.

These are useful failures because they're usually telling you that two parts of your own invoice disagree with each other.

I wouldn't fix that by editing the final XML.

If your application generated the wrong total, find the calculation that produced it.

Otherwise you've fixed one invoice and left the bug exactly where it was.

BR-CL-* usually means you've used a value from the wrong list

EN 16931 doesn't let you make up arbitrary values for everything.

Currencies, countries, units, payment methods and various identifier schemes use defined code lists.

That's where rules such as:

BR-CL-04

come in.

BR-CL-04 checks the invoice currency against ISO 4217 alpha-3 currency codes.

So this is fine:

<cbc:DocumentCurrencyCode>GBP</cbc:DocumentCurrencyCode>

This is not:

<cbc:DocumentCurrencyCode>POUNDS</cbc:DocumentCurrencyCode>

We know what POUNDS means.

The validator doesn't care.

Other BR-CL-* rules cover things like country codes, payment means, VAT categories, unit codes and identifier schemes.

If you hit one of these, before tearing apart the invoice calculations, check the actual value you're supplying and the code list the rule expects.

Quite often the problem is boring.

Boring is good.

BR-DEC-* means calm down with the decimal places

There is a whole family of rules dealing with decimal precision.

For example:

BR-DEC-13

limits the invoice total VAT amount to two decimal places.

So:

<cbc:TaxAmount currencyID="GBP">20.00</cbc:TaxAmount>

is fine.

Something like:

<cbc:TaxAmount currencyID="GBP">20.00372</cbc:TaxAmount>

isn't.

There are decimal rules for document-level allowances and charges, totals, VAT amounts, paid amounts, invoice line amounts and several other monetary values.

This can catch you if your internal accounting model uses higher precision than the final invoice format expects.

And that's completely reasonable internally.

You might calculate with four, six or more decimal places to avoid accumulating rounding errors.

The important bit is making sure you apply the required rounding when you generate the final invoice values.

Don't just globally round every number the moment it enters your application either.

That's how you create a different class of financial bug.

BR-S-* is about standard-rated VAT

This family starts showing up when you're using the standard-rated VAT category.

For example:

BR-S-05

requires the VAT rate on a standard-rated invoice line to be greater than zero.

That makes sense.

If you've said:

This line is standard rated

and then supplied:

VAT rate: 0

the invoice contradicts itself.

A slightly more interesting one is:

BR-S-08

which checks the taxable amount in the VAT breakdown for each standard VAT rate.

This one can be a pain because the error might appear in your VAT breakdown even though the actual mistake happened earlier in an invoice line, allowance or charge.

Say you have:

Line 1: 100.00 @ 20%
Line 2: 50.00  @ 20%

Your taxable amount for that VAT category should reflect those lines, adjusted for any relevant document-level allowances and charges.

If the VAT breakdown says:

Taxable amount: 140.00

then BR-S-08 is going to complain.

The useful question isn't:

What's wrong with my TaxSubtotal XML?

It's:

Which values contribute to this taxable amount and which one is wrong?

That's a much better place to start.

BR-Z-* is zero-rated VAT, which isn't the same thing as just putting zero everywhere

BR-Z-* deals with zero-rated VAT.

If a line is categorised as zero rated, the VAT rate is expected to be zero.

But there are also rules around the VAT breakdown, seller identifiers and consistency between lines, allowances, charges and the zero-rated VAT category.

For example:

BR-Z-05

requires a zero-rated invoice line to have a VAT rate of zero.

That's fairly obvious.

But:

BR-Z-08

checks the taxable amount in the zero-rated VAT breakdown against the invoice lines, document-level allowances and charges belonging to that category.

So again, the error may be reported against the VAT breakdown while the bad value actually came from somewhere else.

This is a recurring theme with EN 16931.

The element that fails the rule is not always the element that originally caused the problem.

BR-E-* is VAT exempt

VAT exempt and zero rated aren't interchangeable.

The invoice might end up with no VAT being charged in both cases, but they mean different things and EN 16931 represents them differently.

If you're using the exempt VAT category, the BR-E-* rules start applying.

One important example is:

BR-E-10

An exempt VAT breakdown needs a VAT exemption reason or exemption reason code.

Simply saying:

VAT rate = 0

isn't enough to explain why the VAT is zero.

That's useful semantically because the invoice is meant to communicate what happened, not just produce a mathematically correct final number.

BR-AE-* is reverse charge

Reverse charge gets its own set of rules as well.

For example, the rules check things such as:

  • whether the correct reverse-charge VAT category is being used
  • whether seller and buyer tax identifiers are present where required
  • whether the VAT rate is zero
  • whether the taxable amount reconciles correctly
  • whether an appropriate exemption/reverse-charge reason is present

So if you get something like:

BR-AE-05

don't treat AE as random noise in the identifier.

It's already telling you what sort of part of the invoice you're dealing with.

Once you've spent enough time with these, the prefix becomes a useful first filter before you've even read the full error message.

Fatal and warning don't mean the same thing

Validators can also attach a severity to findings.

You'll commonly see things such as:

fatal
warning

A fatal EN 16931 rule failure means the invoice doesn't comply with that ruleset.

That isn't something I'd quietly log and send anyway.

Warnings are different.

There are rules where something is discouraged or constrained without necessarily making the invoice invalid.

For example, some UBL conformance rules are warnings around elements that shouldn't normally be included.

Your application should preserve the severity rather than flattening everything into:

{
  "errors": [...]
}

because callers may want to handle them differently.

In Ironfang Finance we keep that distinction in the validation result.

A fatal rule failure is not the same thing as a warning and I'd rather not make the integration guess.

The business term is often more useful than the XML path

You'll notice error messages containing things like:

BT-106
BT-109
BT-116
BT-131

These are business terms from the EN 16931 semantic model.

This is one of the bits that feels unnecessarily cryptic when you first meet it, but it's actually pretty useful.

Take:

BT-131

That's the Invoice line net amount.

The semantic rule talks about the business concept.

UBL has a particular XML representation for it.

Your application probably has its own representation again.

So a validation failure might ultimately involve:

<cbc:LineExtensionAmount currencyID="GBP">100.00</cbc:LineExtensionAmount>

but the actual bug might be in something called:

line.NetAmount

inside your application.

Don't stop debugging just because you've found the UBL element.

Work out what created it.

One error can cause several more

This is probably the most annoying bit when you're looking at a large validation result.

You can fix one value and suddenly five other errors disappear.

That's because business rules aren't isolated from each other.

Imagine an invoice where the line total is wrong.

That value might then affect:

  • the invoice line total
  • the amount before VAT
  • the taxable amount in the VAT breakdown
  • the VAT amount
  • the final invoice total
  • the amount due

You might get several rule failures from what is basically one bad number upstream.

So if your validator returns 14 findings, don't immediately assume you've made 14 mistakes.

Look for the earliest or most fundamental inconsistency.

Calculation and VAT errors in particular tend to travel in packs.

Very considerate of them.

Read the actual values before rewriting the XML

Let's say you get:

BR-CO-15

The rule is telling you that the invoice total including VAT doesn't equal the invoice total without VAT plus the VAT amount.

Don't immediately start moving XML elements around.

Write down the values.

If you've got:

Amount without VAT: 100.00
VAT amount:          20.00
Amount with VAT:    119.00

we don't need an XML expert yet.

We need whoever taught the invoice that 100 + 20 = 119.

Likewise, if a VAT category rule fails, inspect:

  • the category code
  • the rate
  • the taxable amount
  • the tax amount
  • the lines using that category
  • relevant allowances and charges

The XML path matters, but the numbers normally tell the story faster.

Don't assume the validator is complaining about where the bug started

This catches people because validation errors often include a context or location.

That tells you where the rule evaluated or where the failure was reported.

It doesn't necessarily mean:

Change this exact XML element and everything will be fixed.

A VAT breakdown can fail because one of your invoice lines has the wrong category.

A document total can fail because an allowance wasn't included correctly.

A calculated amount can fail because your application rounded something too early.

The validator can tell you which rule has become false.

It can't know which line of your application code made it false.

Unfortunately that bit is still our job.

Peppol rules and EN 16931 rules aren't the same thing

This is worth mentioning because they're often returned together.

An identifier such as:

BR-CO-10

belongs to the EN 16931 ruleset.

Something like:

PEPPOL-EN16931-R020

is a Peppol rule layered on top.

So if your result contains both, keep the source or validation layer around.

It makes debugging much easier and it also means your UI can say something more useful than:

12 validation errors

For example:

EN 16931: 3 errors
Peppol BIS: 2 errors
Warnings: 1

Now you've got some idea what sort of mess you're dealing with.

My usual order for fixing EN 16931 errors

I don't normally work through the result from top to bottom.

I start with the errors that are most likely to make other errors disappear.

First I'd deal with missing mandatory information.

If the invoice is missing a currency, VAT category or some other fundamental business term, fix that first.

Then I'd deal with obviously invalid code-list values.

If you've used an invalid currency or country code, there's no point investigating the maths around it yet.

After that I'd look at line amounts and document totals.

Then VAT category and VAT breakdown errors.

Then decimal and rounding problems.

Finally I'd clean up anything more specific that's left.

That isn't a formal part of EN 16931, it's just a much less irritating way to debug an invoice.

Keep the original rule ID in your own API

If you're building an invoicing product, don't throw the original rule identifier away.

You might be tempted to turn:

BR-CO-10

into:

INVOICE_TOTAL_INVALID

and only return your own error code.

I'd keep both.

Your application-specific message can absolutely be better:

{
  "rule": "BR-CO-10",
  "code": "invoice_line_total_mismatch",
  "message": "The declared line total does not match the sum of the invoice lines."
}

That's useful.

But preserving BR-CO-10 means the developer can still search for the actual standard rule, compare behaviour with another validator and trace it back to the upstream specification.

Standards errors are one of the few places where exposing the ugly original identifier is genuinely helpful.

Test broken invoices on purpose

Once you've fixed one of these errors in your generator, keep a broken fixture for it.

If you had a bug where your VAT breakdown produced BR-S-08, create a test invoice that reproduces it.

Then assert that:

  1. the broken fixture fails with BR-S-08
  2. the corrected fixture passes it

Do the same for calculation rules, code-list mistakes and rounding problems.

Over time you end up with a pretty useful regression suite built from actual failures you've seen rather than hypothetical ones.

That's far better than discovering the same accounting bug again six months later because somebody refactored the invoice calculator.

The error is usually less scary than the identifier

That's really the trick with EN 16931 validation.

Something like:

BR-S-08

looks obscure.

But once you translate it into:

The standard-rated VAT taxable amount doesn't equal the relevant lines, allowances and charges.

you've got an ordinary data problem.

BR-CL-04?

Your currency code is wrong.

BR-DEC-13?

Too many decimal places.

BR-CO-10?

Your lines don't add up.

The identifiers look bureaucratic because, well, they are.

The actual failures are normally much more mundane.

Try it out

We've built the EN 16931 and Peppol rule handling into Ironfang Finance, and there's also a free Peppol invoice validator if you've got a UBL invoice you want to throw at it.

The validator keeps the original rule identifiers and separates findings by validation layer, which makes it a lot easier to work out whether you've got an XML problem, an EN 16931 business rule failure or a Peppol-specific issue.

You don't need an account to use the free validator.

  • peppol
  • en-16931
  • ubl
  • validation
  • How Peppol Invoice Validation Actually Works

    Peppol validation is not one check. Your invoice goes through XML, UBL, EN 16931, Peppol and sometimes country-specific rules. Here is what each layer actually does.

    Peppol11 min read