Two VAT category codes have been listed as usable for months and cannot be
delivered. L (IGIC, Canary Islands) and M (IPSI, Ceuta & Melilla) are valid
under EN 16931 — read the standard and you will reasonably conclude they work —
but our provider has no vocabulary for them, so a document carrying one could
never reach its recipient.
The gateway now refuses them with a 422 unsupported_vat_category. This release
makes the SDK and the CLI say so before the send, so you find out at
validate time rather than at the rejection.
The sendable list
import { SENDABLE_VAT_CATEGORIES } from "@getpeppr/sdk";
// ["S", "Z", "E", "AE", "K", "G", "O"]
Alongside it, VALID_VAT_CATEGORIES holds the ten codes EN 16931 allows. The two
lists are deliberately separate, because "is this a VAT category?" and "can we
deliver it?" are different questions — and answering the second with the first is
what produced the wrong error message until now.
getVatCategories() entries carry a matching sendable: boolean, so you can
filter the catalogue for a dropdown without hard-coding the list.
One code moves the other way
B (Italian split payment) is accepted by the Peppol network, and our validator
called it invalid. That is the more expensive direction of error: telling a
developer the network refuses a code it accepts closes a corridor that is open.
B is now recognised as a real category, and separately reported as one we
cannot route.
Two findings instead of one
validateSchematron used to report a single verdict for anything it did not
like. It now distinguishes:
BR-CL-17— the value is not a VAT category code at all.unsupported_vat_category— a valid EN 16931 code getpeppr cannot route.
⚠️ The rule id PEPPOL-EN16931-R006 is gone. If you filter violations on
that string, update the filter. It is not a deprecation with a grace period: the
id never named a real rule — that Peppol family stops at R005, R007 and R008 in
rulebook v3.0.20, and R006 does not exist. We were citing a rule you could not
look up.
Two smaller fixes worth knowing
A vatCategory of null now counts as absent, exactly like undefined. The
gateway has always accepted it; the validator reported "null" is not a VAT category code for payloads that send fine — the shape any JSON serialiser emits
for an unset field.
And vatCategory is case-sensitive, which is now documented on the type.
"AE" is reverse charge; "ae" is not a category. This one is load-bearing:
"ae" used to be coerced to standard rate, shipping a valid-looking invoice that
never shifted the VAT liability to the buyer.
The CLI catches up
@getpeppr/cli 0.6.0 rebundles this SDK. That matters more than a version
number: getpeppr validate runs the validator locally from the bundled SDK,
so 0.5.4 was still passing L and M in silence, still calling B invalid, and
still citing PEPPOL-EN16931-R006.
It is a minor rather than a patch because documents that used to pass now fail —
if you gate a pipeline on getpeppr validate's exit code, its verdict changes.
That is the intended outcome: those documents were being refused at send time
anyway.
Upgrading
npm install @getpeppr/sdk@3.2.0
npm install -g @getpeppr/cli@0.6.0
L and M remain in the vatCategory type so existing code still compiles, with
the warning carried in the doc comment. They will be removed in the next major
release.
Update, 2026-08-17. That last sentence did not hold. SDK 4.0.0 is the next
major release and it shipped with L and M still in the type. Nothing about
their behaviour changed — they are still unroutable and still refused with a
422 unsupported_vat_category — but the removal was not bundled into an
unrelated breaking change. Treat it as pending, not done. See the
4.0.0 notes.