Repository navigation
Feature Catalogue
Every planned command, by area. All entries are proposed — nothing here is agreed or implemented yet.
Status values: proposed · accepted · deferred · rejected.
Library references were checked against the sources of peppol-commons 13.1.x, peppol-directory-client 1.0.0,
phorm-client 1.0.0-SNAPSHOT, ph-nss 1.0.0, ddd-model, ph-diver and ph-commons, and against the
specifications listed in Specifications. Where a capability does not exist in the libraries,
the Notes column says so explicitly rather than implying reuse.
- Identifiers
- DNS / Discovery
- SML
- SMP — read
- phoss-SMP — write
- Directory
- Certificates / PKI
- Endpoint diagnostics
- Code lists
- Validation
- MLS
- Bulk
- Rejected features
| Command sketch | Description | Status | Notes / Libraries |
|---|---|---|---|
peppol id validate <id> |
Validate a participant identifier: scheme syntax, value syntax, length limits, allowed character set. | proposed |
peppol-id: PeppolIdentifierFactory (isParticipantIdentifierSchemeValid, isParticipantIdentifierValueValid); rule constants in PeppolIdentifierHelper. Strict and lax factories both exist — expose the choice as a flag. |
peppol id normalise <id> |
Print the canonical form, applying case folding where the scheme is case-insensitive. | proposed |
IIdentifierFactoryBase.getUnifiedValue; the factory only folds case for schemes reported as case-insensitive, so the command should say which rule applied. |
peppol id check-value <id> |
Scheme-specific semantic value validation, including check digits. | proposed |
peppol-id-checks: IdentifierValidator.isValidParticipantIdentifier over the IParticipantIdentifierValidatorSPI. Two validators are registered today — Norwegian organisation number (mod-11) and Belgian (mod-97). For GLN (ICD 0088) the check digit algorithm already exists in ph-masterdata as GTINValidator.isValidGLN (GLN = GTIN-13, GS1 mod-10), and peppol-id-checks already depends on ph-masterdata — it is simply not wired into the SPI. See Library Gaps #1. |
| Check-digit failures warn rather than reject | A failed check digit is reported as a warning at exit 0, not as a validation failure. | proposed |
Evidence for this default, found while verifying: the widely-circulated Peppol test identifier 0088:7315458756324 is not a checksum-valid GLN — the GS1 algorithm yields check digit 8, the identifier carries 4. (By contrast 0088:4035600210708 and 0088:5790000435975 do validate.) Test and demo identifiers under ICD 0088 are therefore not reliably real GLNs, so rejecting on check digit would break legitimate test workflows and make the tool unusable on the test network. Proposal: warn by default, and offer --strict-check-digits to turn warnings into exit 4 for those who want it. |
peppol id scheme <icd> |
Look up an ISO 6523 ICD: scheme ID, name, and whether it is deprecated. | proposed |
peppol-id: PeppolParticipantIdentifierSchemeManager (getSchemeOfISO6523Code, isSchemeWithISO6523CodeDeprecated), backed by the generated EPredefinedParticipantIdentifierScheme (code list version 9.7). Deprecation flagging comes for free. |
peppol id country <id> |
Derive the participant's country from the ICD and value where that is possible. | proposed |
peppol-id-checks: PeppolParticipantCountryHelper (handles GLN, GS1, EU VAT incl. the EL/XI prefixes, IBAN). Needs ph-masterdata. Must be explicit that this is a derivation, not authoritative registry data. |
peppol id inspect <doctype-id> |
Decompose a document type identifier into root namespace, local name, customization ID and version. | proposed |
peppol-id: PeppolDocumentTypeIdentifierParts / PeppolGenericDocumentTypeIdentifierParts (extractFromString). Two schemes exist — busdox-docid-qns and peppol-doctype-wildcard — and the wildcard form has its own separator (@) and indicator (*); the output must make clear which was parsed. |
peppol id inspect <process-id> |
Validate and display a process identifier. | proposed |
PeppolIdentifierFactory.isProcessIdentifierValueValid; predefined values via PredefinedProcessIdentifierManager. |
peppol id seat <value> |
Validate a Seat ID and show its parts. | proposed | Per SPIS 1.0.0: P + the 2-letter Peppol Authority abbreviation + the 6-digit number (PeppolIdentifierHelper.REGEX_SEAT_ID). SPIDHelper.getMainIDFromSeatID extracts the 6-digit Main ID. The output must name the PA abbreviation as such, never as a country — POP000270 is OpenPeppol, not a country. > TODO(verify): is there a published list of Peppol Authority abbreviations the CLI could resolve to names? SPIS 1.0.0 gives only the "OP" example.
|
| Seat ID is not a stable key | Worth saying out loud in the output. | proposed | SPIS 1.0.0 is explicit: if a Service Provider changes the Peppol Authority it holds its agreement with, the Seat ID changes — POP000270 becomes PBE000270 — while the 6-digit Main ID stays the same and is unique across all Service Providers. So anything that needs to track a provider over time should key off the Main ID, and peppol cert seat-id should surface both, with the Main ID labelled as the stable one. A tool that encourages people to key off the Seat ID is setting them up for a silent break at the next PA change. |
peppol id spid <value> |
Validate and decompose a Service Provider ID. | proposed |
SPIDHelper (@since 12.6.1): isValidSPID, plus getMainID, getUseCaseID and getServiceProviderSuffix. Verified conformant to SPIS 1.0.0: SPIDHelper.REGEX_COMPLETE is character-for-character identical to the specification's canonical regex [0-9]{6}(-[0-9A-Z_]{3,12}(\.[0-9A-Z\-\._~]{3,24})?)?, MAX_LEN_SPID matches the stated maximum of 44, and all ten worked examples from the spec validate as expected. Note two rules the structure enforces: a Service Provider Suffix is only valid together with a Use Case ID, and the suffix may itself contain dots (000270-Rprtng_MLS.Japan.123), so it is everything after the first dot. |
| SPIS scheme awareness | Recognise service provider participant identifiers. | proposed | The SPIS scheme is ICD 0242 ("OpenPeppol Service Provider Identification Scheme", OpenPeppol AISBL, ACTIVE since code list 9.3); SPIDHelper.SPIS_PARTICIPANT_ID_SCHEME exposes it. So peppol id validate 0242:... should route the value through SPID validation rather than generic syntax checks — a nice example of the scheme-specific validation in id check-value. |
| Command sketch | Description | Status | Notes / Libraries |
|---|---|---|---|
peppol dns hash <participant-id> |
Print the hashed DNS label for a participant. | proposed |
Now confirmed against SML 1.3.0 §3.1.1, which matches the peppol-commons implementation exactly: SHA-256 over the lowercased participant ID, Base32-encoded, with trailing = removed (RFC 6234; POLICY 7 of PFUOI 4.4.0). Not MD5 — documentation saying so refers to a provider removed from the library. Library: AbstractBDXLURLProvider.getHashValueStringRepresentation. The spec supplies a test vector, reproduced and confirmed: 0010:5798000000001 → XUKHFQABQZIKI3YKVR2FHR4SNFA3PF5VPQ6K4TONV3LMVSY5ARVQ. |
peppol dns name <participant-id> |
Print the complete DNS name, including scheme and SML zone. | proposed | Spec format: <hash over recipientID>.<schemeID>.<SML domain>. getDNSNameOfParticipant; zone from ESML via the selected network. |
peppol dns resolve <participant-id> |
Walk the whole chain step by step — lowercase, hash, DNS name, NAPTR/U-NAPTR lookup, resolved SMP URL — showing each step's input, output and timing, and stopping at the first failure with a clear statement of which step failed. | proposed |
getSMPURIOfParticipant; U-NAPTR resolution via ph-dns (NaptrLookup, NaptrResolver) and dnsjava. The provider exposes setUseNaptrDebug, customDNSServers() and setUseDNSCache, which map naturally onto --verbose, --dns-server and --no-cache. The lookup is bounded by --dns-timeout (default 5s), not by the HTTP timeouts; since the resolved SMP is then queried over HTTP, one peppol smp command can involve all three timeouts, so timing each step separately is what makes a slow lookup attributable. This step-by-step output is the single most valuable diagnostic in the tool. |
peppol dns resolve --check-uri-rules |
Validate the resolved SMP URL against the rules SML 1.3.0 §2.1.1 places on U-NAPTR URI values, and report each violation. | proposed | Verified, and pleasingly checkable: the URI MUST use only the https scheme, MUST NOT carry username/password in the authority, and MUST NOT include query or fragment parts. Scheme and host are case-insensitive, everything else case-sensitive (RFC 3986). A misconfigured SMP registration that still "works" for some clients is exactly the kind of latent fault a diagnostic tool should surface. Combine with the SMP spec's port-443 requirement for a complete conformance check of the registration. |
| NAPTR service name filtering | Select the right record among several. | proposed | Spec: the service name MUST be Meta:SMP and no other name may be used for this purpose, but a participant's NAPTR query may legitimately return additional records with other service names for other purposes. So the CLI filters by service name and, under --verbose, should show the records it discarded — a participant with unexpected extra NAPTR records is worth seeing. The field is case-insensitive per RFC 4848. peppol-commons has DNS_UNAPTR_SERVICE_NAME_META_SMP = "Meta:SMP". |
peppol dns resolve --dnssec |
Report whether the resolution chain was DNSSEC-validated, and fail if requested and not available. | proposed | SML 1.3.0 §5 "DNS Spoof Mitigation" recommends that an SML instance use DNSSEC, precisely because a rogue certificate plus DNS poisoning could redirect messages to a malicious access point. Reporting the DNSSEC status of a lookup is therefore a meaningful security diagnostic. > TODO(verify): whether ph-dns/dnsjava as used here can report DNSSEC validation status, and whether the production and test SML zones are in fact signed.
|
peppol dns wait <participant-id> |
Poll until the record resolves, for use after an SML write. | proposed | Plain polling around dns resolve; see the waiter convention in CLI Conventions. |
--network test|prod on all of the above |
Select the SML zone. | proposed |
EPeppolNetwork → ESML. No zone literals in the CLI's own code. |
Grounded in Peppol SML 1.3.0 (see Specifications). The spec settles the question that was previously open here, and the answer shapes this whole area:
There is no anonymous SML read interface. Both management interfaces require authentication, and §4.2.2/§4.3.2 mandate two-way TLS: the caller authenticates with a client certificate, and the server "must reject TLS clients that do not authenticate with a certificate issued under the Peppol root CA". The authenticated identity determines which SMP's data the caller may touch.
So there are two distinct classes of command: things any user can do against DNS, and things only an SMP operator
can do against their own SML records. Conflating them would be misleading, so the CLI should not. Looking up a
third party's registration through the SML is therefore not possible and is listed under
Rejected features; use peppol dns resolve instead.
Requires the configured SMP client certificate. Operations are SOAP 1.1 over HTTP (WSDLs ship with the spec), conforming to WS-I BP 1.1 and BSP 1.0.
| Command sketch | Description | Status | Notes / Libraries |
|---|---|---|---|
peppol sml smp read <smp-id> |
Read your own SMP record: the publisher endpoint (domain name or IP) and the SMP ID. | proposed |
ManageServiceMetadata::Read(). Input ServiceMetadataPublisherID, output ServiceMetadataPublisherService. peppol-sml-client: ManageServiceMetadataServiceCaller. |
peppol sml smp create / update
|
Create or update the SMP record, including its logical and physical address. | proposed |
ManageServiceMetadata::Create() / Update(). The ServiceMetadataPublisherID is assigned out of band, so it is configuration, not something the CLI can discover. |
peppol sml smp delete <smp-id> |
Delete the SMP record. | proposed |
ManageServiceMetadata::Delete(). Highest-impact SML operation — it detaches every participant of that SMP. Confirmation plus blast-radius output mandatory. |
peppol sml participant add <id> |
Register one participant against your SMP. | proposed |
ManageBusinessIdentifier::Create(). Input is ServiceMetadataPublisherServiceForParticipant — the participant identifier plus the SMP ID. Note the spec: exactly one SML record per participant regardless of how many services it exposes. |
peppol sml participant add --from <file.csv> |
Register many participants in one call. | proposed |
CreateList() exists — input is a ParticipantIdentifierPage with NextPageIdentifier absent. So batch registration is a first-class spec operation, not a client-side loop. |
peppol sml participant remove <id> |
Remove one participant registration. | proposed |
Delete(). |
peppol sml participant remove --from <file.csv> |
Remove many. | proposed |
DeleteList(). Note the fault semantics: notFoundFault is returned if one or more of the supplied identifiers is unknown, so partial-failure reporting needs care — the CLI cannot assume all-or-nothing. > TODO(verify): whether a DeleteList containing one unknown identifier rejects the whole batch or deletes the rest. The spec does not say, and it changes how the command must report.
|
peppol sml participant list |
List every participant registered against your SMP, following pages. | proposed |
List() exists, and is described as being for synchronisation. Input PageRequest (SMP ID plus optional NextPageIdentifier); output ParticipantIdentifierPage with the next page identifier if any. Maps onto the --all / --max-items pagination convention in CLI Conventions. Spec caveat to surface in the output: the underlying data may change between page requests, so a paged result "may not represent a consistent set of data" — a sync tool must not present it as an atomic snapshot. |
peppol sml migrate prepare <id> |
Step 1 of a migration, run on the old SMP: generate and register a migration key. | proposed |
PrepareToMigrate(), input MigrationRecord (SMP ID, participant identifier, migration key). Only the SMP currently publishing that participant may call it. The key then travels to the new SMP out of band — the spec explicitly defines no interface for that. |
peppol sml migrate complete <id> --key <key> |
Step 2, run on the new SMP: take over, which repoints DNS. | proposed |
Migrate(). PrepareToMigrate() must have been called with the same key or this fails. After success the participant resolves to the new SMP. |
peppol sml migrate key |
Generate a migration key that satisfies the rules. | proposed | Verified spec rules (§2.2): 8–24 characters; at least 2 lowercase, 2 uppercase and 2 digits; at least 2 from `@ # $ % ( ) [ ] { } * ^ - ! ~ |
| Fault-to-exit-code mapping | proposed | The spec defines exactly four faults, which map cleanly: notFoundFault → 3, unauthorizedFault → 7, badRequestFault → 2, internalErrorFault → 5. All four share the action URI http://busdox.org/2010/02/locator/fault and SOAP Code = Sender. This is a rare case where the exit-code mapping is fully determined by the spec. |
Grounded in Peppol SMP 1.4.0. The spec defines exactly two GET resources and nothing else:
| Resource | URL | Returns |
|---|---|---|
| ServiceGroup | /{participantId} |
<ServiceGroup> — the participant plus references to its ServiceMetadata resources |
| SignedServiceMetadata | /{participantId}/services/{docTypeId} |
<SignedServiceMetadata>, or a Redirect
|
Verified binding rules the CLI must respect and can also check:
- TLS is mandatory and the service MUST be operated on port 443 (new in 1.4.0). An SMP reachable only on another port, or over plain HTTP, is non-conformant — which makes this a reportable finding, not just a connection detail.
-
Content-TypeMUST betext/xmlorapplication/xml; the body MUST be UTF-8 with an XML declaration whoseencodingis UTF-8 (value not case-sensitive). - HTTP status codes are fixed: 200 retrieved, 404 resource not found, 500 internal error. The service
SHOULD NOT use 3xx redirection and clients need not follow it. So
404→ exit 3 and500→ exit 5, directly. - Identifiers are percent-encoded per path segment (RFC 3986); the delimiting slashes are not encoded.
Participant:
{meta scheme}::{scheme}:{value}, e.g.iso6523-actorid-upis::0010:5798000000001. Document type:{scheme}::{value}. The base URL from the NAPTR lookup may carry a constant path prefix.
| Command sketch | Description | Status | Notes / Libraries |
|---|---|---|---|
peppol smp service-group <participant-id> |
List the participant's service group: every document type it declares. | proposed |
peppol-smp-client: SMPClientReadOnly.getServiceGroup / getAllDocumentTypes. |
peppol smp metadata <participant-id> <doctype-id> |
Fetch the service metadata and show every field the spec defines per endpoint. | proposed |
SMPClientReadOnly.getServiceMetadata; getEndpoint, getEndpointAt, isEndpointValidAt, getEndpointAddress, getEndpointCertificate. Spec fields worth surfacing, not just the obvious ones: EndpointReference (a WS-Addressing EPR), @transportProfile (e.g. peppol-transport-as4-v2_0), RequireBusinessLevelSignature, MinimumAuthenticationLevel, ServiceActivationDate, ServiceExpirationDate, Certificate (PEM), ServiceDescription, TechnicalContactUrl, TechnicalInformationUrl. The last two are exactly what a support engineer wants and no other tool shows conveniently. |
| Activation and expiry are normative, not informational | An endpoint outside its validity window must be treated as unusable. | proposed | Spec: "Senders MUST ignore services that are not yet activated" and "MUST ignore services that are expired"; a missing activation date means "valid since forever" and a missing expiration date "valid until eternity". Both are xs:dateTime. So can-receive must exclude out-of-window endpoints and say why — reporting an expired endpoint as usable would be a correctness bug. isEndpointValidAt exists for this, and takes an explicit date, which is also what makes it testable. |
peppol smp verify-signature <participant-id> [<doctype-id>] |
Verify the SMP response's XML signature against the SMP trust store, report the signing certificate, and check the signature conforms to the mandated profile — an enveloped signature with exactly one enveloped-signature transform, KeyInfo/X509Data/X509Certificate present, inclusive c14n (REC-xml-c14n-20010315), rsa-sha256 signature method and sha256 digest method. A signature that verifies but uses a non-conformant algorithm is a finding worth reporting. |
proposed |
smp-client-base: SMPHttpResponseHandlerSigned (signature verification is on by default, with secure validation on) and TrustStoreBasedX509KeySelector, which also supports revocation checking via ERevocationCheckMode and a soft-fail switch. Trust store from PeppolTrustStores.Config2025 SMP entries. |
peppol smp can-receive <participant-id> <doctype-id> [<process-id>] |
The flagship command. One question, one answer: can this participant receive this document, and if so at which endpoint URL, over which transport profile, with which certificate? Reports the failing step when the answer is no. | proposed | Composes dns resolve + getServiceMetadata + endpoint selection + certificate check. Must handle wildcard document types — PeppolWildcardSelector.findPeppolDoctypeWildcardMatch exists for exactly this and is essential for PINT correctness. Exit code 3 when not registered, 4 when registered but unable to receive. |
peppol smp lookup <participant-id> |
Convenience: resolve the SMP, then list everything found, in one call. | proposed | Composition of the above. |
--follow-redirects |
Follow an SMP redirect to a secondary SMP. | proposed |
smp-client-base: ISMPFollowRedirectCallback. Spec rules: only one degree of redirect is allowed — a redirect resource cannot point to another redirect — and clients are not required to follow more than one. So the CLI follows at most one hop and reports a redirect chain as a malformed-SMP finding rather than following it. Redirects change which SMP answered, so the output must always say so. |
| Redirect certificate check | When following a redirect, verify the destination SMP's signing certificate is the one the redirecting SMP vouched for. | proposed | Spec §5.5.3: the redirecting SMP stores the unique identifier of the destination SMP's signing certificate, and the client SHOULD validate that the destination's actual certificate matches it. This is a real security check that clients commonly skip; a diagnostic tool is the right place to actually perform it and report the result. > TODO(verify): which field carries that "unique identifier" in the Redirect element and how it is compared — the spec names the concept but this page should not guess the mechanics.
|
| JSON output for all of the above | proposed |
peppol-smp-client: SMPJsonResponse already renders SMP query results to JSON with stable field-name constants. Strong candidate for the result payload, so the CLI does not invent a second SMP JSON shape. |
Important finding from SMP 1.4.0 §3: the specification "defines a REST-based interface for retrieving Service Metadata, but does not specify interfaces for creating, updating, deleting and managing Service Metadata, or any internal data storage formats." The stated goal is to let SMPs expose data from any back end.
So there is no standard Peppol SMP write API. What peppol-smp-client's SMPClient speaks
(saveServiceGroup, saveServiceInformation, saveServiceRedirect, deleteServiceGroup,
deleteServiceRegistration, with BasicAuthClientCredentials) is in practice the phoss-SMP write API, not a
normative interface.
Decision: these commands live under their own phoss-smp area rather than under smp. The area name states
what the commands actually talk to, so nobody mistakes them for standardised Peppol operations, and the split is
visible at a glance: peppol smp … is the specified read interface that works against any conformant SMP, while
peppol phoss-smp … is implementation-specific administration. It also leaves room for a future peppol <other>-smp
area should another implementation's API ever be worth supporting, without renaming anything.
> TODO(decide): area spelling — phoss-smp (hyphenated, consistent with the hyphenated action names, recommended) or phosssmp.
Consequences that must be stated in the CLI's own documentation:
- Write commands are implementation-specific by construction, and the area name says so. They are developed and tested against phoss-SMP. Other SMP implementations may happen to expose the same API — several do — and are then likely to work, but that is not a promise the tool makes.
- A write failure may mean "your SMP does not support this API", which is a different diagnosis from bad credentials or bad data. That deserves its own error message.
- Anything relying on this convention is at risk if an implementation changes it, since no spec pins it.
- The SMP spec does note (§3) that adding or removing participants in an SMP requires reflecting custody in the
SML — so
peppol phoss-smp create-participantandpeppol sml participant addare two halves of one real operation. A combined command is the genuinely useful one, and the CLI should offer it rather than leaving users to remember the second step.
> TODO(decide): should the tool detect up front that the configured SMP does not speak this API, rather than failing part-way through a multi-step operation?
> TODO(verify): whether any OpenPeppol document standardises an SMP management API that supersedes the BusDox convention. If one exists, it should be the target instead.
Requires SMP credentials, and a --dry-run plus confirmation for everything.
| Command sketch | Description | Status | Notes / Libraries |
|---|---|---|---|
peppol phoss-smp create-participant <id> |
Create a service group. | proposed |
peppol-smp-client: SMPClient.saveServiceGroup, authenticated with BasicAuthClientCredentials. |
peppol participant onboard <id> |
Do both halves of onboarding in the right order: create the service group in the SMP and register the participant in the SML, then optionally wait for DNS. Sits in its own participant area because it spans phoss-smp and sml. |
proposed | Composes smp create-participant + sml participant add + dns wait. The SMP spec explicitly notes that SMP custody must be reflected in the SML, so the two-step nature is inherent, not an implementation quirk. Must be resumable and must report which half succeeded when one fails — a participant in the SMP but not the SML is a real and confusing half-state. |
peppol participant offboard <id> |
The reverse, in the reverse order. | proposed | Remove from the SML first so nothing resolves to metadata that is about to disappear, then delete from the SMP. > TODO(verify): confirm that SML-first is the correct order for removal.
|
peppol phoss-smp delete-participant <id> |
Delete a service group and everything under it. | proposed |
SMPClient.deleteServiceGroup. Highest-impact command in the tool; print what will be removed and require confirmation. |
peppol phoss-smp set-endpoint … |
Create or update service information for a document type: process, endpoint URL, transport profile, certificate, validity window. | proposed |
SMPClient.saveServiceInformation. |
peppol phoss-smp delete-endpoint … |
Remove service information. | proposed |
SMPClient.deleteServiceRegistration. |
peppol phoss-smp set-redirect … |
Create an SMP redirect. | proposed |
SMPClient.saveServiceRedirect. |
peppol phoss-smp export [<participant-id>] |
Export one or all participants to a file. | proposed | Built on the read side. Format decision: > TODO(decide): reuse the SMP implementation's own import/export XML format so exports are interchangeable with the server UI, or define a CLI-native format? Reusing an existing format is far more useful, if one is specified.
|
peppol phoss-smp import <file> |
Bulk create/update from an export file or CSV. | proposed | Needs per-row error handling, a summary, and a documented all-or-nothing versus best-effort choice. > TODO(decide): which one?
|
The client side is the standalone, released peppol-directory-client project
(groupId com.helger.peppol.directory, version 1.0.0), which ships three artefacts:
| Artefact | Provides |
|---|---|
peppol-directory-searchclient |
PDSearchClient, PDSearchQuery, PDSearchRateLimitException — the public search API |
peppol-directory-searchapi |
CPDSearchAPI, EPDSearchAPIField, PDResultListMarshaller and the JAXB result types — the REST API model |
peppol-directory-client |
PDClient — the authenticated indexer API |
Packages are unchanged (com.helger.pd.searchclient, com.helger.pd.searchapi, com.helger.pd.client). Server-side
behaviour below was verified against the phoss-directory server (0.19.1-SNAPSHOT).
Two separate REST interfaces are involved, with very different access requirements — the search API is public, the indexer API needs a client certificate.
Endpoint shape {directory URL}/search/1.0/{format}, where format is xml (the default) or json
(EPDOutputFormat). Directory URL comes from EPeppolNetwork.
| Command sketch | Description | Status | Notes / Libraries |
|---|---|---|---|
peppol dir search <query> |
Free-text search. | proposed | Maps to the q query field. |
peppol dir search --name --country --participant … |
Field-specific search, repeatable per field. | proposed | Verified field names (EPDSearchField): q, participant, name, country, geoinfo, identifierScheme, identifierValue, website, contact, addinfo, regdate, doctype. Multiple values per field are supported as repeated parameters. At least one term is required — an empty query returns HTTP 400 "No valid query term provided!", which maps to exit 2. > TODO(verify): ESearchOperator / ESearchOperatorText exist in the publisher, so fields appear to support operators (exact, partial, ranges?). The exact query syntax for expressing an operator over the REST API needs confirming before the CLI exposes it.
|
| Paging |
--max-items / --page-size / --all over the API's own paging. |
proposed | Request parameters: resultPageIndex (alias rpi, default 0) and resultPageCount (alias rpc, default 20). Hard server cap MAX_RESULTS = 1000 — a first or last result index beyond it is rejected with HTTP 400. The response makes correct paging easy rather than guesswork: ResultListType carries required attributes total-result-count, used-result-count, result-page-index, result-page-count, first-result-index, last-result-index, plus query-terms, version and creation-dt. So --all iterates until last-result-index reaches total-result-count or the 1000 cap, and the CLI can report precisely — "showing 20 of 347", or "1000 of 4812 — truncated by the server" — instead of ending silently. |
| Page iteration is the caller's job | proposed | Confirmed against 1.0.0: PDSearchClient.search (...) fetches one page and there is no multi-page helper. So --all is implemented in the CLI by looping over PDSearchQuery.setResultPageIndex (...), driven by the response's total-result-count and stopping at CPDSearchAPI.MAX_RESULTS. > TODO(decide): contribute a page-iterating helper back to peppol-directory-searchclient, since every consumer will otherwise write the same loop?
|
|
| Query echo and timestamp | proposed |
query-terms is echoed back by the server and creation-dt timestamps the result. Both belong in the JSON envelope: they make a stored CI result self-describing, and query-terms satisfies the same audit principle as alias echoing in CLI Conventions. |
|
| Client library | proposed |
A released client exists: com.helger.peppol.directory:peppol-directory-searchclient:1.0.0 (com.helger.pd.searchclient.PDSearchClient), so the CLI writes no Directory protocol code and — unlike the Phorm path — depends on no snapshot. API: constructors taking a host String, a URI, or a URI plus caller-supplied HttpClientSettings; getSearchURL (PDSearchQuery); and search (PDSearchQuery) returning a ResultListType and throwing IOException. It is Closeable. Note it deliberately does not depend on ph-config or on a key store — the search API is public, so a caller with no Peppol certificate can use it. |
|
| Query building | proposed |
PDSearchQuery in the same module: typed per-field terms via EPDSearchAPIField (repeatable per field), convenience methods (generic, participantID, documentTypeID, name, countryCode), paging setters, hasQueryTerms () to catch the empty query the server rejects with HTTP 400, isBeyondMaxResults () to catch the 1000-result cap before sending, and parseQueryTerms (...) to read back the query-terms attribute a result carries. Constants live in CPDSearchAPI (PATH_SEARCH_10, MAX_RESULTS, the query parameter names and the server defaults). |
|
| Rate limiting | proposed |
PDSearchRateLimitException is thrown on HTTP 429 and carries getRetryAfterSeconds () from the Retry-After header. The CLI must honour it and back off — this matters most in peppol bulk, where a naive loop trips the limiter immediately. |
|
| Response parsing | proposed | Handled inside the client by PDSearchResponseHandler, which unmarshals with PDResultListMarshaller from com.helger.peppol.directory:peppol-directory-searchapi — so every response is validated against directory-search-result-list-v1.xsd. HTTP 404 comes back as null ("nothing found"), distinct from an empty result list. Typed model: ResultListType, MatchType, EntityType, IDType, NameType, ContactType. |
|
| JSON output derives from the response model | proposed |
--output json for dir search should be a rendering of ResultListType, not an independently invented shape. Per match: participantID, zero or more docTypeID, and one or more entity. Per entity: one or more name (each with an optional language), countryCode, optional geoInfo, and repeatable identifier (scheme + value), website and contact (type/name/phone/email), plus optional additionalInfo and regDate. |
|
| Document type IDs come back with search results | A search can answer "what can this participant receive?" without an SMP round trip. | proposed |
MatchType carries docTypeID (zero or more) alongside the participant ID. Worth surfacing, but the output must be clear that this is the Directory index's view, which can lag the SMP — peppol smp can-receive remains the authoritative answer. |
| Deleted entities | proposed |
EntityType has a deleted boolean attribute (default false). The CLI must not silently present deleted entities as live: either filter them by default and say how many were hidden, or flag them explicitly. > TODO(decide): filter or flag?
|
|
peppol dir businesscard <participant-id> |
Fetch and display a participant's business card. | proposed |
Resolved: the card is fetched from the participant's own SMP, not from the Directory, at {smpHostURI}businesscard/{participantId} — verified in SMPBusinessCardProvider, which resolves the SMP through the normal ISMPURLProvider discovery. Data model and parsing already exist in peppol-directory-businesscard (PDBusinessCard, PDBusinessEntity, PDContact, PDName, EBusinessCardVersion, v1/v2/v3). Note this businesscard/ resource is not part of Peppol SMP 1.4.0, which defines only two resources — it is a phoss-SMP/Directory convention, so the same implementation-dependence caveat as the SMP write API applies. |
peppol dir businesscard --validate <file> |
Validate a business card document locally. | proposed | Marshaller-based schema validation via PDBusinessCardHelper. |
A capability worth having that is easy to overlook: an SMP operator can ask the Directory to index or drop a
participant. com.helger.peppol.directory:peppol-directory-client already does this.
| Command sketch | Description | Status | Notes / Libraries |
|---|---|---|---|
peppol dir indexed <participant-id> |
Is this participant currently in the Directory index? | proposed |
PDClient.isServiceGroupRegistered. |
peppol dir index <participant-id> |
Request indexing of a participant. | proposed |
PDClient.addServiceGroupToIndex. |
peppol dir unindex <participant-id> |
Request removal from the index. | proposed |
PDClient.deleteServiceGroupFromIndex. |
| Authentication | proposed |
PDClient talks to the indexer REST interface and needs a client certificate; PDClientConfiguration reads pdclient.keystore.path / .type / .password / .key.alias / .key.password plus its own HTTP timeout and proxy keys. This is a third ph-config-based mechanism alongside SMPClientConfiguration — see the collision note in Configuration. |
|
| Onboarding integration | proposed | Indexing is the natural third step after SMP and SML registration, so peppol participant onboard should offer --index to complete the set. |
| Command sketch | Description | Status | Notes / Libraries |
|---|---|---|---|
peppol cert verify <file|url> |
Verify an AP or SMP certificate against the Peppol CA chain for the chosen network and role. | proposed |
peppol-commons: PeppolTrustedCA with peppolTestAP(), peppolProductionAP(), peppolAllAP(), peppolTestSMP(), peppolProductionSMP(), peppolAllSMP(). Requires two explicit inputs: role (AP or SMP) and network (test or prod) — a certificate valid as one is not valid as the other. Verified from the G3 CA certificates bundled in peppol-commons: a two-tier hierarchy with one root and a separate issuing CA per role, which is exactly why role and network are both required inputs. |
| CA generation — decided: G3, and G3 only | No --generation flag for now. |
proposed | Generations are named by year in PeppolTrustStores: Config2018 = G2 (JKS, @Deprecated since 12.3.8) and Config2025 = G3 (PKCS12, current). PeppolTrustedCA is wired to Config2025 alone, and since 12.4.2 the 2025 trust stores no longer contain the G2 CAs — so G2 verification is not reachable through the supported API and is deliberately not offered. G3 is the default and the only supported generation. A CA generation rollover happens roughly every 8–10 years, so a selector with exactly one valid value would be speculative surface; if a G4 is announced the flag can be introduced then as a purely additive change, with G3 remaining the default until G4 is actually in use. Trust store aliases are literally e.g. peppol root ca - g3. OpenPeppol describes G2 as "the legacy generation of the Peppol PKI (from 2018)" issued through DigiCert MPKI8, and G3 as "the next generation of the Peppol PKI (from 2025)" issued through DigiCert One Trust Lifecycle — which matches the Config2018 / Config2025 naming exactly. |
peppol cert verify --service-domain post-award|pre-award|eb2b |
Verify against the CA set for a specific service domain. | proposed |
EPeppolServiceDomain binds per-domain CA checkers: POST_AWARD, PRE_AWARD, ENHANCED_B2B. |
peppol cert info <file> |
Print subject, issuer, serial, validity window, key algorithm and size, extensions and fingerprints. | proposed | JDK plus ph-security helpers. No Peppol-specific logic. |
| CA hierarchy and DNs | Reference values the tool checks against and prints. | proposed | Read directly from the trust stores in peppol-commons (truststore/2025/*.p12, password peppol), all SHA256withRSA with RSA-4096 keys and valid 2025-01-14 → 2035-01-13/14:Production — root CN=PEPPOL Root CA - G3, O=OpenPEPPOL AISBL, C=BE; AP CN=PEPPOL ACCESS POINT CA - G3, …; SMP CN=PEPPOL SERVICE METADATA PUBLISHER CA - G3, …Test (pilot) — the same three with TEST in the CN and an extra OU=FOR TEST ONLY, e.g. CN=PEPPOL Root TEST CA - G3, OU=FOR TEST ONLY, O=OpenPEPPOL AISBL, C=BE. |
peppol cert verify detects a test certificate used on production |
And the reverse. | proposed | The pilot CAs carry OU=FOR TEST ONLY, so a certificate issued for test is unmistakable. Using a test certificate against production (or vice versa) is a common and confusing operator mistake, and a diagnostic tool should name it explicitly — "this is a TEST certificate, but --network is prod" — rather than reporting a generic chain failure. |
| Revocation endpoints | proposed | Taken from the G3 CA certificate extensions: CRL http://crl.one.nl.digicert.com/PEPPOLRootCA-G3.crl, OCSP http://ocsp.one.nl.digicert.com. Both are plain HTTP by design, which is normal for revocation services and worth not flagging as a TLS problem. |
|
peppol cert expiry <file|url> [--warn-days N] |
Report remaining validity, warning when it falls below the threshold. | proposed | Decided: "expiring soon but still valid" is exit 0 with a warning, not exit 4. The certificate is valid, and a check command should not claim otherwise — a warning is advice, and advice is not failure. Only an already expired or not-yet-valid certificate is a real check failure and exits 4. This is the command most likely to be wired into monitoring, so the boundary has to be unambiguous. |
| Making the warning actionable in CI | Exit 0 must not mean "nothing to see here". | proposed | Since the warning does not change the exit code, it has to be machine-readable: --output json carries it in messages[] with a stable code such as CERT_EXPIRING_SOON, plus the expiry date and remaining days as typed fields in result, so a pipeline can decide for itself. For jobs that do want a non-zero exit, add an explicit opt-in — > TODO(decide): --fail-on-warn as a general flag across all commands, or a command-specific --fail-below <duration> here? A general flag is more predictable; a command-specific one is more discoverable.
|
peppol cert revocation <file> |
Check revocation status via OCSP, with CRL as fallback. | proposed |
ERevocationCheckMode and the soft-fail/caching options on TrustStoreBasedX509KeySelector; PeppolTrustedCA.clearRevocationCheckCache() exists. Soft-fail versus hard-fail must be an explicit flag, never a silent default, because a soft-fail pass looks identical to a real pass. |
peppol cert seat-id <file> |
Extract the Seat ID from a service provider certificate and decode it: the Peppol Authority prefix plus the 6-digit sequence. | proposed |
Fully covered by the library. The Seat ID is the certificate's Subject CN — confirmed both against real certificates (CN=PNL000052, CN=PNO000070, CN=POP000000) and against the OpenPeppol PKI 2025 enrolment documentation. SPIDHelper.getSeatIDFromCertificate (X509Certificate) / getMainIDFromCertificate (X509Certificate) do the whole job, with getSeatIDFromSubjectCN (String) / getMainIDFromSubjectCN (String) and isValidSeatID (String) for callers holding the CN. Added in peppol-commons 13.1.0. |
peppol cert of-endpoint <participant-id> <doctype-id> |
Fetch the certificate the SMP publishes for an endpoint and verify it in one step. | proposed |
SMPClientReadOnly.getEndpointCertificate + PeppolTrustedCA. The practical everyday check. |
peppol keystore inspect <file> |
List entries, aliases, types, chains and validity in a keystore. | proposed |
ph-security: KeyStoreHelper, EKeyStoreType, TrustStoreDescriptor. |
peppol keystore convert <in> <out> |
Convert between keystore types (JKS ↔ PKCS12), which is directly relevant to the G2→G3 move from JKS to PKCS12. | proposed |
KeyStoreHelper. Passwords via prompt or environment variable only. |
peppol keystore csr |
Generate a key pair and a certificate signing request for a Peppol certificate application. | proposed |
Fully specified, and simpler than assumed. The OpenPeppol "Peppol PKI 2025 — Issuing and Enrolment Process" page gives the reference flow verbatim:openssl genpkey -algorithm RSA -pkeyopt 'rsa_keygen_bits:2048' -aes256 -out private-key.pemopenssl req -new -sha256 -key private-key.pem -subj "/" -out private_key.csrSo: RSA 2048, private key encrypted with AES-256, CSR signed with SHA-256, and an empty subject ( -subj "/") because "Subject / SAN / Extensions: the values will be overridden by the CA". The CSR is then uploaded to the DigiCert portal. Needs BouncyCastle; ph-bc exists in ph-commons. |
| A CSR carries no validity — there is nothing to get wrong | proposed | A PKCS#10 request has no validity field at all; notBefore / notAfter are set by the CA at issuance, and the Peppol default is 2 years. Combined with the empty subject, this means the CLI has no certificate policy decisions to make: its entire job is a correct RSA-2048 key pair, encrypted at rest, and a SHA-256-signed CSR with an empty subject. An earlier note here warned that a wrong subject DN could waste a paid application — that concern was misplaced and has been removed. |
|
| Output format for the generated material — decided: both | --format pem|p12|both |
proposed | Both are supported, because both are genuinely needed: PEM matches the documented OpenPeppol reference commands and is what the DigiCert portal flow expects, so a user can follow the official instructions and cross-check against them; PKCS#12 is where the key has to end up anyway for an AP or SMP configuration, and doing it here avoids a manual conversion step people routinely get wrong. both writes the pair plus the keystore in one go. > TODO(decide): which value is the default? "both" is the most helpful and the least likely to send someone back to openssl, at the cost of writing three files instead of two.
|
| The private key is encrypted in every format | proposed | The reference flow uses -aes256 for the PEM key, and a PKCS#12 keystore is password-protected. The CLI must never write an unencrypted private key in any format, and must take the password from a prompt or environment variable rather than a command line argument (see CLI Conventions). |
|
| Guidance in the output | proposed | OpenPeppol notes that CSR-based enrolment is "a more advanced form of generating the key-pair" and that the Service Desk has "limited ability to guide you through this process". That makes the command's output worth writing carefully: state what was generated, where, that the private key must never leave the machine, and what the next step is (upload the CSR to the DigiCert portal). | |
peppol keystore export-cert <file> --alias <a> |
Export a certificate from a keystore to PEM/DER. | proposed |
KeyStoreHelper. |
Transport level only. No AS4 — see ADR-001.
| Command sketch | Description | Status | Notes / Libraries |
|---|---|---|---|
peppol endpoint tls <url> |
Report the negotiated TLS version and cipher suite, the offered protocol versions, the served certificate chain, SNI behaviour and hostname match — and check them against the Peppol Policy for Transport Security 1.1.0. | proposed | JDK SSLSocket/SSLParameters. The policy gives concrete, checkable rules (see the next rows), so this command can report pass/fail rather than merely describe. |
| POLICY 1 — approved CA | The TLS certificate must chain to a publicly trusted root. | proposed | "Each TLS certificate used in the Peppol eDelivery Network MUST be issued (directly or indirectly) only by a root certificate contained in the latest version of the List of pre-loaded CA certificates of the Mozilla Network Security Services." Checkable exactly, not approximately: com.helger:ph-nss 1.0.0 ("Mozilla NSS root certificate trust store for Java applications") ships the list as a PKCS#12 trust store — MozillaNSSTrustStore.TRUSTSTORE (loaded), TRUSTSTORE_DESCRIPTOR, TRUSTSTORE_CLASSPATH = "truststore/mozilla-nss-root-certs.p12", TRUSTSTORE_PASSWORD = "changeit". So the CLI verifies against the Mozilla list the policy actually names, rather than against the JDK default trust store that merely resembles it. |
| The NSS list has a version, and the policy says "latest" | proposed | The bundled list is versioned — currently Mozilla NSS v2.90, dated 2026-09-23 per mozilla-nss-root-certs.md. POLICY 1 requires the latest version, so a stale bundle could pass a certificate that the current list no longer trusts, or vice versa. The output must therefore state which NSS version it checked against, so the answer is interpretable. Same class of staleness as the code lists, and handled the same way: bundled, versioned, refreshed by upgrading. > TODO(decide): warn when the bundled NSS list is older than some threshold?
|
|
| POLICY 2 — no self-signed | proposed | "Self-signed TLS certificates are not allowed", because a man-in-the-middle attack could go unnoticed. Trivially checkable and worth reporting explicitly, since a self-signed endpoint otherwise just looks like a generic trust failure. | |
| POLICY 3 — SSL Labs grade A | The headline requirement, and the source of the TLS 1.2 floor. | proposed | "The TLS configuration MUST constantly be of at least grade 'A' according to SSL Labs", and anything graded below A "is considered to be unavailable with regards to the Transport Infrastructure Agreement". Note the policy states no protocol version at all — a minimum of TLS 1.2 follows from the grade-A requirement rather than from a literal rule, which is deliberate: an outcome-based policy does not need re-issuing as cryptography moves. The CLI checks the components it can see locally — protocol versions offered, cipher suites, key sizes, chain validity — and reports "no grade-A blocker found locally", without claiming to reproduce an SSL Labs grade, which it cannot. |
| Name SSL Labs in the output — decided: yes | Point at the authoritative grader. | proposed | SSL Labs is named normatively by the policy itself (reference [SSL-LABS], https://www.ssllabs.com/ssltest), so referring to it is quoting the requirement rather than endorsing a third party. The local checks are explicitly a pre-check, and the output should say so and print the SSL Labs URL for the endpoint under test, so the user can get the grade that actually counts. |
| Do not call the SSL Labs API by default | proposed | Printing a URL is not the same as submitting the endpoint. An automatic API call would send a third party's hostname to an external service, is rate-limited, and takes minutes — none of which suits a command expected to answer quickly. So: print the link by default, and if the grade is ever fetched automatically it must be an explicit opt-in that says what it is about to do. Consistent with how document content is treated for Phorm — nothing leaves the machine without the user choosing it. > TODO(decide): offer --ssllabs to fetch the real grade, or leave that to the browser entirely?
|
|
| Scope of the policy | Which endpoints it applies to. | proposed | Explicitly: all Access Points for all transport protocols, all SML and Peppol Directory instances, and SMP instances when operated over https. So peppol endpoint tls is meaningful against essentially every URL this tool touches, not just APs. |
| The TLS certificate is not the Peppol certificate | A distinction users confuse constantly. | proposed | The policy is explicit: "The TLS Certificates are not provided by OpenPeppol and MUST be issued by third party Certificate Authorities." So an endpoint has two unrelated certificates — a public WebPKI certificate for the TLS layer, and a Peppol AP/SMP certificate from the Peppol PKI at the application layer. peppol endpoint tls checks the former, peppol cert verify the latter. The output of both should name which one it is looking at, since "the certificate is fine / invalid" is ambiguous otherwise. |
peppol endpoint http <url> |
Check reachability: status code, redirect chain, response headers, timing. | proposed |
ph-httpclient, consistent with the rest of the ecosystem. |
peppol endpoint check <participant-id> <doctype-id> |
Resolve the endpoint from the SMP and run the TLS and HTTP checks against it. | proposed | Composition. Must state clearly that a pass does not prove an AS4 exchange would succeed. |
| Command sketch | Description | Status | Notes / Libraries |
|---|---|---|---|
peppol codelist list <kind> |
List document types, processes, participant schemes, transport profiles or SPIS use cases. | proposed |
peppol-id generated enums, which mirror the bundled code list XMLs entry for entry: EPredefinedDocumentTypeIdentifier, EPredefinedProcessIdentifier, EPredefinedParticipantIdentifierScheme, EPredefinedTransportProfileIdentifier, EPredefinedSPISUseCaseIdentifier; lookups via PredefinedDocumentTypeIdentifierManager, PredefinedProcessIdentifierManager, PeppolParticipantIdentifierSchemeManager. Counts in code list v9.7, read from the bundled XMLs: document types 321, processes 109, participant schemes 105, transport profiles 5, SPIS use cases 1. Each generated enum also exposes its own CODE_LIST_ENTRY_COUNT, so the CLI can report the count without walking the values. |
peppol codelist list spis-use-cases |
List the Peppol Use Case IDs. | proposed |
Resolved — the list exists and is already bundled. EPredefinedSPISUseCaseIdentifier in com.helger.peppolid.peppol.spisusecase implements IPredefinedSPISUseCaseIdentifier extends ICodeListItemWithRelease, with getUseCaseID (), getInitialRelease (), getState (), getDeprecationRelease () and getRemovalDate (). Generated from Peppol Code Lists - SPIS Use Case v9.7.xml, so it carries the same three-valued state and release metadata as every other kind. This is the eDEC-maintained list SPIS 1.0.0 promised would be "published together with the other code lists". |
| Exactly one Use Case ID exists today | MLS |
proposed | The whole code list is a single active entry: MLS ("MLS", Version.parse ("9.4"), ECodeListItemState.ACTIVE, null, null) — active since code list 9.4, with CODE_LIST_ENTRY_COUNT = 1. Worth a caveat in the output: SPIS 1.0.0 uses Rprtng_MLS in its worked examples, but that is illustrative syntax, not a registered value — the only assigned Use Case ID is MLS. Anyone who copied Rprtng_MLS out of the specification has an unregistered identifier, which is exactly the kind of thing peppol id spid should flag. |
| Entry state is three-valued, not two | |||
peppol codelist list transport-profiles |
proposed | Worth knowing when designing the output: only one transport profile is active in v9.7 (AS4, profile version 2.0); START, AS2 1.0, AS2 2.0 and AS4 1.0 are all removed. |
|
peppol codelist search <text> |
Search across kinds by ID, name or fragment. | proposed | Same enums. Purely local, so it is instant and works offline. |
peppol codelist show <id> |
Show one entry in full, including its deprecation state. | proposed |
ECodeListItemState / ICodeListItem in edelivery-id. |
peppol codelist alias <name> |
Resolve a code list display name to its full identifier — so a user can type a short, memorable name instead of a 200-character document type ID. | proposed | Display names are available: EPredefinedDocumentTypeIdentifier.getCommonName() returns e.g. "Virtual Company Dossier", and the code list XML carries them as the name attribute. But see the ambiguity finding below — display names are not unique.
|
Ambiguous alias fails, in every codelist action too |
codelist show, codelist alias and anything else taking an alias. |
proposed | If an alias resolves to more than one identifier — unlikely but real, since 16 display names in v9.7 are duplicated — the command fails with exit 2 and the message "alias is not unique", listing each candidate in full and telling the user to pass the full identifier instead. It never resolves to the first match. See CLI Conventions for the JSON shape (ALIAS_NOT_UNIQUE plus the candidate list). |
| Aliases accepted wherever a document type ID is | Any command taking a document type ID also accepts an alias, so peppol smp can-receive 0088:123 "peppol-bis-invoice-3" works. |
proposed | Slugify the display name (lowercase, spaces and punctuation to hyphens) so it needs no shell quoting. Verified constraint: of the 321 document type display names in v9.7, 16 are used twice — for example A-NZ PINT Invoice v1.0, JP PINT Invoice v1.0 and PEPPOL Billing profile Invoice V1 each appear on two entries, typically a busdox-docid-qns and a peppol-doctype-wildcard variant. So an alias must not silently resolve to one of two candidates. Required behaviour: on ambiguity, exit 2 and list the candidates with a qualified form (e.g. --scheme peppol-doctype-wildcard) to disambiguate. |
| Alias resolution is always echoed | When an alias resolves, the full identifier used is printed (stderr in text mode, an explicit field in JSON). | proposed | Non-negotiable for scripting: a log line saying "checked A-NZ PINT Invoice v1.0" is not auditable, whereas one naming the resolved ID is. It also teaches users the real identifier. |
peppol codelist deprecated |
List every deprecated entry, optionally filtered by kind. | proposed |
isSchemeWithISO6523CodeDeprecated and the per-entry state. |
peppol codelist version |
Print the bundled code list version per kind. | proposed | Each code list is versioned independently and each generated enum carries its own CODE_LIST_VERSION constant (all "9.7" at the time of writing, but they need not match). This is by design, so the command reports one version per kind and must not treat differing versions as an error. A single aggregate "the code list version" would be a misrepresentation. |
| Offline by construction | Every code list command works with no network at all. | proposed | Free, and unconditional: the enums are compiled in, so there is no cache to invalidate, no download to fail and nothing for --no-cache to affect in this area. This follows from rejecting codelist update and codelist diff — see Rejected features. |
| Transport profile source — decided | proposed | Use EPredefinedTransportProfileIdentifier, generated from the Peppol code list: it is what Peppol allows, so it backs codelist list transport-profiles and every "is this valid in Peppol?" check. ESMPTransportProfile in peppol-commons is a wider collection used internally by the SMP client and is not a statement about Peppol validity, so the CLI must never present it as one. |
Via a configured Phorm instance — see ADR-003.
A published client library now exists: com.helger:phorm-client ("Java client library for the phorm REST
API"), so the CLI writes no Phorm protocol code. PhormClient is AutoCloseable, is constructed either with
(String sBaseURL, String sToken) or through PhormClient.builder (), and exposes an IIdentifierFactory.
Errors surface as PhormClientException.
| Command sketch | Description | Status | Notes / Libraries |
|---|---|---|---|
peppol validate file <doc> |
Validate a document, letting the server detect the document type. | proposed |
PhormClient.determineAndValidate (byte[]) or (IReadableResource) → PhormValidationResult. |
peppol validate file <doc> --vesid <id> |
Validate against an explicitly chosen artefact set. | proposed |
PhormClient.validate (DVRCoordinate, ...), or the validate (String sVESID, ...) overload that parses the coordinate for you. A VESID is groupId:artifactId:version[:classifier], where the version may be a pseudo-version such as latest, latest-release or latest-active (ph-diver DVRCoordinate). Example: eu.peppol.bis3:invoice:latest-active. |
peppol validate detect <doc> |
Detect syntax, Customization ID, process, sender/receiver and the derived VESID without validating. | proposed |
PhormClient.determineDocumentDetails (byte[]) → DocumentDetails (the ddd-model type), so the CLI gets the detection result as a typed object. Note this is the server-side detection endpoint. For a genuinely offline detection, embed ddd-model directly — it is already a compile dependency of phorm-client, so it costs nothing extra. > TODO(decide): offer both, e.g. peppol validate detect --local? Local detection is the honest way to show what would be sent before anything leaves the machine.
|
peppol validate vesids |
List the artefact sets the configured instance offers. | proposed |
PhormClient.getAllVESIDs () / getAllVESIDs (boolean bIncludeDeprecated) → ICommonsList <PhormVESID>. Also the cheapest connectivity probe, since the server does not token-protect this endpoint. |
--output text report |
Human-readable report: counts by severity, then each finding with location, rule ID and message. | proposed |
PhormValidationResult gives everything needed without touching raw JSON: isSuccess (), isInterrupted (), getMostSevereErrorLevel (), getOverallValidity () (EExtendedValidity), getAllErrors () / getAllFailures () (ErrorList), getValidationResultList () (the phive type), getDocumentDetails (), getInvocationDateTime () and getInvocationDurationMillis (). |
--output json report |
Machine-readable report for CI. | proposed |
Decision now easy: PhormValidationResult.getAsJson () returns an IJsonObject, so Phorm's own JSON can be embedded in the result field of the CLI envelope unchanged, with no second shape to maintain. getRawResponse () is available if the untouched body is wanted. |
peppol validate file --hybrid <pdf> |
Validate a hybrid (PDF-carried) document. | proposed |
PhormClient.hybridValidate (byte[]), with overloads taking an EPhormHybridCountry. Lower priority than the XML path. |
| XML / HTML report passthrough | Hand the server's own report straight to the user. | proposed |
validateAsXML / determineAndValidateAsXML → IMicroDocument (schema: phorm-validation-result.xsd), and validateAsHtml / determineAndValidateAsHtml → String. Useful for --output xml or for writing a shareable HTML report, which support engineers will want. > TODO(decide): expose an --output html that writes the server-rendered report to a file?
|
| Error and exit code mapping | proposed | Failures arrive as PhormClientException rather than as raw status codes, which removes the trap noted earlier: Phorm answers HTTP 400 both for an invalid document and for an unparseable body, and the phorm.api.response.onfailure.http400 server setting can turn failures into 200s. The CLI maps on the result, not the transport: a parsed report with findings → exit 4; a client exception carrying a server-side problem → 2 or 6; authentication rejected → 7; unreachable or 5xx → 5. getServerException () carries the server's own error object when present. > TODO(verify): which conditions PhormClientException distinguishes, so the mapping keys off its own API rather than off parsing messages.
|
|
| Configuration | proposed | Base URL plus the shared-secret token — peppol.cli.phorm.url and peppol.cli.phorm.token in Configuration. The token is the X-Token header value; PhormClient handles the header itself. |
|
| Dependency note | proposed |
phorm-client is at 1.0.0-SNAPSHOT and therefore unreleased. > TODO(decide): M2 depends on a released phorm-client, so either wait for 1.0.0 or treat Phorm validation as the one feature that may lag. Worth knowing early rather than discovering at release time.
|
| Command sketch | Description | Status | Notes / Libraries |
|---|---|---|---|
peppol mls parse <file> |
Parse and pretty-print an MLS response: response code, status reason codes, referenced message, line-level detail. | proposed |
peppol-commons peppol-mls: PeppolMLSMarshaller, EPeppolMLSResponseCode, EPeppolMLSStatusReasonCode, CPeppolMLS. Built on ph-ubl21. |
peppol mls validate <file> |
Validate an MLS document against its Schematron rules. | proposed |
PeppolMLSValidator (Schematron via ph-schematron-xslt, phive-api). |
peppol mls generate … |
Generate an MLS response. | proposed — discuss | The builders exist (PeppolMLSBuilder, PeppolMLSLineResponseBuilder, PeppolMLSLineResponseResponseBuilder), so this is cheap to implement — which is precisely why the question is whether it should be. An MLS response is a statement by the receiving party about a message it processed; a CLI that emits one invites hand-crafted statuses being sent outside the AP that actually handled the message. See Open Questions #4. |
peppol sbdh parse <file> |
Parse and display an SBDH envelope. Adjacent, and useful for the same audience. | proposed |
peppol-sbdh: PeppolSBDHData, PeppolSBDHDataReader, EPeppolSBDHDataError. Would also validate the Peppol SBDH constraints. > TODO(decide): is SBDH inspection in scope? It is not in the original feature list, but it is a small addition that integrators would use constantly.
|
| Command sketch | Description | Status | Notes / Libraries |
|---|---|---|---|
peppol bulk lookup <file.csv> |
Run any lookup command over a CSV of participants, writing one result row per input row. | proposed |
ph-csv for reading and writing. Must include the input row's identity in every output row so results can be joined back. |
peppol bulk can-receive <file.csv> --doctype <id> |
The most useful bulk case: check a whole customer list against one document type. | proposed | Composition of smp can-receive. |
peppol bulk cert-expiry <file.csv> |
Check certificate expiry across many participants — the natural monitoring job. | proposed | Composition. |
--parallel <n> |
Run lookups concurrently, limited per SMP. | proposed | Decided: the limit is applied per resolved SMP URL, not globally, with a default of 2. So a CSV spread across 50 SMPs still runs at useful throughput — total in-flight requests can far exceed 2 — while no single SMP ever sees more than 2 concurrent requests from this tool. A global limit would have been both slower across many SMPs and harsher on any one of them, which is the wrong trade in both directions. |
--parallel above 4 warns |
Configurable, but not silently. | proposed | Any value greater than 4 proceeds and emits a warning, because per-SMP concurrency beyond that is antisocial rather than merely aggressive — an SMP operator has no way to distinguish it from a small attack. Per the general rule in CLI Conventions a warning does not change the exit code, so --parallel 8 still exits 0; the warning carries a stable code in --output json so a CI job can catch it in review. > TODO(decide): a hard ceiling as well, or is warn-and-proceed the whole story? Warn-only trusts the operator, which suits a tool aimed at people running their own infrastructure.
|
| Bucketing requires resolving first | An implementation consequence worth planning for. | proposed | A participant's SMP is only known after DNS resolution, so the work cannot be bucketed until each row has been resolved. That makes the pipeline two-stage: resolve (bounded by its own concurrency, since DNS goes to the resolver and not to any SMP), then group by resolved SMP URL and apply the per-SMP limit. Getting this wrong collapses into a single global limit and quietly loses the benefit. |
| The Directory has its own throttle | Not covered by --parallel. |
proposed |
peppol dir search in bulk is limited by the Directory's own rate limiter (HTTP 429 with Retry-After), which is a single shared service rather than per-participant infrastructure. So Directory bulk work needs its own, more conservative concurrency plus Retry-After handling — reusing the per-SMP limit there would be meaningless, since every request goes to the same host. |
--continue-on-error |
Keep going after a failed row and report a summary. | proposed | Should be the default for bulk; the exit code then reflects whether any row failed. > TODO(decide): confirm.
|
| Output | proposed | CSV output for spreadsheets, JSON Lines for pipelines. > TODO(decide): is JSON Lines acceptable given that --output json is otherwise defined as a single JSON document? A per-row stream is genuinely better for large bulk runs.
|
Things considered and deliberately not planned. Kept here rather than deleted, because "why isn't there a command for X?" is a question that will be asked repeatedly, and because a rejection is a decision worth being able to revisit on purpose.
| Command sketch | Why rejected |
|---|---|
peppol codelist diff <v1> <v2> |
The generated enums are the only runtime API for code list data, so a single JAR exposes exactly one version, and no shared library for runtime code list access is expected. Diffing would mean the CLI carrying its own code list parser plus a download mechanism — disproportionate for the benefit. Anyone needing a diff can compare two releases of peppol-id directly. |
peppol codelist update |
Same root cause: code list data lives in compiled enums and cannot be refreshed at runtime. Code lists are updated by upgrading the tool, which is predictable and means an unattended run never depends on a background download. peppol codelist version still reports what is bundled, per kind. See Open Questions #5. |
peppol sml query <a third party's participant ID> |
Not implementable. SML 1.3.0 has no anonymous read interface — both management interfaces require mTLS with a certificate under the Peppol root CA, and the authenticated identity scopes what may be read. The only public discovery path is DNS, which peppol dns resolve already provides. Own-SMP reads (sml smp read, sml participant list) are available and are listed under SML. |
peppol mlr parse (Message Level Response) |
MLR is end of life. Building tooling for a format on its way out would create users for something that is going away, and would invite confusion with MLS, which is the one that matters. peppol-commons still ships a peppol-mlr module for existing consumers; peppol-cli does not use it. |
peppol sml smp-cert-update (certificate rollover) |
SML 1.3.0 defines no certificate-change operation. An earlier draft of this page speculated one existed; it does not. If BDMSL offers one as an implementation extension, it is out of spec, and the CLI should not present an extension as though it were standard. > TODO(verify): does BDMSL expose non-spec operations (certificate change, SMP listing) worth supporting explicitly as extensions?
|
Support for other networks — DBNAlliance, HR eDelivery, generic BDXR SMP deployments (--flavour, --provider) |
Out of scope: this is a Peppol tool. The libraries happen to support other networks — BDXRClientReadOnly / BDXR2ClientReadOnly for OASIS BDXR SMP 1.0/2.0, DBNAURLProviderSMP for DBNAlliance discovery, and the dbnalliance-* / hredelivery-commons modules — but exposing them would double the surface of nearly every command, double the test matrix, and force every output and error message to be hedged about which network it refers to. The value of this tool is that it answers Peppol questions precisely. See ADR-007. |
| AS4 send / receive, AS4 message inspection, mock AP | Out of scope for the project, not just unimplemented. See ADR-001 and Vision and Scope. |
| TSR / EUSR report submission | Out of scope — needs the AP's own message history and identity. See ADR-002. |
Two entries elsewhere are not rejected but are worth knowing about, since they are commonly assumed to exist:
-
peppol mls generateisproposed — discuss, not rejected. The builders exist; whether a CLI should emit an MLS response is the question. See Open Questions #4. - SMP write commands are proposed but not standardised — Peppol SMP 1.4.0 defines no create/update/delete interface, so they target a de-facto convention. See Open Questions #10.
peppol-cli — licensed under the Apache License, Version 2.0.
Not an official OpenPeppol tool. This is an independent open source project and is neither endorsed by nor affiliated with OpenPeppol AISBL. "Peppol" is a trademark of OpenPeppol AISBL.
Status: planning
Overview
Design
Decisions
Reference