Skip to content
Back to News
Changelog

SDK 5.0.0 and CLI 0.10.0 — two currency codes the network stopped accepting

sdkclipeppol

Peppol BIS Billing 3.0.21 became mandatory on the network on 17 August 2026. Bulgaria and Croatia have joined the euro, and BR-CL-04/BR-CL-05 — the rules that decide which currency codes a document may carry — dropped BGN and HRK with it.

Until today this SDK still accepted both. It validated your invoice, told you it was fine, and handed it to a network that refuses it. That is the headline breaking change.

Two narrower ones travel with it, both measured against the published 4.8.1:

This is also the first publish since 4.8.1 on 23 August, so several fixes that had been merged and were sitting unreleased travel with it.

BGN and HRK are no longer currencies

getCurrency("BGN") and getCurrency("HRK") return undefined, and validateInvoice rejects both. Use EUR.

In the other direction, XCG — the Caribbean guilder that replaced the Netherlands Antillean guilder — is now accepted. ANG never existed in this SDK, so nothing was swapped out for it.

The ISO 6523 ICD list gains 0246, 0247 and 0248, bringing it to 243 codes running 00020248. That change is purely additive — no scheme was removed — and it is the list BR-CL-10 judges a PartyIdentification against, so a seller registered under one of the three is no longer dropped from it.

Structured API results, and retryability the response decides

PeppolApiError now carries resultCode, resultMessage, requestId, retryable, remediation and docs.

Each one is independent. They are read from separate response headers and kept separately, so any of them can be present while the others are not — a stripping proxy, a gateway that has not enabled its result catalogue, or a code with no guide behind it all produce partial results. Read every field as optional; the only guarantee is that error.result itself is undefined when none of them arrived.

isRetryableError now reads the Getpeppr-Retryable header before falling back to the status code, and the header decides in both directions: nine public 5xx result codes are definitively fatal, while three 409s are marked as repairing themselves. If the header is absent or unreadable, the historical status list applies unchanged — an ambiguous header must not grant a retry the status refused, nor withdraw one it allowed.

⚠️ Retryable is not the same as replayable, and the SDK asks both. "Can retrying succeed?" is what the header answers. "Is replaying THIS request safe?" is a separate guard, and it is unchanged: a 429 is always replayable (the request was never processed), so are GET, DELETE and HEAD — every other method needs a usable Idempotency-Key. So a 409 marked retryable on a POST is still not replayed unless you supplied one.

Also in this release

An old promise we are still not keeping, and why

The 3.2.0 notes said the VAT categories L (IGIC, Canary Islands) and M (IPSI, Ceuta & Melilla) would leave the vatCategory type "in the next major release". 4.0.0 shipped without doing it and said so. This is the third major, and they are still there.

We tried, during this release, and reverted it. Here is the reason, because "still pending" three times running deserves better than a repeat.

One type feeds two different questions. invoices.send() genuinely cannot transmit L or M — our provider has no vocabulary for either, and the gateway refuses them with 422 unsupported_vat_category. But buildInvoiceXml, buildCreditNoteXml and Peppol.toXml() take that same input type, and they deliberately apply no routability gate: they render <cbc:ID>L</cbc:ID> on purpose. An IGIC invoice is a perfectly valid EN 16931 document. Measured against the official rulebook on the day of this release: an IGIC document and an otherwise identical standard-rate one came back from POST /v1/validate/ubl with the same single violation, and it had nothing to do with VAT — the rulebook raised no objection to the category itself.

So the honest statement is narrower than the one we were about to publish: these categories are not routable by our JSON send path, not "undeliverable". Removing them from the type would have taken away local UBL generation for valid documents in order to forbid sending them.

The promise gets kept by splitting the type — one for sending, one for the builders. That is tracked, and it is the version that ships it which will close this out.

CLI 0.10.0

@getpeppr/cli bundles the SDK, so getpeppr validate and getpeppr convert run these rules on your machine. 0.10.0 rebundles 5.0.0.

One verdict changes: BGN and HRK. A document carrying either used to exit 0 and now exits 1, which matters if you gate a pipeline on it. L and M are unaffected — measured on the published 0.9.0, getpeppr validate already exits 1 on them, and getpeppr convert without --validate still renders them, as it should.

It also picks up a fix the CLI had missed: 0.9.0 shipped with SDK 4.8.0 bundled, one release behind, so it never carried the baseQuantity correction from 4.8.1. A 24-unit order priced "€30 per box of 12" was rendering €720 instead of €60 — a fatal PEPPOL-EN16931-R120 violation. getpeppr convert gets that right now.

Upgrading

npm install @getpeppr/sdk@5.0.0
npm install -g @getpeppr/cli@0.10.0

If you pass neither BGN nor HRK, there is nothing to change. The migration notes ship inside the package, in README.md under "Upgrading to 5.0.0" and in CHANGELOG.md.