Skip to content
Back to News
Changelog

SDK 5.8.0 refuses identifiers Peppol cannot carry, and finds a customer by your own reference

sdkcliplatform

Two changes reach npm today, and one of them can refuse input that 5.7.0 accepted. If you call validatePeppolIdentifier anywhere, read the first section before upgrading.

validatePeppolIdentifier enforces POLICY 1's alphabet

The Peppol Policy for use of Identifiers 4.4.0, POLICY 1, requires that a participant identifier's value contain only letters a-z and A-Z, digits 0-9, the minus sign, the period, the underscore or the tilde. Up to 5.7.0 the SDK refused an empty value, leading or trailing whitespace and anything over 64 characters, then applied a format pattern for the schemes it knows — which are the minority. For every other scheme, nothing looked at which CHARACTERS the value used.

The alphabet is now checked for every scheme, after those three guards and before the per-scheme pattern, under both the addressing and the registration usage. Measured on the published 5.7.0 and on this release:

validatePeppolIdentifier("GB:CRN", "AB/CD");
// 5.7.0 → { valid: true, checksumChecked: false }
// 5.8.0 → { valid: false, error: "Identifier may only contain letters (a-z, A-Z),
//            digits (0-9), '-', '.', '_' or '~' (Peppol Policy for use of
//            Identifiers 4.4.0, POLICY 1)." }

The set is narrower than ASCII. A space, a slash and a colon are ASCII and are refused.

Why this is a fix and not a restriction. A value outside that set cannot be carried by the Peppol network, and accepting it was worse than merely permissive. Participant lookup lower-cases in full Unicode, so an identifier spelled with the Kelvin sign U+212A and its plain-ASCII twin resolve to the same network participant — while a system that accepted both would hold them apart as two customers. validatePeppolIdentifier("0096", "A\u212A9") returned { valid: true, checksumChecked: false } in 5.7.0 and now returns the same POLICY 1 refusal as above.

One thing to check if you display the error text. When a value breaks both the alphabet and its scheme's format, the alphabet is reported first, so the message changes even though the verdict does not. A number pasted with the spaces its owner writes it with is the common case:

validatePeppolIdentifier("GB:VAT", "GB 123 4567 89");
// 5.7.0 → { valid: false, error: "Format mismatch: expected 9 or 12 digits,
//            optionally prefixed GB or XI (or GD/HA + 3 digits)." }
// 5.8.0 → { valid: false, error: "Identifier may only contain letters (a-z, A-Z),
//            digits (0-9), '-', '.', '_' or '~' (Peppol Policy for use of
//            Identifiers 4.4.0, POLICY 1)." }

The same swap happens on 0002 with "732 829 320", on 0184 with "1234 5678" and on 9944 with "NL 123456789B01". Every one of them was already refused, in both versions; only the text moved.

Two cases do not change, so there is no need to over-correct. A leading or trailing space still reports "Identifier must not have leading or trailing whitespace.", because that guard runs first. And a separator POLICY 1 allows still reaches the pattern: ("GB:VAT", "GB-123-4567-89") returns the format hint in both versions.

If your form showed the format hint to help someone fix a pasted value, strip the formatting characters rather than trimming — value.replace(/\s/g, "") removes the inner spaces that trim() leaves in place.

Nothing in production was affected. We checked 287 live identifiers across our own records before shipping this: zero POLICY 1 violations.

Find a customer by your own reference

legalEntities.list() takes an externalId filter, so you never need to have stored our id to find a customer again:

const { data } = await peppol.legalEntities.list({ externalId: "acme-42" });
const customer = data[0]; // undefined if you never registered that one

Matched exactly — no prefix search, no case folding, no trimming. Because the reference is unique among your live customers, the page carries at most one entity. An unknown or archived reference gives an empty data, not an error; an empty string, or a reference over 64 characters, is refused with 400 rather than answered with an empty list.

listAll() accepts it too — it takes every list() option except offset, so a filter you build once can be handed to either.

This is the SDK surface for GET /v1/legal-entities?externalId=…, already described on the legal entities documentation page.

CLI 0.12.3

The CLI bundles the SDK into its published artifact, so an SDK release reaches CLI users only once the CLI is rebundled. 0.12.2 still carried SDK 5.4.2; 0.12.3 carries 5.8.0.

No command, validation rule or exit code changes. validate, convert and init do not call validatePeppolIdentifier — the alphabet gate is not in the CLI's bundle at all — so they accept and refuse exactly what 0.12.2 did. What changes is the SDK version the CLI reports in its User-Agent.

npm install @getpeppr/sdk@5.8.0
npm install -g @getpeppr/cli@0.12.3