News and noteworthy - phax/phoss-ap GitHub Wiki
v0.13.2 - work in progress
- The HTTP forwarding modes
http_post_syncandhttp_post_asyncnow send the phoss-ap transaction ID in the new headerX-phoss-AP-Transaction-ID, next toX-SBDH-Instance-ID. Unlike the SBDH Instance ID it is unique per transaction, so the Receiver Backend no longer needs a REST lookup to correlate a forwarded document. The value stays the same for all retries and replays of a transaction. See #105
v0.13.1 - 2026-10-02
-
Fixed: outbound transactions failing in the SMP lookup were retried forever, ignoring
retry.sending.max-attempts. The maximum number of attempts was only checked for failures of the AS4 transmission. A transaction that never got past the SMP lookup - e.g. because the SMP is unreachable or signs its responses with an untrusted certificate - stayed in statusfailedwith an ever growingattempt_count. The same applied to transactions that hit an open circuit breaker for longer thancircuit-breaker.defer-max-duration: from then on the attempts were counted, but never limited. All of them now end aspermanently_failedonceretry.sending.max-attemptsis reached, andonOutboundPermanentSendingFailureis invoked. Unchanged: an MLS response whose (default SPID) receiver is not registered in the SMP is still retried without limit, as required by PNP rule MLS-4. -
The outbound transaction
error_detailsnow contain the real reason of a failed SMP lookup or AS4 transmission. Previously only the outermost exception message was stored, e.g.Failed to resolve SMP endpoint (...). Now the messages of all causes are appended as. Technical details: <SimpleClassName>: <message> - <SimpleClassName>: <message>, so that for example the untrusted SMP signing certificate is visible without looking into the sending report. -
The phorm document verification now uses the
phorm-clientlibrary instead of its own HTTP and JSON code.PhormDocumentVerifiertalked to the verification service with a hand writtenHttpPost, parsed the answer withJsonReaderand converted it withPhiveJsonHelper. All of that moved intocom.helger:phorm-client1.0.0, and the verifier keeps only what is specific to the AP: deciding whether a document goes to the regulardd_and_validateAPI or - for a Factur-X style non-XML payload - to the hybrid PDF API, extracting that PDF from the SBDHBinaryContentelement, and mapping the phive findings toVerificationIssues. Three defects of the hand written code are fixed by the move. A rejectedX-Token(HTTP 403) was reported as "Failed to parse Phorm response as JSON", because the check only ever tried to read the body as a phive result; it is now named as what it is. A 400 with atext/plainbody - what phorm answers for a payload that is not XML or an unresolvable VESID - lost that body entirely; it is now carried in the logged message. And the response charset was assumed instead of being taken from theContent-Typeheader. The verifier also distinguishes the three failure modes the client reports - a faulty request, an unavailable service and an unusable answer - which is what theEVerificationFailModehandling has always needed. A document is still never rejected because the verification service itself did not work. -
The verification service is no longer contacted with a fresh connection pool per document. Every call created and closed its own
HttpClientManager, so every single inbound and outbound document paid a full TCP and TLS handshake. A process widePhormClientis now shared by all verifications, in the same waySMPClientManageralready shares one for the SMP queries. It is rebuilt whenverification.phorm.urlorverification.phorm.tokenchanges, so a configuration change is never masked by a cached client. The document is streamed to phorm as before -phorm-clienttakes the payload as anIHasInputStream, so a large document is not read into memory to be sent. A document verifier may now implementAutoCloseableto release such a resource;APCoreMetaManager.shutdown ()closes every loaded verifier that does. Nothing changes for a verifier that does not. - Requires ddd 0.9.0 (was 0.8.10):
phorm-clientdepends on the newddd-modelartifact, which carriesDocumentDetailssince the ddd 0.9.0 split, so keeping 0.8.10 would put that class on the classpath twice.com.helger:ddditself is unchanged for its users. -
Updated to phase4 4.8.0, which contains security relevant fixes:
- Updated to WSS4J 4.0.2 - fixes CVE-2026-89238
- The Peppol inbound message handling now compares the AS4
CollaborationInfo/Service,CollaborationInfo/Service/@typeandCollaborationInfo/Actionagainst the Process Identifier and the Document Type Identifier of the SBDH, as mandated by section 4.6 of the Peppol AS4 profile. A mismatch currently only emits a warning - in one of the next phase4 versions it will be elevated to an error that rejects the message - The new phase4 configuration property
phase4.incoming.security.enforcepmode(defaultfalse) can be set in the AP configuration. If enabled, an incoming UserMessage must be signed and encrypted if the effective PMode leg defines a signature respectively an encryption algorithm - which the Peppol PMode does. Otherwise it is rejected withEBMS:0103(Policy Noncompliance) before it reaches the AP - Fixed the size of the encryption session key, which did not follow the configured encryption algorithm, so messages labelled as AES-256 were effectively encrypted with a 128-bit key. Peppol mandates AES-128-GCM, so the Peppol traffic of the AP was not affected
- The PMode setting
ReceptionAwareness.DuplicateDetectionis now honoured for incoming messages. The default behaviour is unchanged - The sending report now contains the details of the TLS connection to C3 as
connection(JSON) respectivelyConnection(XML), also in case of success
- Updated dependencies:
- peppol-commons 13.1.0 → 13.1.1
- AWS SDK 2.55.3 → 2.55.8
- Apache Commons Lang 3.20.0 → 3.21.0
- Flyway 13.7.0 → 13.8.1
- H2 2.5.250 → 2.5.252
- Apache MINA sshd-sftp 2.19.0 → 2.20.0
v0.13.0 - 2026-09-24
-
Fixed: starting with
otel.enabled=truefailed withIllegalStateException: GlobalOpenTelemetry.set has already been called(issue #102). The firstGlobalOpenTelemetry.get ()of a JVM permanently installs the official no-op instance as the global one, and every laterGlobalOpenTelemetry.set (...)then throws. Up to 0.12.0 the SDK was installed byOtelConfigon theApplicationStartedEvent- which fires after the context refresh - while the Flyway migration runs during the refresh, when the AS4 servlet bean is created, and is wrapped in a telemetry span byph-db-flyway. So the no-op was always registered first and the AP could not start at all with OpenTelemetry enabled.OtelConfigis replaced byOtelBootstrapInitializer, anApplicationContextInitializerregistered inMETA-INF/spring.factories. Those run after the environment is prepared but before the first bean is created, so the SDK is now in place before anything can take a span. The sameotel.enabledproperty and the same autoconfigure-based configuration apply - nothing changes for an existing deployment except that it starts. Resetting the global withGlobalOpenTelemetry.resetForTest ()is deliberately not used as the fix. It makes the startup succeed, but the ph-telemetry OTel bindings used to cache the tracer and the meter they resolved fromGlobalOpenTelemetryon first use, so every span would silently stay a no-op for the lifetime of the JVM - and on an agent-instrumented deployment it would discard the agent's own SDK. The other half of the defect was fixed in the library instead, in ph-telemetry 1.1.1:OtelTelemetryTracerSPIandOtelTelemetryMeterSPInow resolve viaGlobalOpenTelemetry.getOrNoop ()- the API that OpenTelemetry documents for instrumentation and that has no side effect on the global slot - and only cache the result once an instance is really registered. Taking a span before the SDK bootstrap can therefore no longer break that bootstrap, and no longer pins the no-op forever. The two fixes are independent: the library one makes an early span harmless, and the initializer makes it actually recorded. The SDK is now built withoutsetResultAsGlobal ()and registered in a separate, guarded step. If a global instance is already present - realistically only when the OpenTelemetry Java Agent is attached - the AP keeps it, shuts down the SDK it just built and logs a warning, instead of aborting the startup and leaking the built SDK. With the agent, setotel.enabled=false. New:OtelBootstrapInitializerwith the constantCONFIG_KEY_OTEL_ENABLED; removed:OtelConfig. -
The circuit breakers no longer suspend healthy SMPs, and a suspension no longer burns retry attempts.
Operators saw outbound transactions fail with
SMP access limited by Circuit Breaker 'smp$<host>', always for the same large SMPs, while the SMP operators saw no failed requests at all - and changing thecircuit-breaker.*properties had no visible effect. Four independent defects contributed to that. A permit acquired in the half-open state was never released if aRuntimeExceptionoccurred. Failsafe releases such a permit only inrecordSuccess/recordFailure; there is no timeout. With the defaultcircuit-breaker.half-open-max-attempts=1a single leak therefore left the circuit breaker stuck inHALF_OPENforever: every call rejected, no further state change event in the log, and no way out but a restart - which is also why changing the properties appeared to do nothing. All three usages - the SMP lookup, the AS4 sending and the inbound forwarding - now record exactly one result for every acquired permit on every code path, including the "no document forwarder configured" branch that returned without recording anything at all. "The receiver is not registered" counted as an SMP failure. EveryPhase4Exceptionfrom the lookup calledrecordFailure, so five lookups for unregistered receivers in a row suspended a large SMP for all of its participants. The newSmpLookupFailureClassifiertells a negative answer (HTTP 404, or no endpoint matching the process and transport profile) from a real infrastructure failure (connection failure, connect or read timeout, unknown host, HTTP 5xx, unparsable or badly signed response). A negative answer now records a success - the SMP answered, that is a valid answer - and reports the receiver as not registered. With the current phase4 version those two cases can only be recognised by their exception message, because both arrive as a cause-lessPhase4SMPExceptionwithretryFeasible=true; the string matching is isolated in the classifier, marked with a TODO and covered by tests using the exact current messages. An SMP timeout permanently failed the transaction. phase4 classifiesSMPClientSMPUnavailableExceptionandSMPClientHttpExceptionas "retry not feasible", because neither matches its list of retryable exceptions - so a read timeout against a busy SMP did not lead to a retry but straight topermanently_failed. The classifier now overrides the retry decision for both of the cases above: a negative answer is never retried against the same SMP, an infrastructure failure always is, independent of whatisRetryFeasible ()says. This also fixes the MLS SPOG section 5.4 fallback in both directions: an unreachable custom MLS_TO receiver now correctly falls back to the default SPID receiver, and a transient SMP problem no longer triggers that fallback. A rejection consumed a retry attempt. A lookup or a send that the circuit breaker rejects never reaches the remote system, but it still incrementedattempt_countand wrote a failed attempt row - so a transaction could reachpermanently_failedwithout a single real attempt, and the inflated count made the first real AS4 error permanent. The outbound direction now does what the inbound forwarder already did: no attempt row, unchangedattempt_count, andnext_retry_dtset tonow + max (remaining open duration, retry scheduler interval), so the retry happens as soon as the circuit breaker may grant a permit again. The rejection stays visible inerror_detailsand in the sending report. The new propertycircuit-breaker.defer-max-duration(default12h) caps this: an older transaction falls back to the regular attempt counting, so a permanently unreachable SMP or AP cannot defer a transaction forever. No database migration is needed -updateStatusAndRetryalready takes the attempt count as a parameter. -
Circuit breaker rejections now say what is wrong and for how long. Instead of
SMP access limited by Circuit Breaker 'smp$https://...', the log, the transactionerror_detailsand the sending report carry e.g.SMP access to 'https://smp.example.org' suspended by circuit breaker (state OPEN since 2026-09-14T10:12:03Z, 42s remaining) after 5 failures; last failure: SocketTimeoutException: Read timed out after 10000 ms.CircuitBreakerManagertracks per key when the circuit breaker was opened and what the last failure was; the newrecordFailure (String, Throwable)overload carries the cause (the old signature is kept), stored as the simple exception class name plus its message and truncated to 200 characters.onOpenmoved from INFO to WARN and includes the last failure cause;onHalfOpenandonClosestay at INFO. -
The failure thresholding can be made tolerant of short load peaks. Five consecutive failures - the default - are easily reached while an otherwise healthy SMP is under load. Three new optional properties are added, all unset by default so that nothing changes unless they are configured:
circuit-breaker.failure-executionsmeasures the threshold over the last N executions instead of requiring the failures to be consecutive,circuit-breaker.failure-periodmeasures it over a rolling time window, andcircuit-breaker.failure-rateturns the absolute count into a failure rate in percent. Note that the first two alone make the circuit breaker more sensitive, not less, precisely because the failures no longer have to be consecutive - only the failure rate tolerates isolated failures. A tolerant configuration iscircuit-breaker.failure-rate=50,circuit-breaker.failure-executions=20,circuit-breaker.failure-period=5m: open only if at least half of the last 20 or more executions within 5 minutes failed. An invalid combination is logged as an error and falls back to the consecutive-failure default, because a circuit breaker that cannot be built would take the whole sending down. -
The SMP timeouts are configurable, via the new
peppol.smp.timeout.connect(default5s) andpeppol.smp.timeout.response(default10s). Both defaults equal the values that were in effect before:SMPClientManagerbuilds the sharedHttpClientManagerfrom a plainSMPHttpClientSettings, and because that shared manager takes precedence over the per-client settings, theHttpClientSettingsdefaults were the only timeouts an SMP query ever used - there was no way to change them. Raising the response timeout is the direct lever against an SMP circuit breaker that opens because of a slow, but working, SMP. -
Circuit breakers are observable and resettable at runtime.
GET /api/ops/circuit-breakerslists every circuit breaker that was used at least once with its key, state, open-since timestamp, remaining delay, failure count and last failure cause;POST /api/ops/circuit-breakers/{circuitKey}/resetforgets a single one, so that the next call creates a new, closed one from the current configuration. That is also the way to apply a changedcircuit-breaker.*property without a restart, because the configuration is only read when a circuit breaker is created. Both are part of the existing API token protected Operations API. Two new counters, both tagged withphoss.ap.circuit_breaker.keyandphoss.ap.circuit_breaker.state:phoss.ap.circuit_breaker.rejectionsandphoss.ap.circuit_breaker.state_changes. The circuit breaker key is a deliberate exception to the "no high cardinality attributes on metrics" rule - it is bounded by the number of SMPs and remote APs this AP talks to, and without it the counters are not actionable. New: the code listECircuitBreakerState(closed/open/half_open), the recordCircuitBreakerInfo, the DTOCircuitBreakerResponse,CircuitBreakerManager.getAllInfos (),reset (String),getRemainingDelay (String)andrecordFailure (String, Throwable), the lookup stateCIRCUIT_OPEN, the propertiesAPConfigurationProperties.CIRCUIT_BREAKER_DEFER_MAX_DURATION,CIRCUIT_BREAKER_FAILURE_EXECUTIONS,CIRCUIT_BREAKER_FAILURE_PERIOD,CIRCUIT_BREAKER_FAILURE_RATE,PEPPOL_SMP_TIMEOUT_CONNECTandPEPPOL_SMP_TIMEOUT_RESPONSEwith the matchingAPCoreConfigaccessors, and the constantsCPhossAPOtel.METRIC_CIRCUIT_BREAKER_REJECTIONS,METRIC_CIRCUIT_BREAKER_STATE_CHANGES,ATTR_CIRCUIT_BREAKER_KEYandATTR_CIRCUIT_BREAKER_STATE. -
The positive MLS can now be triggered by the Receiver Backend instead of by the successful forwarding, via the new
mls.sending.triggerproperty (issue #76). For a C3 that forwards to a middleware in front of C4 - the normal deployment, not the exception - the AP has always equated "the forwarding HTTP call returned 2xx" with "the document was delivered", and sent the MLSAPat that moment. In such a setup that MLS states a fact that has not happened yet: the middleware accepted the document, the delivery to C4 is still pending, and a later failure between middleware and C4 can no longer be reported at all, because the MLS for that message is already closed. The party that knows when the document reached C4 is the backend, and the AP already offers exactly this handshake for Peppol Reporting viaPOST /api/inbound/report- only the MLS counterpart was missing. The property is deliberately separate frommls.type, which maps 1:1 onto the Peppol enumEPeppolMLSTypeand should keep doing so - the trigger is an implementation concern of this AP, not a Peppol concept.auto(default) sends the positive MLS immediately after the successful forwarding, byte for byte the behaviour of all versions before 0.13.0;apidefers it until the backend calls the new endpoint. Only the success path is deferred. The negative MLS (RE) of a failed inbound verification and the acknowledging MLS (AB) after exhausted forwarding retries are always sent automatically - in both cases the document never reached the backend, so the backend cannot report anything about it. Note that delaying the forwarding response until C4 confirms is not an alternative to this mode: the forwarding runs synchronously inside the AS4 request, so a pending HTTP response means no AS4 Receipt for C2 and therefore a retransmission. The new endpointPOST /api/mls/sendtakes the SBDH Instance Identifier plus the MLS status to report -AP,ABorRE, with an optional response text and any number of issues, which are mandatory forREand optional for the positive codes, since MLS allows line responses there as well. It is secured by the existing API token filter and documented in the OpenAPI output. The answers are200with the statussent(orrecorded, ifmls.type=FAILURE_ONLYsuppressed a positive MLS - unchanged semantics),400for an invalid body,404for an unknown SBDH Instance Identifier,409if an MLS was already determined for the transaction,422for an inbound MLS or MLR document and for a transaction that is not in the statusforwarded- the backend can only have an outcome for a document that actually reached it -500if the MLS could not be created and503ifmls.sending.enabled=false. The endpoint is not limited to the trigger modeapi: inautomode it works as well, as long as no MLS was determined yet, which is what lets a backend report anREfor a document it received but cannot process. The endpoint returns as soon as the outbound MLS transaction is persisted, and names its ID in the response message; the AS4 transmission and its retries then run in the background through the normal outbound path. That required splitting the MLS creation from the MLS sending: the newMlsHandler.createInboundResultMls (...)returns the newMlsCreationResult, andMlsHandler.sendCreatedMlsAsync (...)puts the transmission on the worker pool.MlsHandler.triggerSendingInboundResultMls (...)is unchanged in signature and behaviour - it is now the composition of both - so the three call sites in the receive path and their telemetry are untouched. A silent backend must not push the AP out of MLS-1 compliance - 99.5 % of all MLS responses within 20 minutes, measured from the reception of the business document - so the trigger modeapiadds a watchdog to the retry scheduler cycle: every transaction in statusforwardedwithout anmls_response_codewhoseas4_timestampis older thanmls.sending.api.timeoutis answered withmls.sending.api.timeout.code. The default timeout is5mand not the 15 minutes originally proposed in the issue, because the fallback MLS still has to be built, sent and possibly retried inside the remaining SLA budget; whoever raises it takes on that risk. The default fallback code isAB, the semantically correct one - the document was accepted, the delivery was never confirmed - andREis rejected as a value, because a rejection is a statement only the backend can make. The window is deliberately measured from the reception of the document and not from its forwarding, because that is where the MLS-1 clock starts. Anchoring oncompleted_dtwould add the forwarding duration on top of the SLA budget instead of consuming it from it - a document that only reached C4 after 15 minutes of forwarding retries would then be answered at minute 20 at the earliest, and no configuration of the timeout could prevent that. A slow forwarding now shortens the window of the backend, and in the extreme case the fallback goes out with the first cycle after the forwarding, which is the correct answer when there is no budget left. A transaction withmls.type=FAILURE_ONLYis excluded from the watchdog. It never gets a positive MLS at all, so the fallback would put nothing on the wire and only record a response code - which would then block theREthe backend may still want to report for it. WithFAILURE_ONLYthe backend therefore keeps its window open indefinitely, exactly as in the trigger modeauto. Exactly one MLS per business document is now enforced atomically. Five parties can want to answer the same inbound transaction - the receive path after a successful forwarding, the exhausted forwarding retries, the verification rejection, the watchdog and the Receiver Backend - and they all create their MLS through the same method, which now claims the MLS slot before it builds anything:UPDATE ... SET mls_response_code=? WHERE id=? AND mls_response_code IS NULL. The first writer wins and every later one is told that the MLS is already decided. A check of the current response code could not do this: the transaction the receive path works with is a snapshot read before the forwarding call, so an outcome the backend reported in the meantime is not visible in it. The case that made this necessary: the middleware receives and processes the document but the HTTP response is lost, the AP goes toforward_failedand schedules a retry, the backend reports the outcome through the endpoint, and the exhausted retries would then send a second, contradicting MLS. A claim whose creation then fails is given back, so a later attempt can still answer C2 - and a response code that already belongs to a real MLS outbound transaction can never be erased by that. Both the endpoint and the watchdog honour the 0.12.0 rule that a document forwarded despite a verification rejection gets exactly one MLS, namely theREof that rejection, and neither of them ever answers an inbound MLS or MLR. No database migration is needed - the existingstatus,completed_dt,mls_response_codeandverification_resultcolumns carry everything the watchdog selection needs - andGET /api/mls/missingalready lists the transactions waiting for an API triggered MLS, so the backlog is observable without a new endpoint. New: the code listEMlsSendingTrigger(auto/api), the constantsAPConfigurationProperties.MLS_SENDING_TRIGGER,MLS_SENDING_API_TIMEOUT(+_DEFAULT) andMLS_SENDING_API_TIMEOUT_CODE, the accessorsAPCoreConfig.getMlsSendingTrigger (),getMlsSendingApiTimeout ()andgetMlsSendingApiTimeoutResponseCode (), the DTOsMlsSendRequestandMlsSendIssue, the manager methodsIInboundTransactionManager.getAllForMlsApiTimeout (int, OffsetDateTime),claimMlsResponseCode (String, EPeppolMLSResponseCode)andreleaseMlsResponseCodeClaim (String), the recordMlsCreationResult- with the outcomealreadyDetermined (...)next tocreated (...),suppressed (...)andfailure (...)- and the scheduler telemetry namemls-fallbackon thephoss.ap.scheduler.cyclespan.InboundOrchestrator.isMlsSuppressedAfterRejection (IInboundTransaction)widened from package visibility topublic, because the endpoint and the watchdog need the same verdict as the receive path. -
Configured participant IDs can now be excluded from Peppol Reporting, via the new
peppol.reporting.exclude.participant-idsproperty (issue #73). Synthetic monitoring transactions - a daily automated end-to-end health check that is sent through the real Peppol network, with real document types and real participant IDs, so that SMP lookup, AS4 transport, validation and forwarding are all covered - ended up in thereportingdatabase like any other transaction, and therefore in the monthly TSR and EUSR. The property takes a comma separated list of participant identifiers, each of them either URI encoded (iso6523-actorid-upis::9915:test) or in the default scheme notation (9915:test). A transaction is excluded if the configured identifier matches the sender or the receiver, in the inbound as well as in the outbound direction, so a monitoring participant is treated the same no matter which side it is on. The comparison is done on parsed identifiers and therefore respects the scheme specific case sensitivity - it is not a string comparison. A matching transaction is not stored in the Peppol Reporting backend and gets the new reporting statusexcludedinstead ofreported, so that the skip stays visible in the transaction table and via the status API. Archival treatsexcludedexactly likereported, so such a transaction is archived normally. An excluded inbound transaction is closed withexcludedeven if no C4 country code could be determined - the country code is only needed for the EUSR entry that is not written anyway. Such a transaction therefore no longer shows up inGET /api/inbound/missing-c4-country-codeand no longer waits for an asynchronous C4 country code report before it can be archived. This is opt-in - nothing is excluded unless the property is set. The effective list is logged once at startup, and a value that cannot be parsed as a participant identifier is logged as an error once and ignored - meaning matching transactions are counted, which is the safe direction. Note: Peppol Network Reporting is meant to cover every transaction exchanged over the Peppol network, and a synthetic monitoring transaction is real network traffic. Excluding it is a deliberate deviation that the operator of the AP owns. New:APConfigurationProperties.PEPPOL_REPORTING_EXCLUDE_PARTICIPANT_IDS,APCoreConfig.getPeppolReportingExcludedParticipantIDs (),APPeppolReportingHelper.getAllExcludedParticipantIDs ()and the code list valueEReportingStatus.EXCLUDED. - Updated dependencies:
- ph-telemetry 1.1.0 → 1.1.1 (the OpenTelemetry bindings no longer claim the global
OpenTelemetryslot when a span is taken before the SDK bootstrap, and no longer cache the no-op tracer / meter forever - see issue #102 above)
- ph-telemetry 1.1.0 → 1.1.1 (the OpenTelemetry bindings no longer claim the global
v0.12.0 - 2026-09-02
-
Every verifier and every document forwarder now has its own ID (issue #80).
IDocumentVerifierandIDocumentForwarderextendIHasID <String>now, sogetID ()must be implemented by every verifier and every forwarder. Contrary to the existingIDocumentVerifier.getVerifierName ()- which is a human readable name that defaults to the local class name - the ID is a short, stable, machine readable identifier that is meant to end up in dashboards and alerting rules, so it should not change between releases. The IDs are checked for uniqueness at startup: verifier IDs must be unique over all inbound and all outbound verifiers, and forwarder provider IDs must be unique over all providers. A duplicate or an empty ID aborts the startup with anInitializationException. A verifier class that implements both the inbound and the outbound SPI - like the bundled phorm verifier - is loaded once per SPI and therefore contributes the same ID twice; that is explicitly not treated as a duplicate. The bundled verifier uses the IDphorm(PhormDocumentVerifier.VERIFIER_ID), and the built-in forwarders use the ID of theirEForwardingMode-http_post_sync,http_post_async,s3_link,sftpandfilesystem. A forwarder created by anIDocumentForwarderProviderSPIuses the ID of that provider. Incompatible change:IInboundDocumentVerifierSPIandIOutboundDocumentVerifierSPIare no longer functional interfaces, because of the second abstract method. That is irrelevant for real implementations - aServiceLoaderSPI must be a class with a public no-argument constructor anyway - but a lambda used in a test must become an anonymous class. - The IDs are included in the telemetry, via the two new attributes
phoss.ap.verifier.id(CPhossAPOtel.ATTR_VERIFIER_ID) andphoss.ap.forwarder.id(CPhossAPOtel.ATTR_FORWARDER_ID). The verifier ID is set on thephoss.ap.verificationspan of the inbound and of the outbound direction whenever a verifier rejected the document or was unavailable - a passed verification has no single responsible verifier and therefore no ID. It is carried there by the newverifierIDcomponent of theVerifierResultrecord, which is also available to everyIAPNotificationHandlerSPI. The forwarder ID is set on thephoss.ap.forwarder.dispatchspan, next to the existingphoss.ap.forwarder.type. The type keeps naming the kind of forwarder, so existing dashboards continue to work, whereas the ID identifies the concrete forwarder - which is what tells two forwarders provided via SPI apart. - A configured outbound HTTP proxy is now also installed as the JVM wide default
ProxySelectorat startup, via the new methodAPBasicConfig.installGlobalProxySelector (). Only the connections made through Apache HttpClient used the configured proxy so far. The certificate revocation check that the JSSE PKIX trust manager performs during a TLS handshake - SMP lookups as well as AS4 sending - downloads the CRL and contacts the OCSP responder through the JDK internalURLConnectioninstead, and that code path only honours the defaultProxySelector. In a network where outbound traffic is only possible through a proxy, the TLS handshake therefore stalled on the CRL download and eventually failed with aCertPathValidatorException, even though the proxy was configured correctly. The proxy is derived from the existinghttp.proxy.*properties, so no new configuration is needed. Nothing is installed if no proxy host and port are configured, or ifhttp.proxy.enabledis explicitlyfalse.http.proxy.nonProxyHostsis honoured with the same exact-host-name matching as for the Apache HttpClient connections, and ifhttp.proxy.usernameis set, the credentials are registered as the JVM defaultAuthenticatorfor proxy authentication requests only. - A document that failed the inbound verification can now optionally still be forwarded to C4, controlled by the new
verification.inbound.rejection-forwardingproperty (issue #82). A verification failure used to be a dead end: the document was never forwarded and only C2 learned about it via the negative MLS. The UAE Peppol DCTCE / TDD-TDS model needs the opposite - C3 must be able to act on an invoice that failed the content validation, by generating a Tax Data Summary instead of a Tax Data Document, which means the failed document has to reach C4 while the rejection stays recorded. The property is deliberately an enum and not a boolean, following theverification.verifier-fail-modeidiom, so the delivery strength is selectable without adding a second key later:none(default) never forwards the document and ends in statusrejected- byte-for-byte the pre-0.12.0 behaviour;best-effortdispatches a fire-and-forget copy to the primary and to all secondary forwarders and still ends in statusrejected, without a forwarding attempt row and without any retry;retryruns the document through the regular forwarding state machine including the retries, so the transaction ends inforwardedorpermanently_failedwhileverification_resultstaysrejected. This is opt-in - nothing changes unless the property is set. The mode applies to every rejection verdict, so also to a document rejected because a verifier was unavailable and the fail mode isclosed, and to one whoseverification.deferred.max-durationwas exceeded. In the forwarding modes C2 receives exactly one MLS, namely theREof the rejection: the positiveAP/ABafter a successful forwarding and theacknowledgingMLS on exhausted retries are both suppressed. The suppression is derived from the persistedverification_resultand not from a call parameter, soforwardDocument (...)keeps its signature and the retry scheduler and the operations endpoint behave exactly like the initial receive path. In moderetrya successfully forwarded rejected document is counted for Peppol Reporting, as any other forwarded document - the document was received over the Peppol network. Enabling that mode therefore moves the reported TSR figures upwards.POST /api/ops/inbound/{sbdhInstanceID}/reverify-and-forwardis deliberately not widened: it still only accepts transactions in statusverification_deferred, so a rejected document stays terminal for the operator API. In moderetryit ends inforwardedorpermanently_failedanyway, and a deliberate re-delivery is still possible via the replay endpoint. New code listEVerificationRejectionForwarding(none/best-effort/retry) with the constantAPConfigurationProperties.VERIFICATION_INBOUND_REJECTION_FORWARDINGand the accessorAPCoreConfig.getVerificationRejectionForwarding (). An unsupported value falls back tononewith a single warning, as for the fail mode. Thebest-effortdispatch emits the new OpenTelemetry spanphoss.ap.inbound.forward.rejected, one per forwarder, with the attributephoss.ap.forwarder.index- the same shape as the existing secondary forwarder span. -
A copy of every MLS this AP generates itself can now be forwarded to C4 (issue #85), controlled by the new
forwarding.mls-copy.enabledproperty. The asymmetry was easy to miss: an MLS received from another AP is forwarded to C4 like any other inbound document, but theAP/AB/REthat this AP sends back to C2 never left the AP. The payload has always existed on disk -MlsHandlerstores it as a bareApplicationResponseand links it viainbound_transaction.mls_outbound_transaction_id- it is exactly the input a Tax Data Summary builder needs, only the dispatch was missing. The sink is deliberately configured separately from the primary forwarder, because a reporting integration is normally not the same endpoint as the invoice inbox. It supports everyEForwardingModeincludingspiunder theforwarding.mls-copy.prefix; if noforwarding.mls-copy.modeis set, the whole primary forwarder configuration is reused, soforwarding.mls-copy.enabled=truealone is a valid setup. This is opt-in - disabled by default, with no configuration required and no log output at all in that case. The dispatch is fire-and-forget from the moment the outbound MLS transaction is created, so a failing sink affects neither the MLS sending to C2 nor the inbound transaction status, and is never retried. Note that the copy is therefore the MLS that was generated, not proof that C2 received it. The payload is the bareApplicationResponseand deliberately not wrapped in an SBDH: the AP never materializes an SBD itself - phase4 adds the envelope during the AS4 transmission - so wrapping would mean generating and storing a second document per MLS. A consumer that handles received and sent MLS documents alike distinguishes them byEForwardableKind(inbound-mlsversusoutbound-mls-copy). For the same reason no metadata JSON sidecar is written for a copy. New:APConfigurationProperties.FORWARDING_MLS_COPY_ENABLED/FORWARDING_MLS_COPY_MODE/FORWARDING_MLS_COPY_PREFIX,APCoreMetaManager.getMlsCopyForwarderOrNull (),ForwardableDocument.fromOutboundMlsCopy (IOutboundTransaction)and the OpenTelemetry spanphoss.ap.mls.forward.copy. -
The name under which a forwarded document is stored is configurable, for the
sftp, thefilesystemand thes3_linkforwarding (issue #90, PR #91). A downstream routing service that picks the documents up from a shared landing directory usually routes them by the receiver participant ID. Doing so meant opening every XML payload or reading the metadata sidecar, only because the name was hard-wired -yyyyMMddHHmmss_(Incoming ID)for SFTP, the SBDH Instance Identifier for the filesystem and for S3. The new propertiesforwarding.sftp.filename-pattern,forwarding.filesystem.filename-patternandforwarding.s3.filename-patternsupport the placeholders{datetime},{incoming-id},{sbdh-instance-id},{receiver-id},{receiver-value},{sender-id},{sender-value},{doctype-id}and{process-id}. Note that only the{name}syntax works:${name}is the variable syntax of the configuration itself - such a value is replaced before the forwarder ever sees it - and is therefore rejected at startup with a dedicated error message. The resolved value is the base name, so the.xmlof the document and the.jsonof the optional metadata sidecar are appended to it - and in theper-transactionfilesystem layout it is the name of the subdirectory. The*-valueplaceholders drop the identifier scheme, so everything up to and including the::, and keep the ICD -0151:35747532810and not35747532810, because the same number in a different ICD is a different participant. Every default is the previous layout of that forwarder, so the configuration of an existing installation keeps producing the same names. Every character of the resolved name that is not a letter, a digit,.,-or_is replaced with_, and leading dots are removed. That is deliberately stricter and, contrary to the general filename sanitization, independent of the operating system -:is illegal on Windows only - so that the same document is named the same everywhere, and so that a sender provided value like the SBDH Instance Identifier can never point outside of the target directory. Note: this sanitization now also applies to the filesystem and to the S3 forwarding, which passed the SBDH Instance Identifier through almost unchanged before - a document whose Instance Identifier contains e.g. a:or a/is therefore stored under a slightly different name than in v0.11.0. Names longer than 200 characters - 900 for an S3 object key - are truncated. Only fors3_linkthe literal part of the pattern may contain/, so that an object key can span "folders" like{receiver-value}/{datetime}_{incoming-id}. A placeholder value never contributes a path separator, no matter which forwarder. The pattern is validated at startup: an empty pattern, an unknown placeholder, unbalanced braces, a${name}and - except for S3 - a path separator abort the startup with a configuration error, instead of ending up in every name. A pattern that contains neither{incoming-id}nor{sbdh-instance-id}is accepted but logs a warning, because it cannot guarantee unique names; the SFTP and the S3 upload silently overwrite an existing file, and only the filesystem forwarder resolves a collision itself by appending a numeric suffix. New inphoss-ap-api, and therefore usable by a forwarder provided viaIDocumentForwarderProviderSPIas well: the classForwardingFilenamePattern- which parses, validates and resolves a pattern - and the enumEForwardingFilenamePlaceholder, where each constant carries its ID as it is written in the pattern plus the function that takes its value out of theIForwardableDocument. Adding a placeholder is therefore a single enum constant.isUniquePerDocument ()marks the placeholders that make a resolved name unique, which is what the warning above is derived from. PlusAPConfigurationProperties.FORWARDING_SFTP_FILENAME_PATTERN_DEFAULT,FORWARDING_FILESYSTEM_FILENAME_PATTERN_DEFAULTandFORWARDING_S3_FILENAME_PATTERN_DEFAULT. -
BREAKING — a document forwarder no longer receives an
IInboundTransaction, but the new neutralIForwardableDocument(issue #84). Forwarding was hard-wired to a received document, which blocks forwarding anything the AP generates itself - a copy of an outbound MLS, for instance, where the thing to forward is anIOutboundTransaction. The newphoss-ap-apirecordForwardableDocumentdescribes a document to forward independently of where it came from:id,kind,sbdhInstanceID,documentPath,documentSize,docTypeID,processID,senderID,receiverID,timestamp,localID,verificationResult,verificationDetailsandmetadataJson. The new code listEForwardableKind(inbound-document/inbound-mls/outbound-mls-copy) carries the one thing the document itself cannot tell the receiver: C4 can distinguish an MLS it received from an MLS this AP sent, which matters as soon as both land on the same endpoint.documentPathis deliberately absolute, so the same forwarder serves the inbound and the outbound storage root without having to know which one it is.metadataJsonis aSupplierand may benull, so the JSON sidecar of thes3,sftpandfilesystemmodes is only rendered by the forwarders that actually write one - as before, it is the very sameInboundTransactionResponse. Note that the supplier makes two independently built records neverequals (). The contract ofIDocumentForwarderis deliberately broken rather than kept compatible:forwardDocument (IForwardableDocument)is abstract, so a third-party forwarder that implements onlyforwardDocument (IInboundTransaction)no longer compiles. Adefaultthat silently does nothing for a self-generated document would have been the worse outcome - a compile error says exactly what needs attention. The adaptation itself is a one-liner, becauseForwardableDocument.fromInbound (IInboundTransaction)is public API: an existing forwarder keeps its body and only changes what it reads its values from, and any code that holds anIInboundTransactionturns it into anIForwardableDocumentwith that single call.forwardDocument (IInboundTransaction)therefore remains as adefaultmethod that does exactly this and delegates, so every caller stays untouched - only implementors have to act. The wiki has the full before/after porting table of the accessors. The four built-in forwardershttp,s3,sftpandfilesystemwere migrated; there is deliberately no functional change at all, the forwarded bytes and the sidecar content are identical. -
BREAKING — the verification outcome is now exposed to the forwarders and to the SPI callbacks (issue #83).
Once the verdict is persisted, a downstream consumer that builds a report from it - e.g. a Tax Data Summary for a rejected invoice - should not have to call the REST API a second time to learn why the document was rejected. Two SPI signatures therefore changed; both are breaking for external implementors, and there is deliberately no default-method fallback, because a silently unimplemented callback is worse than a compile error:
IAPNotificationHandlerSPI.onInboundVerificationRejection (...)gained theMlsOutcomeof the rejection - the response code plus the individual issues, i.e. the very same data that is sent to C2 as the negative MLS. It is nevernulland is also handed out for a document that receives no MLS at all (an MLR or an MLS itself), because it describes the verdict and not the message that was sent.IAPLifecycleEventSPI.onInboundDocumentForwarded (...)gained theEVerificationResult, which isnullif no verification was performed. A value ofrejectedmeans the document was forwarded despite its rejection, which only happens in the rejection forwarding moderetry. The verdict is available to every document forwarder, including a third-partyIDocumentForwarderProviderSPIimplementation, asverificationResult ()andverificationDetails ()on theIForwardableDocumentit is handed (see issue #84 above).HttpDocumentForwardernow sendsX-Verification-Result(passed,rejectedorunverified) next to the existingX-SBDH-Instance-ID, as soon as a verdict exists - the absence of the header means that the verification is disabled or still deferred, so C4 can tell the three cases apart without a lookup. The findings themselves are opt-in via the newforwarding.http.verification-detailsproperty: if enabled, they are sent as base64 encoded JSON inX-Verification-Details. A header value is size bounded, so if the issue list does not fit into 4096 characters, the trailing issues are dropped until it does - what remains is still a valid JSON array - andX-Verification-Details-Truncated: truemarks that case. The REST API stays the authoritative source for the complete list. New metricphoss.ap.inbound.verification.rejections.forwarded, a subset ofphoss.ap.inbound.forwarded, counting the documents that reached C4 despite their rejection. It only counts the moderetry, because abest-effortcopy never runs through the forwarding state machine. All in-tree implementations were adapted:APMetricsNotificationHandler,APMetricsLifecycleEventHandler,SafeNotificationHandler,SafeLifecycleEventHandler,APNotificationHandlerSentry,DemoNotificationHandlerSPIandDemoLifecycleEventSPI. -
A Factur-X / ZUGFeRD PDF is now verified with the phorm hybrid validation (issue #93).
Such a document was effectively unverifiable before: the bundled
PhormDocumentVerifieralways POSTed the stored document to/api/dd_and_validate/asapplication/xml, and that endpoint needs XML - its document type detection cannot look into aBinaryContentbusiness message and answers with an HTTP 400 and a plain text body. The AP could not read that as a validation result and reported the verifier as unavailable, so with a fail-closed or deferred configuration a perfectly fine Factur-X invoice was rejected or parked, and outbound sending of a PDF payload was blocked outright. The endpoint is now chosen per document: a document type ID with a non-XML syntax specific ID -urn:peppol:doctype:pdf+xmlof the French Factur-X document types - whose payload is a PDF goes to/api/hybrid_validate, everything else keeps going to/api/dd_and_validate/. phorm validates the PDF carrier (PDF/A-3 conformance and theBR-HYBRID-*rules via kaltblut) plus the invoice XML embedded in it and returns both layers in the very same phiveValidationResultList, soPhiveToVerificationMapper, the MLS projection and the REST API are unchanged. All three storage shapes are covered: the bare PDF of an outbound payload-only submission is streamed as-is, and the base64 encoded PDF of an inbound document as well as of an outbound prebuilt SBD is taken fromPeppolSBDHData.getBusinessMessageAsBinaryContent (). TheBinaryContentMIME type must beapplication/pdf. If the SBDH carries a syntactically valid C1 country code, it is passed on as thecountryquery parameter - that is what activates the country specificBR-HYBRID-DE-*/BR-HYBRID-FR-*rules. It is deliberately not filtered against the countries phorm knows today, so a country added there later works without a change here. An outbound payload-only PDF has no SBDH yet - phase4 only creates the envelope during the AS4 transmission - so no country is sent and phorm falls back to its default. Anything that is not understood - an unparseable SBD, a business message that is notBinaryContent, a different MIME type - falls back to/api/dd_and_validate/with a warning, so no document is ever silently validated with fewer rules than before. No configuration is needed and nothing changes for an XML document. Note that this needs a phorm 2.1.0 or newer, because/api/hybrid_validatedoes not exist in older versions. - Fixed: an inbound document was stuck forever when the forwarder circuit breaker was open.
No forwarding was attempted in that case, but the transaction status was left untouched - and neither the retry scheduler (which only looks at
forward_failed) nor the startup recovery (which only resetsforwarding) ever picked such a transaction up again, so C2 never received an MLS for it. The transaction is now set toforward_failedwith anext_retry_dtof oneretry.forwarding.initial-backoff, and the situation is logged as a warning. Noinbound_forwarding_attemptrow is written and theattempt_countstays unchanged, because nothing was actually tried - so an open circuit breaker no longer consumes the retry budget either. With the deferred verification this was even more visible: a document that passed its re-verification while the breaker was open stayed inverification_deferredwith anext_retry_dtin the past and was re-verified in every scheduler cycle, without ever reaching the maximum deferral duration. - Fixed: if no document forwarder was configured, the inbound transaction went to
permanently_failedsilently - without an MLS to C2, without theonInboundPermanentForwardingFailure (...)notification and without error details in the database. All three now happen, as for any other permanent forwarding failure. The reason is stored asNo document forwarder configured. - Fixed:
POST /api/ops/inbound/{sbdhInstanceID}/reverify-and-forwardaccepted transactions in any status and now answers409 Conflictfor anything butverification_deferred. Re-verifying an already forwarded document delivered it to the Receiver Backend a second time, and - because the endpoint really re-runs the verifiers - a verifier that had become reachable again could send an MLSREfor a document for which an MLSAPhad already been sent. A deliberate re-delivery is still possible via the replay endpoint. - Fixed: a Peppol Reporting item was created a second time when an already reported inbound transaction was forwarded again - e.g. via the replay API - which over-reported the received document to the Peppol network.
APPeppolReportingHelper.createInboundPeppolReportingItem (...)now returns success without storing anything, if the transaction is already in reporting statusreported. - Fixed: a completed inbound transaction kept the
next_retry_dtand theerror_detailsof the previous failure or deferral, so a successfully forwarded document still looked like it had a retry pending. Both columns are now cleared together with the status. - Fixed: a re-verification was scheduled a full
verification.deferred.retry-intervalbeyondverification.deferred.max-duration, so the final rejection could arrive one interval later than documented. The next re-verification is now clamped to the deadline. - An inbound document verifier that violates its
@NonNullcontract and returnsnullno longer produces aNullPointerExceptionthat ends up as an AS4 error towards C2. Such an outcome is treated as a passed verification, as the pre-0.12.0 API defined fornull. - The re-verification cycle of the retry scheduler now opens a
phoss.ap.scheduler.cycleOpenTelemetry span withphoss.ap.scheduler.name = reverify, like the inbound and the outbound retry cycles. It deliberately does not fireIAPLifecycleEventSPI.onRetrySchedulerCycle (...), because that callback only distinguishes inbound from outbound. - The verdict of the inbound document verification is now persisted in the two new
inbound_transactioncolumnsverification_resultandverification_details(Flyway migrationV5, mirrored into the archive table). Previously the verdict only existed as the transaction statusrejectedplus free text inerror_details. That conflated two different things: the status describes the lifecycle of a transaction and is overwritten by every forwarding attempt, and since this releaseupdateStatusCompleted (...)also clearserror_details- so a verdict could not survive a forwarding at all.verification_resultis one ofpassed,rejectedorunverified, orNULLif no verification was performed (yet).verification_detailscarries the findings as a JSON array ofVerificationIssueobjects - the transport-neutral form, so it is the same shape the outbound submit API returns and the validation errors are available without a second lookup. It is filled for any verdict: on a passed verification the findings are the warnings. A document forwarded underverification.verifier-fail-mode=openis now recorded asunverifiedand is therefore no longer indistinguishable from a properly verified one; previously only a log line mentioned it. Both values are exposed by the REST API on the inbound transaction resource. This is the groundwork for forwarding a rejected document to C4 (issue #82) - the status is then free to follow the forwarding state machine while the rejection stays recorded. -
BREAKING — outbound document verification no longer discards the validation detail (issue #74).
IOutboundDocumentVerifierSPI.verifyOutboundDocument (...)returnedESuccess, so a rejected outbound document produced nothing but the generic API answer"Failed to submit outbound transaction". The actual phive validation errors existed inside the verifier and were thrown away, which made automated error handling on the client side impossible. It now returns the sameVerificationOutcomeas the inbound counterpart. Migration:return ESuccess.SUCCESSbecomesreturn VerificationOutcome.passed (),return ESuccess.FAILUREbecomesreturn VerificationOutcome.rejected (sMessage, aIssues). -
BREAKING — a verifier no longer reports its findings as Peppol MLS data.
VerificationOutcomenow carries a list of the new transport-neutralVerificationIssueinstead of anMlsOutcome. MLS is how C3 answers C2 about a received document; it is meaningless for an outbound submission, and forcing phive results into it damaged them in several ways: the machine-readable rule ID was mashed into the human text as[PEPPOL-EN16931-R001] ..., severity and rule kind were crushed into a single status reason code (an XSD warning and an XSD error both becameSV), an unknown location became the MLS sentinel"NA", and every warning was dropped as soon as the document was valid. AVerificationIssuekeepslevel(error/warning),type(syntax/business_rule),code,locationanddescriptionapart. A verifier now states what is wrong with the document and the AP decides how to report it — as an MLS response to C2 for an inbound document, and as JSON to the submitter for an outbound one.MlsOutcomeitself is unchanged and is built from the issues via the newMlsOutcomeIssue.fromVerificationIssue (...); the resulting MLS wire format is identical to before, because the rule ID is prefixed back into the description. Migration:VerificationOutcome.rejected (MlsOutcome)becomesVerificationOutcome.rejected (sMessage, aIssues). The MLS-specificPhiveToMlsMapperbecamePhiveToVerificationMapper.toVerificationIssues (...). -
BREAKING — the outbound submit methods return
OutboundSubmitResultinstead of a nullableIOutboundTransaction.OutboundOrchestrator.submitRawDocument (...)andsubmitPrebuiltSBD (...)returnednullfor "a verifier rejected the document" and "the payload could not be parsed" and "an internal error occurred" — indistinguishable, which is why all three ended up as the same generic API message.OutboundSubmitResultseparates them and carries the verification detail. A verification rejection deliberately creates no outbound transaction: the document is never sent, so there is nothing to track, retry or report to the Peppol network. - The outbound submit endpoints now answer with a JSON object carrying
errorMessage,verificationPerformedandverificationIssuesinstead of a bare JSON string with a generic message.verificationPerformeddistinguishes "the document is invalid" from "a verifier could not make a verdict at all" — outbound verification has no fail mode, so an unreachable verifier still blocks the submission, but the submitter is now told which of the two happened. The HTTP status codes of the individual endpoints are unchanged, including the pre-existing inconsistency that/submitand/submit-s3answer422while/submit-sbdand/submit-autoanswer400. - Verification warnings on an accepted document are no longer dropped, in both directions.
Outbound they are reported back as
verificationWarningson the sending report; if there are none, the response is byte-identical to before. Inbound they are sent to C2 as line responses of the positive MLS — MLS allows details on theAPandABresponse codes as well, so a warning does not require a rejection.MlsOutcome.acceptance (...)andacknowledging (...)accept an issue list for this. Previously every warning was discarded as soon as the document was valid, and the aggregation across multiple verifiers dropped them too. - Fixed: the projection of verification findings onto MLS turned any finding into a rejection reason. A rejection is now only built from the findings if at least one of them has severity
error; a verifier that rejects while reporting warnings only gets a synthesized fatal line response naming it, with the warnings appended. This only matters for contradictory verifier results, but it also means a passed verification can never be turned into a rejection by accident. - The aggregated verification result — the outcome plus the name of the decisive verifier — is the new
VerifierResultrecord inphoss-ap-api, shared by both directions. It was an implementation detail nested insideInboundOrchestratorbefore. The MLS projection deliberately did not move with it and stays inInboundOrchestrator: MLS is how C3 answers C2 about a received document, so putting it on a type that the outbound path also uses would reintroduce exactly the layering problem this release removes. As a result the outbound rejection now names the verifier that objected — in the log, in theverifierNamefield of the submit error body and inonOutboundVerificationRejection (...), which carries theVerifierResult. Inbound already did this via the transaction error details and the MLS text. - The two verifier SPIs share the new base interface
IDocumentVerifier, which carriesgetVerifierName (). This is not a merge — the verification methods stay separate, because inbound and outbound differ in fail modes, deferral and MLS. It only means that a verifier implementing both (like the bundled phorm verifier) inherits one name method instead of two unrelated defaults. It carries noSPIsuffix on purpose: unlike its two sub-interfaces it is never loaded viaServiceLoader. -
BREAKING —
IInboundDocumentVerifierSPI.verifyInboundDocument (...)returns the newVerificationOutcomeinstead of@Nullable MlsOutcome.MlsOutcomeis the MLS response DTO; using it to say "I could not check this document" required aREJECTIONresponse code for something that is not a rejection. The new return type states the three cases explicitly:VerificationOutcome.passed (),VerificationOutcome.rejected (aMlsOutcome)(its issues still flow into the MLS response; there is also a message-only variant without MLS details) andVerificationOutcome.serviceUnavailable (sMessage)for "the verifier backend service was unavailable, so nothing was verified". The latter is deliberately not a rejection of the document any more - see the fail mode below. Migration:return nullbecomesreturn VerificationOutcome.passed (),return MlsOutcome.rejection (...)becomesreturn VerificationOutcome.rejected (MlsOutcome.rejection (...)).MlsOutcomeitself is unchanged. The SPI additionally got agetVerifierName ()method with a default implementation returning the local class name. That name is used in log messages, in the transactionerror_detailsand in the generated MLS response, so a verifier can now present a stable name independent of its class name. - Added a configurable fail mode for unavailable inbound document verifiers, controlled by the new
verification.verifier-fail-modeproperty:closed(default, unchanged behaviour) rejects the document and sends a negative MLS to C2;openforwards the unverified document to C4 and logs a warning per document (theonInboundVerificationAcceptedcallback is deliberately not fired, because nothing was verified);deferredparks the document in the new inbound statusverification_deferredand re-verifies it later, without sending an MLS in the meantime. All registered verifiers are now evaluated before the fail mode is applied, so an explicit rejection always wins over an unavailability - previously the first failing verifier ended the evaluation. Indeferredmode the re-verification runs in the regular retry scheduler cycle everyverification.deferred.retry-interval(default5m); afterverification.deferred.max-duration(default12h, relative to the reception of the document) the document is rejected anyway, so that C2 always gets a final answer. Once the verification passes, the processing continues exactly where it was interrupted: an incoming MLS is correlated with its outbound transaction, the document is forwarded to C4 and the positive MLS is sent to C2. The forwarding attempt count is never modified by a deferral - deferred verification and forwarding retries are counted and scheduled independently.POST /api/ops/inbound/{sbdhInstanceID}/reverify-and-forwardtriggers the same resume manually. Unlike the replay endpoint it does not look into the archive. New statusverification_deferred(no DB migration needed - thestatuscolumn is untyped text) is included inGET /api/inbound/in-processing. See #71 and #72 - thx @dmaus2018, and the Receiving Process wiki page. -
BREAKING — new
IAPNotificationHandlerSPI.onInboundVerificationDeferred (...)callback. It is deliberately not adefaultmethod, so every implementation must handle it - the SPI has other breaking changes in this version anyway. It carries the transaction ID, the SBDH Instance Identifier, the name of the verifier that made no verdict, the date and time of the next scheduled re-verification and the error details, and it is fired on every deferral, i.e. also on every unsuccessful re-verification. A deferred document is neither forwarded nor rejected and no MLS was sent to C2 yet, so this callback is the signal that a verifier needs operator attention - if nobody reacts withinverification.deferred.max-duration, the document is rejected and the existingonInboundVerificationRejection (...)fires. The bundled implementations were extended accordingly: the OpenTelemetry handler increments the newphoss.ap.inbound.verification.deferredcounter, the Sentry handler logs it as a warning (not an error - the document is not lost yet) and the demo extension logs it. - The built-in
PhormDocumentVerifiernow reports a failed phorm call as "service unavailable" instead of rejecting the document itself. With the default fail modeclosedthe outcome is unchanged - the document is rejected and the MLS keeps the status reason codeBUSINESS_RULE_VIOLATION_FATAL- but the new fail modes now work out of the box for the shipped verifier. phoss AP never sends the status reason codeFAILURE_OF_DELIVERY: the PNP reserves it for a permanent inability to deliver, and every case in which phoss AP cannot deliver is assumed to be resolvable later. A call that could not be made at all (phorm not reachable, misconfigured URL or token, unreadable payload) is reported as unavailable as well, so indeferredmode a configuration error also parks the affected documents - see the Receiving Process wiki page. - (Docker) The official image can now run as an arbitrary non-root UID, so it works in Kubernetes namespaces enforcing the
restrictedPodSecurity profile (runAsNonRoot/runAsUser) and on OpenShift./app.jarwas added from a remote URL and therefore had mode0600 root:root, so a non-root UID could not read it and startup failed withcp: cannot open '/app.jar' for reading: Permission denied- it is now added with--chmod=0444. Additionally/extand/var/phoss-ap/dataare now group owned by GID 0 with the group permissions mirroring the owner permissions, which is the group Kubernetes uses whenrunAsGroupis not set. The image still runs asrootby default, so existing deployments are unaffected. See #79 - thx @walles12 - (Docker) The image now sets
GLOBAL_DATAPATH=/var/phoss-ap/data/, so documents and AS4 dumps are written to the/var/phoss-ap/datavolume that the image declares. Previously theglobal.datapath=generated/default of the bundledapplication.propertieswas resolved relative to the working directory/, so everything ended up in/generatedinside the container - a location that is not covered by any volume and is therefore lost when the container is removed, and that is not writable for a non-root UID. Deployments that mounted/var/phoss-ap/databut did not setGLOBAL_DATAPATHthemselves should check the old location for data before upgrading. - Added a minimal root page (
index.html) to the web application, so that opening the AP base URL in a browser no longer results in a "Whitelabel Error Page". It briefly states that the AP has no user interface and links to/management/status,/actuator/healthand the OpenAPI specification at/openapi/v3/api-docs. - Improved the End User ID determination for Peppol Reporting (EUSR), so that a single End User addressed via different Peppol identifier schemes is no longer counted multiple times.
The End User ID (C1 when sending, C4 when receiving) is no longer the participant identifier as-is, but is now derived via the new
PeppolEndUserHelperof peppol-commons 12.8.1 (through the new methodAPPeppolReportingHelper.getEffectiveEndUserID (...)). It unifies the identifier and maps the Belgian VAT numbers (9925) onto the Belgian enterprise numbers (0208, leadingBEremoved), the German VAT numbers (9930) onto the German Electronic Business Addresses (0246) and the removed Finnish OVT identifiers (0037) onto the Finnish OVT codes (0216) - so9925:BE0123456789and0208:0123456789now count as one single End User. The unification uses the identifier factory selected viapeppol.identifier.mode, and deployments can add or remove mapping rules viaPeppolEndUserHelper. Note: End User IDs of previously stored reporting items are not migrated, so a month that was partially recorded with the old logic may still contain both variants. See phax/peppol-commons#80 and the Peppol Specifics wiki page. - Updated to peppol-commons 12.8.1
-
Breaking change: the method
InboundTransactionResponse.getAsJson ()was renamed toInboundTransactionResponse.toJson (). The old name matched the JavaBean getter pattern, so Jackson picked it up as an additionalasJsonproperty, recursed into the returned ph-jsonIJsonObjectand failed withCould not write JSON: Conversion from type java.lang.String to type java.lang.Byte failed!. Because of that all inbound read APIs returned HTTP 500 —GET /api/inbound/status/{sbdhInstanceID},GET /api/inbound/missing-c4-country-code,GET /api/inbound/missing-c4-country-code/{sbdhInstanceID},GET /api/ops/inbound/history,POST /api/ops/inbound/{sbdhInstanceID}/replayandGET /api/inbound/in-processing(the latter only as soon as it had rows, which made an idle AP look healthy). The@Schema (hidden = true)marker added in 0.11.0 only removed the derived property from the OpenAPI document, but not from the runtime JSON serialization. Outbound APIs were never affected, becauseOutboundTransactionResponsehas no such method. The JSON metadata sidecar files written by the filesystem, SFTP and S3 forwarders are unchanged. See #77 - thx @StefanKert - The API Specification now documents the actual JSON property names of the inbound and outbound transaction responses.
They are derived from the JavaBean getters, so the internal transaction ID is serialized as
ID(and not asid), the AS4 Message ID of an inbound transaction asAS4MessageID(and not asas4MessageID), and the two inbound duplicate flags asduplicateAS4andduplicateSBDH(and not asisDuplicateAS4andisDuplicateSBDH). The JSON metadata sidecar files written by the filesystem, SFTP and S3 forwarders are not affected and keep usingid,as4MessageID,isDuplicateAS4andisDuplicateSBDH. Additionally the documented response field lists were aligned with the fields the endpoints really return. The outbound submission endpoints, the inbound reporting endpoint and the Peppol Reporting endpoints now document their real response bodies and status codes — the outbound submissions return the phase4Phase4PeppolSendingReportas JSON and not aqueued/not_queuedstatus — and the/management/statusexample was completed withversion.ddd,database.type,sentry.enabled,otel.enabledanddns.config.servers. - The outbound submission APIs now reject incomplete SBDH parameters for non-XML document types with a
400instead of letting the transmission fail deep inside phase4. Document types whose syntax specific ID is not of the form<root NS>::<local name>— likeurn:peppol:doctype:pdf+xml, used by the French Factur-X and the eB2B hybrid document types — carry neither an XML root element namespace URI nor a local name, so phase4 can derive none of the SBDHStandard,TypeVersionandTypevalues from them, and it must be told explicitly that the payload is not XML. Submitting such a document type withoutpayloadMimeTypepreviously failed withFailed to parse payload InputStream to a DOM node, and omittingsbdhTypeVersionproduced anINVALID_PARAMETERSsending result with no exception attached at all — up to 0.10.3 both were additionally masked byA Peppol Reporting item can only be created AFTER sending.POST /api/outbound/submit/...andPOST /api/outbound/submit-s3now require all four ofpayloadMimeType,sbdhStandard,sbdhTypeVersionandsbdhTypefor such document types and name the missing one in the error message. Providing apayloadMimeTypefor an XML document type is still accepted, but now logs a warning. See #70 - thx @RemyMarin
v0.11.0 - 2026-08-05
- Fixed the SMP client caching, so that repeated lookups of the same receiver no longer query the SMP server for every single message.
Previously each outbound message created its own
CachingSMPClientReadOnlywith an instance local cache, so a cache hit was impossible. All SMP clients are now created by the new classSMPClientManager, which shares one process wide SMP response cache (SMPClientCacheof peppol-commons 12.6.2) and one process wide HTTP connection pool between all SMP clients, so an SMP query no longer needs to establish a new TLS connection either. The cache can be configured via the new propertiespeppol.smp.cache.enabled(defaulttrue),peppol.smp.cache.ttl(default15m) andpeppol.smp.cache.max-size(default1000entries per cache) - see Configuration Properties. Note: inbound receiver checks inpeppol.receiver-check.mode=smlstill create one SMP client per message inside phase4 and are therefore not cached yet. See #68 - Added a new set of Operations APIs below
/api/opsfor transaction history auditing, payload inspection and manual re-forwarding:GET /api/ops/inbound/historyandGET /api/ops/outbound/historyreturn the active transactions page-wise and newest first (offset/limitquery parameters, default0/50);GET /api/ops/inbound/sizeandGET /api/ops/outbound/sizereturn the number of active (non-archived) transactions;GET /api/ops/inbound/{sbdhInstanceID}/payloadandGET /api/ops/outbound/{sbdhInstanceID}/payloadreturn the stored raw document asapplication/octet-stream(looking into the archive table as well);POST /api/ops/inbound/{sbdhInstanceID}/replayre-forwards an existing inbound transaction through the regular forwarding pipeline. Backing this,IInboundTransactionManagerandIOutboundTransactionManagerreceived the new methodsgetAllTransactions (int nOffset, int nLimit)andgetTransactionCount (). The endpoints require theX-Tokenheader like all other/api/**endpoints, but they expose complete document payloads and can trigger duplicate deliveries to the Receiver Backend — see the API Specification and Security Considerations wiki pages. See #67 / #69 - thx @dmaus2018 - The generated OpenAPI document no longer contains the derived
asJsonproperty in the inbound transaction schema (InboundTransactionResponse.getAsJson ()is now marked as hidden). Additionally thesbdhInstanceIDpath variables of the inbound status and "missing C4 country code" endpoints are now named explicitly, so their binding no longer relies on the compiler's-parametersflag. - Implemented the MLS
MLS_TOfallback for SMP lookup, following the Peppol Service Provider Operational Guideline on MLS (SPOG, sections 5.1 and 5.4). When acting as C3, an incomingMLS_TOis now only accepted if its SPID Main ID correlates to the sending C2's Main ID (from the AP certificateCN); a non-correlatingMLS_TOis ignored (redirecting an MLS to a different Service Provider is not allowed) and the default SPID receiver is used. When a customMLS_TOreceiver cannot be resolved via SMP, the AP now fires the newIAPNotificationHandlerSPI.onSpecialMlsToNotReachable()notification and falls back to the default SPID receiver (0242:<Main ID>); if even the default SPID is not resolvable, the MLS is queued and retried (per PNP Rule MLS-4) instead of failing permanently. Retries reconstruct the same fallback automatically formls_responsetransactions. The OpenTelemetry metrics handler exposes a newphoss.ap.outbound.mls.special_to_not_reachablecounter. Regular business-document sending is unaffected (an unresolvable receiver remains a permanent failure). See #66 and the Message Level Status wiki page. - Enriched the
IAPLifecycleEventSPI.onInboundMLSCorrelated()lifecycle callback so integrations get the full context when an MLS is received. It now additionally provides the correlated outbound transaction ID, the resolvedEMlsReceptionStatus(RECEIVED_AP/RECEIVED_AB/RECEIVED_RE), the MLS document ID, and the AS4 reception timestamp. The OpenTelemetry metrics handler now also breaks thephoss.ap.inbound.mls.correlatedcounter and thephoss.ap.mls.roundtrip.durationhistogram down by the newphoss.ap.mls.reception_statusattribute. See #63. - Added three optional custom fields (
custom1,custom2,custom3, max 255 characters each) to outbound transactions. They can be supplied on all outbound submit endpoints (/api/outbound/submit/...,/submit-sbd,/submit-auto/...as query parameters;/submit-s3in the JSON body), are stored in theoutbound_transactiontable (and mirrored intooutbound_transaction_archive), and are returned by the outbound status APIs when set. Values longer than 255 characters are rejected with HTTP400. Existing database rows are filled withNULLvia a new Flyway migration (V4). See #64.
v0.10.4 - 2026-07-21
- The deprecation warning for a legacy
.msconfiguration key (inAPCoreConfigandAPDirSenderConfig) is now logged only once per key, instead of on every access. - Fixed outbound sending masking the real transport error with
Phase4PeppolException: A Peppol Reporting item can only be created AFTER sending.OutboundOrchestratorcalledcreatePeppolReportingItemAfterSending(...)unconditionally right after the AS4 send — on a failed send (e.g. a TLS handshake failure) that call threw, and the resulting exception overwrote the actual transport exception (captured inaCaughtSendingEx) in the sending report, so the sending report and logs only showed the reporting error instead of the underlyingPKIX path building failed/SSLHandshakeException. The Reporting item is now only created when sending actually succeeded, so failures surface the real transport exception and retry decisions are made on the real error. Affected both thePAYLOAD_ONLYandPREBUILT_SBDoutbound paths. See #58 - thx @ajay2280 - Added a startup check that the TLS trust store used for outbound HTTPS connections (SMP lookup and AS4 sending) actually contains trust anchors.
Since phase4 4.5.0 the AP HTTP client uses the JVM default trust store (honoring the
javax.net.ssl.trustStoresystem property); if that store is overridden with an empty or invalid file, every outbound SMP lookup and AS4 transmission fails withPKIX path building failed. The application now fails fast at startup with a clear message instead of failing silently on every transmission. See #58 -
Security fix: the API token check was skipped for every
/api/**request when the AP is deployed under a non-root servlet context path (server.servlet.context-path), leaving all API endpoints (outbound submit, status queries, MLS SLA reports, inbound reporting) unauthenticated in such deployments.ApiTokenFiltermatched the context-absolute request URI (getRequestURI(), e.g./ap/api/...) against/api/; it now matches the context-relative servlet path (getServletPath()), so the token check applies regardless of the context path. Root-context deployments were unaffected. See #62 - thx @karelkryda - Added a runtime extension mechanism that lets you add custom SPI implementations (notification handlers, verifiers, document forwarders, …) and additional JDBC drivers to a deployment without rebuilding the application jar.
The
phoss-ap-webappSpring Boot jar is now repackaged with theZIPlayout (using Spring Boot'sPropertiesLauncher), and the Docker image setsLOADER_PATH=/extand creates an/extdirectory whose contents are appended to the runtime classpath on startup. Any thin jar dropped into/ext— bind-mounted from the host or baked into a derived image — is discovered via the standardjava.util.ServiceLoader, with no further wiring. See the new Runtime Extensions wiki page. Addresses #60. - Added the new
phoss-ap-extension-demomodule — a minimal, logging-only example extension (implementingIAPLifecycleEventSPIandIAPNotificationHandlerSPI) with an exampleDockerfileandassemble.sh/run-docker-ph.shhelper scripts, as a copy-paste starting point for writing your own extension. - Added experimental support for IBM DB2 (LUW) as a persistence backend, selected via
phossap.jdbc.database-type=db2, including DB2 Flyway migration scripts. Because the IBMjccdriver is proprietary, it is not bundled: the driver and theflyway-database-db2dialect are supplied at runtime via the new/extextension mechanism (see Runtime Extensions), keeping the default distribution lean and Apache-licensed. Timestamp parameters are converted tojava.sql.Timestamponly when running on DB2 (whose JDBC driver rejectsOffsetDateTimeviasetObject()); PostgreSQL, MySQL and H2 continue to bindOffsetDateTimeunchanged, so their behavior is unaffected. The Peppol Reporting SQL backend also supports DB2 (sincepeppol-reporting4.2.0), so the reporting database may run on DB2 too. See #56 / #57 - thx @dmaus2018 - Added a generic custom document storage backend extension point. Set
storage.mode=spiand supply anIDocumentPayloadManagerProviderSPIimplementation (selected by thestorage.spi.idproperty, matched against the provider'sgetID()) to store document payloads in any backend — a relational database, an object store, a message queue, etc. The AP core makes no assumption about the storage technology; any technology-specific setup (schema, migrations, bucket provisioning) is the provider's own responsibility. The implementation is loaded via the/extextension mechanism, exactly like the other provider SPIs (see Runtime Extensions). See #59 - thx @dmaus2018
v0.10.3 - 2026-07-05
- Updated to ph-commons 12.3.2 - security improvements on XML processing and bug fixing in background cache eviction
- Updated to peppol-reporting 4.1.5 - exclude MLS from EUSR
v0.10.2 - 2026-07-02
- Added an outbound dev loopback mode for local/test environments. When
outbound.dev-loopback.enabled=trueandpeppol.stage=test, outbound sending skips the SMP/SML lookup and sends the generated AS4 message back to this AP's ownphase4.endpoint.address, using the AP's own AS4 certificate as the receiver certificate. Disabled by default, opt-in only, and ignored on the production stage — production behaviour is unchanged. See #49 - thx @fmarslan - HTTP forwarding now sends the SBDH Instance Identifier as the
X-SBDH-Instance-IDrequest header, so the Receiver Backend can obtain it without deserializing the SBD stream (relevant e.g. for French PPF e-reporting). See #51 - thx @shetty-tejas - SFTP forwarding can now optionally write a JSON metadata sidecar next to each uploaded SBD, mirroring the filesystem forwarder's metadata.
Enabled via the new
forwarding.sftp.write-metadataproperty (defaultfalse). The sidecar is written with the same base name as the SBD ({name}.json) and contains the full inbound transaction metadata (the same JSON as the inbound transaction REST API). - S3 forwarding can now optionally write a JSON metadata sidecar object next to each uploaded SBD, mirroring the SFTP and filesystem forwarders' metadata.
Enabled via the new
forwarding.s3.write-metadataproperty (defaultfalse). The sidecar uses the same object key as the SBD but with a.jsonextension (<key-prefix><sbdhInstanceID>.json) and the same content.
v0.10.1 - 2026-06-25
- Fixed Phorm call result handling to handle negative validations properly
- MLS messages are no longer using status reason code
FDbutABas we assume a temporary problem only - Filtering out Saxon "transformation warnings" when creating MLS errors
- Fixed potential issues with S3 connection under load. See #47 - thx @gaboull
v0.10.0 - 2026-06-22
- Added new
IAPNotificationHandlerSPI.onInboundDuplicateRejected(...)callback that is invoked when an inbound AS4 message is rejected because it was detected as a duplicate before an inbound transaction could be persisted. Carries the Peppol sender ID (C1), receiver ID (C4), document type ID, process ID, sender provider ID (C2, derived from the signing certificate common name), AS4 Message ID, SBDH Instance Identifier, thebIsDuplicateAS4/bIsDuplicateSBDHflags and the error detail sent back as the AS4 error. Declared as adefault-method, so existing in-tree and externalIAPNotificationHandlerSPIimplementations remain source- and binary-compatible. See #42 - thx @fmarslan -
BREAKING —
IInboundDocumentVerifierSPIsignature changed.verifyInboundDocumentnow returns@Nullable MlsOutcomeinstead of@NonNull ESuccess.null(or any non-REJECTIONoutcome) means the verifier passes; a non-nullMlsOutcomewith response codeREJECTIONrejects the document and itsMlsOutcomeIssueentries flow into the outbound MLS line responses. External implementations of this SPI must be updated; the in-treePhormDocumentVerifierhas been migrated. See #38. - Added
PhiveToMlsMapperinphoss-ap-validationthat maps a phiveValidationResultListto a detailedMlsOutcome. Inbound rejections now carry one MLS line response per failed phive entry instead of a single genericBV/NAline: XSD validation failures becomeSV(Syntax Violation); Schematron and other failures becomeBV(Business Rule Violation, fatal) orBW(Business Rule Warning) based on phive severity; each issue'serrorFieldcarries the phiveerrorFieldName(typically the failed assertion's XPath context) with fallback to the formatted error location orNA; the issue description prefixes the rule ID where available (e.g.[BR-CO-15] Sum mismatch). Warnings are dropped when no errors are present (acceptance MLS responses do not carry line issues). See Message Level Status — Verifier-driven issue mapping. - Fixed Peppol Reporting outbound
endUserIDbeing set to the receiver participant ID instead of the sender participant ID — affected both thePEPPOL_SBDHandPREBUILT_SBDoutbound paths inOutboundOrchestrator, so EUSR reports submitted by an AP that also sends documents contained the wrong end-user identifier. See #40 - thx @DmitriiKhudiakov - Added a machine-readable OpenAPI 3 specification of all REST endpoints, served by the running application at
GET /openapi/v3/api-docs(JSON) andGET /openapi/v3/api-docs.yaml(YAML). Generated at runtime by springdoc-openapi from controller annotations — no Swagger UI is bundled (only the raw spec is exposed). Each endpoint carries an operation summary/description, parameter descriptions with examples, and the documented success / 4xx / 5xx response codes. The referenced request/response DTOs (OutboundS3SubmitRequest,InboundTransactionResponse,OutboundTransactionResponse,MlsSlaReportResponse,MlsSlaEntryResponse,ReportResponse) carry field-level descriptions, examples, required-vs-optional markers and allowed-value lists for enum-like fields (status, MLS response codes, …) so generated clients reflect the real contract. Declares theX-TokenAPI-key security scheme on all/api/**endpoints so the auth requirement is visible to spec consumers and client generators;/management/**is unprotected. The base path is configurable viaspringdoc.api-docs.path(defaults to/openapi/v3/api-docs). - All sending reports, also in case of error, now at least contain the SBDH Instance Identifier and the country of C1 to make sure a later alignment is possible. See #44 - thx @Robcio35
- Not sending MLS on MLR messages
- Not placing
MLS_TYPEandMLS_TOSBDH parameters on MLR or MLS messages
v0.9.1 - 2026-05-29
- Replaced the in-tree tracing facade (
APTrace,IAPSpan,EAPSpanKind,IAPTracerSPIinphoss-ap-api) with the newph-telemetrysubmodule from ph-commons 12.2.6.phoss-ap-otelnow provides the tracer via theITelemetryTracerSPIservice-loader contract instead of the in-tree SPI. Inbound / outbound / forwarding / MLS / scheduler / reporting code now usescom.helger.telemetry.Telemetrydirectly — no behavioural change, but consumers that only depended on the SPI now depend on ph-commons instead ofphoss-ap-api. - Updated dependencies:
- Parent POM 3.0.3 → 3.0.5
- ph-commons 12.2.5 → 12.2.6 (new
ph-telemetrysubmodule providing a vendor-neutral tracing facade;EErrorLevel.SUCCESSdeprecated in favour ofINFORMATION) - ph-web 11.4.0 → 11.4.1 (proxy configuration via
http.proxy.host/http.proxy.portno longer requireshttp.proxy.enabled=true;http.proxy.enablednow only acts as an explicit kill-switch) - phase4 4.5.0 → 4.5.1 (improved backwards compatibility around the AP TLS truststore default change and clearer error messages on TLS / PKIX failures pointing at the migration guide)
- ddd 0.8.7 → 0.8.8 (added value-provider entries for the French CTC Extended-CTC-FR profile on
cii,ubl2-invoiceandubl2-creditnote; added support for the French CTC CDAR D22B syntax) - peppol-reporting 4.1.3 → 4.1.4
- AWS SDK 2.44.4 → 2.45.0
- Flyway 12.6.1 → 12.6.2
- JAXB 4.0.8 → 4.0.9
- OpenTelemetry Instrumentation 2.27.0 → 2.28.1
- Sentry 8.41.0 → 8.43.0
- Apache MINA sshd-sftp 2.17.1 → 2.18.0
- Added OkHttp 5.3.2 as a centrally managed dependency for the OpenTelemetry OTLP exporter runtime
v0.9.0 - 2026-05-17
- Added support for secondary forwarders — fire-and-forget fan-out of every successfully forwarded inbound document to any number of additional endpoints.
Useful for duplicating from a TEST AP to both a Dev and a QA backend, or for adding a filesystem archive alongside an HTTP forward.
Configured via indexed keys
forwarding.secondary.{n}.mode/forwarding.secondary.{n}.*(up to 10 secondaries). Each secondary may use any supported mode (http_post_*,s3_link,sftp,filesystem) independently of the primary. Secondaries run only after the primary succeeded, have no retries, no SLA, no effect on the inbound transaction status, MLS response or circuit breaker; failures are logged only. Each dispatch is wrapped in aphoss.ap.inbound.forward.secondaryOpenTelemetry span. See #23 and Forwarding Process — Secondary Forwarders. - Added optional OpenTelemetry integration in the new
phoss-ap-otelmodule — emits metrics (failure counters, lifecycle counters and SLA histograms for forwarding / sending / MLS round-trip / scheduler cycles) and distributed traces across the inbound, outbound, MLS, SMP, verification, reporting, scheduler and archival flows. Disabled by default; enabled by settingotel.enabled=trueand configuring the standard OTel environment variables (e.g.OTEL_EXPORTER_OTLP_ENDPOINT). Adds two new SPIs inphoss-ap-api:IAPLifecycleEventSPI(positive lifecycle events, complementing the existing failure-sideIAPNotificationHandlerSPI) andIAPTracerSPI(tracing facade behindAPTrace/IAPSpan/EAPSpanKind, so in-tree modules record spans without depending on the OpenTelemetry API jar). See #33 - AS4 incoming duplicate detection is now backed by the configured database (PostgreSQL, MySQL, H2).
Survives restarts and is shared across multiple AP instances.
Adds Flyway V2 migration creating the
as4_duplicate_itemtable. See #19 - CRL downloads now use the dedicated
Phase4PeppolCRLHttpClientSettings(pre-tuned timeouts) from phase4 - Added new configuration property
peppol.revocation.soft-fail(defaultfalse) to globally enable certificate revocation soft-fail mode — when set, certificate validation succeeds even if the CRL/OCSP status cannot be determined (e.g. responder unreachable due to a network error). Maps toCertificateRevocationCheckerDefaults.setAllowSoftFail(boolean). -
/api/outbound/submit-sbdnow also runs Phorm validation whenverification.outbound.enabled=true, matching the behavior of/api/outbound/submitand/api/outbound/submit-auto. See #21 - thx @roiLeo - Added optional cleanup scheduler that periodically deletes archived inbound and outbound transactions (database row + document file)
whose
completed_dtis older than the configured retention. Operates only on archive tables; requires archival to be enabled. Configured via new propertiescleanup.scheduler.enabled(defaultfalse),cleanup.scheduler.interval(default24h),cleanup.scheduler.retention(default90d, minimum2d), andcleanup.scheduler.batch-size(default100). Adds Flyway V3 migration creating indexes oncompleted_dtof the archive tables. See #20 - thx @thordw - Switched all time-related configuration properties to use the duration grammar (e.g.
5s,1m 30s,2d 12h). The legacy.ms/.milliskeys continue to be honored for backwards compatibility (with a deprecation warning) and will be removed in v1.0.0. See Configuration Properties for the duration grammar. Affected properties:retry.sending.initial-backoff(.ms)retry.sending.max-backoff(.ms)retry.forwarding.initial-backoff(.ms)retry.forwarding.max-backoff(.ms)retry.scheduler.interval(.ms)circuit-breaker.open-duration(.ms)archival.scheduler.interval(.ms)dirsender.scan-interval(.ms)dirsender.initial-delay(.ms)shutdown.timeout(.ms)phossap.jdbc.pooling.max-wait(.millis)phossap.jdbc.pooling.between-evictions-runs(.millis)phossap.jdbc.pooling.min-evictable-idle(.millis)phossap.jdbc.pooling.remove-abandoned-timeout(.millis)phossap.jdbc.execution-time-warning(.ms)
-
GET /api/outbound/status/{sbdhInstanceID}andGET /api/inbound/status/{sbdhInstanceID}now accept the optional query parameterincludeArchive=true(defaultfalse). When set, the corresponding archive table is consulted if the transaction is no longer present in the active table. Adds the new DB API methodsIOutboundTransactionManager.getBySbdhInstanceIDIncludingArchive(String)andIInboundTransactionManager.getBySbdhInstanceIDIncludingArchive(String). See #29 - thx @Robcio35 - Removed the obsolete
&serverTimezone=UTCparameter from the default MySQL JDBC URL inapplication.properties— the parameter has been a no-op since MySQL Connector/J 8.x and emits a deprecation warning. See #18 - Fixed missing
returnstatement inPeppolReportingControllerthat caused the success response of the manual report trigger endpoint to be dropped, so a successful run was reported as an internal server error. See #25 - thx @gkoszegi - Updated dependencies:
v0.2.3 - 2026-04-19
- Updated to BouncyCastle 1.84 (fixing CVE-2026-5588, CVE-2025-14813 and CVE-2026-5598)
- Made sure all time related activities use UTC timezone only
v0.2.2 - 2026-04-12
- Recommendation to use phorm 2.0.3+ as it support to extract payloads from SBDH payloads
- Added support for custom S3 endpoints and path-style access across all three S3 integration points (storage, outbound, forwarding), enabling use of S3-compatible providers like MinIO or Garage. See #16
- Added new configuration properties:
storage.s3.endpoint,storage.s3.path-style-access,outbound.s3.endpoint,outbound.s3.path-style-access,forwarding.s3.endpoint,forwarding.s3.path-style-access - Added H2 support for integration tests, including a Flyway migration script for H2. See #17
- Switched JAXB runtime from
com.sun.xml.bind:jaxb-impltoorg.glassfish.jaxb:jaxb-runtime(the maintained coordinates). Pinnedjaxb-implto4.0.6because4.0.7contains a regression. - Added new configuration property
phossap.flyway.history-tablefor customizing the Flyway schema history table name (from ph-db 8.1.3) - Added new configuration properties
phossap.flyway.debug-modeandphossap.flyway.repair-modefor Flyway diagnostics and history table repair (from ph-db 8.2.0) - Updated dependencies:
- peppol-commons 12.4.0 → 12.4.1 (eDEC Code Lists v9.6;
PeppolMLSBuildernow has a customizable identifier factory) - phive 12.0.1 → 12.0.3 (avoid NPE; avoid infinite ZIP size handling; avoid automatic owner document selection)
- ph-db 8.1.2 → 8.2.0 (customizable Flyway history table name; customizable JDBC pooling parameters; Flyway debug and repair modes)
- ddd 0.8.2 → 0.8.5 (support for new Peppol TDD formats; ability to capture inner element; wrappers in result details)
- peppol-reporting 4.1.2 → 4.1.3
- peppol-ap-support 2.1.2 → 2.1.3
- AWS SDK 2.42.22 → 2.42.32
- Flyway 12.2.0 → 12.3.0
- Jackson 3.1.0 → 3.1.1
- Log4j 2.25.3 → 2.25.4
- peppol-commons 12.4.0 → 12.4.1 (eDEC Code Lists v9.6;
v0.2.1 - 2026-04-01
- Added MySQL 8.0+ as a supported database backend alongside PostgreSQL, including Flyway migration scripts for MySQL and MySQL JDBC driver dependency (#14)
- Fixed phase4 and SMP Client not sharing the same configuration sources
- Fixed missing
MLS_TOandMLS_TYPEparameters when passing outbound transaction as params - Fixed using the correct
MLS_TOvalue from the inbound transaction - Added field
database.typeto the status data
v0.2.0 - 2026-03-30
- Added new REST endpoint
POST /api/outbound/submit-auto/{senderID}/{receiverID}/{c1CountryCode}for submitting raw XML documents with automatic document type and process detection via the DDD (Document Details Determinator) library — no need to specifydocTypeIDorprocessID - Added
DDDHelperutility in phoss-ap-core for DDD integration - Added new
phoss-ap-dirsendermodule — optional directory-based batch sender that watches a directory for pre-built SBD XML files, submits them via the normal outbound pipeline, and moves processed files tosuccess/orerror/subdirectories with JSON result files. Includes crash-safepending/intermediate directory with startup recovery and periodic cleanup. - Added new configuration properties:
dirsender.enabled,dirsender.directory,dirsender.scan-interval.ms,dirsender.initial-delay.ms - Added new forwarding mode
filesystem— writes received SBDs to a local directory with a metadata JSON file alongside each document. Supportsflatandper-transactionlayout modes viaforwarding.filesystem.layout. Atomic write pattern (.tmp→ rename) prevents partial file reads. - Added support for custom HTTP headers on forwarding requests via
forwarding.http.headers.{n}.name/.valueindexed configuration — useful for Bearer tokens, API keys, or custom metadata headers - AS4 message dumps now include a
metadata.jsonfile with incoming message metadata (remote address, timestamps, unique ID, etc.) — applies to bothdirectionandgroupeddump modes - Changed default document storage paths from
/var/phoss-ap/inboundand/var/phoss-ap/outboundto${user.home}/phoss-ap/inboundand${user.home}/phoss-ap/outbound - The management status endpoint now includes the DDD library version
- Added
dddlibrary dependency (v0.8.2) for document type auto-detection
v0.1.3 - 2026-03-29
- Added configurable C4 country code determination modes via
forwarding.c4countrycode.modes— supportsreceiver_pid(derive from receiver participant ID scheme) andbusiness_card(look up from Peppol Directory Business Card via SMP). Multiple modes can be chained as a fallback sequence. The asynchronous reporting API always remains available as the final fallback. - Added new REST endpoint
GET /api/inbound/missing-c4-country-codeto list all forwarded transactions still missing a C4 country code - Added new REST endpoint
GET /api/inbound/missing-c4-country-code/{sbdhInstanceID}to check if a specific transaction is missing a C4 country code (200 = missing, 204 = already set, 404 = not found) - Added
c4CountryCodefield to theInboundTransactionResponseDTO — previously omitted from the JSON response - Added management status endpoint
GET /management/statusreturning non-sensitive configuration, version information, and runtime metadata as JSON (#13) - Added new configuration property
peppol.receiver-check.modewith valuesnone(default),smp, andsmlto explicitly control receiver check behavior. Thesmlmode uses dynamic per-participant SMP resolution via the SML (#12, requires phase4 4.4.2+) - Added new configuration property
peppol.dns.serversto specify custom DNS servers for Peppol NAPTR lookups - Added new configuration property
management.status.enabled(defaulttrue) to disable the status endpoint - Changed default
retry.sending.initial-backoff.msfrom 60 seconds to 180 seconds (3 minutes) - Fixed
HttpDocumentForwardernot applying configuredHttpClientSettings(proxy, timeouts, TLS) — the settings were loaded from configuration but never passed to theHttpClientManager - Updated dependencies:
- phase4 4.4.1 → 4.4.2
v0.1.2 - 2026-03-27
- Added new configuration property
mls.sending.enabled(defaulttrue) to globally disable all outbound MLS sending — useful for testing or during initial rollout - Added
"retry": "none"support inhttp_post_syncforwarding responses — the Receiver Backend can now suppress retry attempts for permanently unprocessable documents - Added new configuration property
archival.scheduler.batch-size(default100) to control how many transactions are archived per scheduler cycle; values < 1 are rejected at startup - Added new configuration property
retry.scheduler.batch-size(default50) to control how many transactions are retried per scheduler cycle; values < 1 are rejected at startup - Added MLS correlation tracking: incoming MLS responses now record the inbound transaction ID (
mls_inbound_transaction_id) for full audit trail - The
peppol.receiving.enabled=falseflag is now enforced — incoming AS4 messages are rejected immediately when receiving is disabled - Added logging configuration template in
application.propertieswith commented-out options for file output, daily rotation and retention - Improved error diagnostics: AS4 private key loading failures now include the attempted key alias in the error message
- Improved
Phase4PeppolSendingReportto always include the AS4 sending result code, even on failure - Fixed NPE in
OutboundOrchestratorwhen AS4 sending returns a negative result code without an accompanying exception - Fixed NPE in
OutboundOrchestratorerror message construction when exception is null - Fixed test backend to use configured country code for
http_post_asyncforwarding responses instead of hardcodingAT(#10) - Moved all client-relevant REST API DTOs from
phoss-ap-webappto newcom.helger.phoss.ap.api.dtopackage inphoss-ap-api, so that API clients can depend onphoss-ap-apialone without pulling in the webapp or Spring Boot-
OutboundS3SubmitRequest— S3 submission request body -
OutboundTransactionResponse— outbound transaction status (includesfromDomain()) -
InboundTransactionResponse— inbound transaction status (includesfromDomain()) -
ReportResponse— inbound reporting response -
MlsSlaReportResponse(with nestedMlsSlaEntryResponse) — MLS SLA compliance report
-
- Updated dependencies:
- Upgraded Jackson from 2.x to 3.1.0 (new
tools.jackson.coregroupId) - Spring Boot 4.0.3 → 4.0.5
- phase4 4.3.2 → 4.4.1
- peppol-commons 12.3.12 → 12.4.0
- ph-web 11.2.4 → 11.2.5
- Flyway 12.1.0 → 12.2.0
- AWS SDK 2.42.13 → 2.42.22
- Sentry 8.35.0 → 8.37.1
- JAXB 4.0.6 → 4.0.7
- commons-compress 1.28.0, commons-lang3 3.20.0
- Imported parent POMs for
ph-db,phase4andpeppol-reportingfor centralized dependency management
- Upgraded Jackson from 2.x to 3.1.0 (new
v0.1.1 - 2026-03-18
- Added S3 as an alternative document storage backend via
storage.mode=s3(newDocumentPayloadManagerS3implementation ofIDocumentPayloadManager) - Document paths in the database are now opaque strings — either absolute filesystem paths or
s3://bucket/keyURIs depending on the configured storage mode - Added new
EStorageModeenum (FILE_SYSTEM,S3) for type-safe storage mode configuration - Added new REST endpoint
POST /api/outbound/submit-s3for submitting outbound documents by S3 reference instead of inlining the payload - Added new
OutboundS3SubmitRequestJSON DTO for the S3 submission endpoint - Added configuration properties:
storage.mode,storage.s3.*(bucket, region, access-key-id, secret-access-key),outbound.s3.*(enabled, bucket, region, access-key-id, secret-access-key) - Changed
IDocumentPayloadManager.renameFile()return type fromFiletoStringto support non-filesystem storage backends -
storage.inbound.pathandstorage.outbound.pathare now used by all storage modes as base path components - Added AWS S3 SDK dependency to
phoss-ap-basicmodule
v0.1.0 - 2026-03-17
- Initial release as open-source
- Requires Java 21 or later
- Based on Spring Boot 4.x
- Uses phase4 4.x for AS4 message exchange
- Uses peppol-commons 12.x for Peppol identifiers, SMP client and MLS
- Uses peppol-reporting 4.x for Peppol Reporting
- PostgreSQL is the required backend database, schema managed via Flyway 12.x
- Inbound AS4 document reception with duplicate detection on AS4 Message ID and SBDH Instance Identifier
- Outbound document sending with automatic SMP lookup and SBDH creation
- Configurable document forwarding to backend systems via HTTP POST, AWS S3 or SFTP
- Retry with exponential backoff and per-endpoint circuit breaker (via Failsafe 3.x) for both sending and forwarding
- Optional document validation via external phive Validation Service
- MLS (Message Level Status) support with integrated SLA-1 and SLA-2 reporting
- Peppol Reporting with scheduled EUSR and TSR submission
- Automatic archival of completed transactions
- REST API for outbound submission, status queries and inbound reporting
- API token authentication
- Multi-instance safe via PostgreSQL
SKIP LOCKEDfor concurrent job processing - (Docker) Multi-stage Dockerfile with health check on
/actuator/health - (Docker) Docker Compose setup with PostgreSQL included
- Optional Sentry integration for error tracking