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:

  • An idempotency key that cannot protect anything now throws instead of travelling. On 4.8.1, idempotencyKey: " " was sent — HTTP strips the whitespace, so the gateway received nothing and treated each retry as a fresh request. On 5.0.0 it raises a PeppolValidationError before anything leaves. If you passed a blank key, you were already unprotected; this is where you find out.
  • A negative allowance or charge amount is refused. 4.8.1 accepted it and rendered a document whose totals disagreed with its lines. An allowance reduces the invoice and a charge increases it — the direction is the field's job, never the sign's.

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 0002–0248. 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

  • A blank idempotency key is refused instead of sent. " " is truthy in JavaScript, so it used to unlock the POST retry, while HTTP strips whitespace from a header value — so the gateway received an empty key, skipped its cache, and treated each attempt as a fresh request. One POST /v1/invoices could leave four times under the default retry config. An unusable key now throws before anything leaves.
  • Every operation the contract keys now accepts an idempotencyKey. The contract declares Idempotency-Key on six operations; this package took one on invoices.send / invoices.create alone.
  • 0007, the Swedish organisation number, is a first-class scheme.
  • LegalEntity.registrationDetail exposes the stable reason a registration failed — already_registered, invalid_format or provider_error.

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.