-
Notifications
You must be signed in to change notification settings - Fork 18
Forwarding Process
After an inbound document is received and stored, the AP forwards it to the Receiver Backend (C4). The forwarding mode is selected once per AP instance via the forwarding.mode configuration property. Five modes are available, each with different trade-offs regarding delivery confirmation, infrastructure requirements, and reporting flow.
| Mode | Config value | Transport | Delivery confirmation | Reporting trigger |
|---|---|---|---|---|
| HTTP POST Async | http_post_async |
HTTP POST | Yes (HTTP 200) | Asynchronous callback |
| HTTP POST Sync | http_post_sync |
HTTP POST | Yes (HTTP 200 + JSON) | Synchronous (C4 country code in response) |
| S3 | s3_link |
Amazon S3 PutObject | No | Asynchronous callback |
| SFTP | sftp |
SFTP file upload | No | Asynchronous callback |
| Filesystem | filesystem |
Local file write | No | Asynchronous callback |
The delivery confirmation flag determines the MLS response code sent back to C2 on successful forwarding:
- With delivery confirmation (HTTP modes): MLS response code AP (Approved)
- Without delivery confirmation (S3, SFTP, Filesystem): MLS response code AB (Accepted Blind)
The SBD is POSTed to the Receiver Backend. The AP only checks for a successful HTTP status code (2xx). The response body is ignored.
-
Method:
POST -
URL: Configured via
forwarding.http.endpoint -
Content-Type:
application/xml - Body: The raw SBD XML (SBDH envelope + business document)
-
HTTP Status:
2xx(any successful status) - Body: Ignored
The Receiver Backend must call back to the AP's inbound reporting API to provide the C4 country code:
POST /api/inbound/report?sbdhInstanceID={id}&c4CountryCode={code}
with the X-Token header set to the configured API token. This triggers the Peppol Reporting record creation.
| Property | Required | Description |
|---|---|---|
forwarding.mode |
Yes | http_post_async |
forwarding.http.endpoint |
Yes | Target URL of the Receiver Backend |
The SBD is POSTed to the Receiver Backend. The response must contain a JSON object with the C4 country code, enabling immediate Peppol Reporting without a separate callback.
-
Method:
POST -
URL: Configured via
forwarding.http.endpoint -
Content-Type:
application/xml - Body: The raw SBD XML (SBDH envelope + business document)
-
HTTP Status:
2xx -
Content-Type:
application/json -
Body: A JSON object containing at minimum the
countryCodeC4field
{
"countryCodeC4": "DE"
}{
"countryCodeC4": "AT",
"documentId": "a4f7e2b1-8c3d-4e5f-9a6b-1c2d3e4f5a6b",
"status": "received"
}Only the countryCodeC4 and retry fields are read by the AP. Any additional fields are ignored.
If the Receiver Backend determines that a document cannot be processed and retrying would not help (e.g., invalid content, duplicate document, permanent rejection), it can return an HTTP 2xx response with "retry": "none" to prevent the AP from scheduling retry attempts:
{
"retry": "none",
"errorMessage": "Duplicate document — already processed"
}When the AP receives "retry": "none", the transaction immediately transitions to PERMANENTLY_FAILED without any retry, and an MLS rejection (RE) is sent back to C2. The optional errorMessage field is recorded as the failure reason.
| Field | Required | Description |
|---|---|---|
retry |
No | Set to "none" to suppress retries. If absent or any other value, normal retry behavior applies. |
errorMessage |
No | Human-readable reason for the failure. Recorded in the forwarding attempt details. |
If the Receiver Backend cannot process the document, it can also return an HTTP error status (4xx or 5xx). Optionally, a JSON body can be included:
{
"error-source": "Sbdh parsing",
"error-message": "Invalid SBDH envelope",
"status": "rejected"
}The AP treats any non-2xx status as a forwarding failure and schedules a retry. To prevent retries, use a 2xx response with "retry": "none" instead (see above).
Triggered immediately and automatically by the AP when the forwarding response contains a countryCodeC4 value. No separate callback is needed.
| Property | Required | Description |
|---|---|---|
forwarding.mode |
Yes | http_post_sync |
forwarding.http.endpoint |
Yes | Target URL of the Receiver Backend |
The SBD is uploaded to an Amazon S3 bucket. The Receiver Backend is expected to pick up documents from S3 independently (e.g., via S3 event notifications or polling).
-
Bucket: Configured via
forwarding.s3.bucket -
Key:
{key-prefix}{sbdhInstanceID}.xml(e.g.,inbound/550e8400-e29b-41d4-a716-446655440000.xml) -
Content-Type:
application/xml - Authentication: Either explicit access key + secret, or IAM role-based (if credentials are omitted)
The Receiver Backend must call back to the AP's inbound reporting API after processing:
POST /api/inbound/report?sbdhInstanceID={id}&c4CountryCode={code}
| Property | Required | Description |
|---|---|---|
forwarding.mode |
Yes | s3_link |
forwarding.s3.bucket |
Yes | S3 bucket name |
forwarding.s3.region |
Yes | AWS region (e.g., eu-central-1) |
forwarding.s3.access-key-id |
No | AWS access key. If empty, IAM role-based auth is used. |
forwarding.s3.secret-access-key |
No | AWS secret key. Paired with access-key-id. |
forwarding.s3.key-prefix |
No | Optional prefix for S3 keys (e.g., inbound/). A trailing / is auto-appended if missing. |
The SBD is uploaded to the Receiver Backend via SFTP. An atomic write pattern is used: the file is first written with a .tmp extension and renamed after the transfer completes, so the Receiver Backend never sees partial files.
-
Target directory: Configured via
forwarding.sftp.server-directory-upload -
Filename:
{yyyyMMddHHmmss}_{incomingID}.xml(e.g.,20260327143052_abc123.xml) -
Transfer pattern: Write to
filename.xml.tmp, then rename tofilename.xml
The Receiver Backend must call back to the AP's inbound reporting API after processing:
POST /api/inbound/report?sbdhInstanceID={id}&c4CountryCode={code}
| Property | Required | Description |
|---|---|---|
forwarding.mode |
Yes | sftp |
forwarding.sftp.host |
Yes | SFTP server hostname |
forwarding.sftp.port |
Yes | SFTP server port (typically 22) |
forwarding.sftp.username |
Yes | SFTP username |
forwarding.sftp.password |
Yes | SFTP password |
forwarding.sftp.server-directory-upload |
Yes | Remote directory for uploads |
The SBD is written to a local directory on the filesystem. The Receiver Backend is expected to pick up documents independently (e.g., via a file watcher or polling mechanism). A metadata JSON file is written alongside each SBD.
Two layout modes are available, selected via forwarding.filesystem.layout:
Flat layout (default):
{directory}/
├── 550e8400-e29b-41d4-a716-446655440000.xml
├── 550e8400-e29b-41d4-a716-446655440000.json
├── a593a0aa-6ff7-48b0-8906-5534fa5212e0.xml
└── a593a0aa-6ff7-48b0-8906-5534fa5212e0.json
Per-transaction layout:
{directory}/
├── 550e8400-e29b-41d4-a716-446655440000/
│ ├── sbd.xml
│ └── metadata.json
└── a593a0aa-6ff7-48b0-8906-5534fa5212e0/
├── sbd.xml
└── metadata.json
Files are written atomically: the SBD is first written with a .tmp extension and renamed to the final name after the transfer completes. This prevents the Receiver Backend from seeing partial files.
Filenames are based on the SBDH Instance Identifier. If a collision occurs (e.g., duplicate detection in store_and_flag mode), a numeric suffix is appended (-1, -2, ...) to guarantee uniqueness.
The Receiver Backend must call back to the AP's inbound reporting API after processing:
POST /api/inbound/report?sbdhInstanceID={id}&c4CountryCode={code}
| Property | Required | Description |
|---|---|---|
forwarding.mode |
Yes | filesystem |
forwarding.filesystem.directory |
Yes | Absolute path of the target directory |
forwarding.filesystem.layout |
No |
flat (default) or per-transaction
|
The C4 country code is required for Peppol Reporting (TSR/EUSR). In http_post_sync mode, the Receiver Backend provides it in the response. For all other modes (and as a fallback for http_post_sync), additional automatic determination modes can be configured via forwarding.c4countrycode.modes.
After a successful forwarding, the AP determines the C4 country code as follows:
-
Synchronous response — If the forwarding response contains a
countryCodeC4value (http_post_syncmode only), it is used immediately. -
Configured automatic modes — If no country code was obtained from step 1, the modes listed in
forwarding.c4countrycode.modesare tried in order:-
receiver_pid— Derives the country code from the receiver's Peppol Participant Identifier scheme. The first 4 digits of the receiver value (the ISO 6523 code) are mapped to a country code via the predefined participant identifier scheme registry. Only works for country-specific schemes (e.g.0007→ SE for Sweden). International schemes like0088(EAN/GTIN) do not yield a country code. -
business_card— Looks up the receiver's Peppol Directory Business Card from the SMP (resolved via SML). The country code of the first business entity is returned. Business Cards are cached for 1 hour. Requirespeppol.stageto be configured.
-
-
Asynchronous reporting API — If no country code was determined by the above, the Receiver Backend must provide it via the reporting API:
POST /api/inbound/report?sbdhInstanceID={id}&c4CountryCode={code}
When a country code is determined (from any source), the AP immediately stores it and creates the Peppol Reporting record.
# Try participant ID scheme first, then Business Card, with async API as final fallback
forwarding.c4countrycode.modes=receiver_pid,business_cardTwo API endpoints are available to monitor which transactions are still missing a C4 country code:
-
GET /api/inbound/missing-c4-country-code— Returns all forwarded transactions where the C4 country code has not yet been determined. -
GET /api/inbound/missing-c4-country-code/{sbdhInstanceID}— Returns the transaction details for a specific SBDH Instance ID (check thec4CountryCodefield —nullmeans missing).
Every forwarding attempt follows the same lifecycle regardless of mode:
RECEIVED ──> FORWARDING ──> FORWARDED (success)
│
└──> FORWARD_FAILED (retry scheduled)
│
└──> PERMANENTLY_FAILED (max retries exhausted)
- RECEIVED — Document stored, awaiting forwarding.
- FORWARDING — Forwarding attempt in progress.
- FORWARDED — Successfully delivered to Receiver Backend.
- FORWARD_FAILED — Attempt failed, retry scheduled with exponential backoff.
- PERMANENTLY_FAILED — Maximum retries exhausted. An MLS rejection (RE) with status reason FD (Failure of Delivery) is sent back to C2.
Each attempt (success or failure) is recorded in the inbound_forwarding_attempt table with:
-
attempt_dt— When the attempt occurred -
attempt_status—successorfailed -
error_code— Machine-readable error classification (null on success) -
error_details— Human-readable error description (null on success)
Each forwarding mode produces specific error codes on failure:
| Mode | Error code | Meaning |
|---|---|---|
| HTTP | http_status |
Non-2xx HTTP status code |
| HTTP | http_response_error |
Response body could not be parsed as JSON (sync mode) |
| HTTP | http_sync_no_retry |
Receiver returned "retry": "none" in sync response (no retry) |
| HTTP | http_io_error |
Network or I/O error during HTTP communication |
| HTTP | http_error |
Unexpected exception during HTTP forwarding |
| HTTP | http_configuration_error |
Invalid forwarding mode configuration |
| S3 | s3-error |
S3 SDK HTTP response indicates failure |
| S3 | s3_error |
Exception during S3 upload |
| SFTP | sftp_execution |
SFTP upload command failed |
| SFTP | sftp_error |
JSch connection or transmission error |
| SFTP | sftp_exception |
Unexpected exception during SFTP upload |
| General | forward_exception |
Internal exception caught by the orchestrator |
Failed forwarding attempts are retried with exponential backoff. A periodic scheduler checks for retry-eligible transactions.
| Property | Default | Description |
|---|---|---|
retry.forwarding.max-attempts |
3 |
Maximum number of forwarding attempts before permanent failure |
retry.forwarding.initial-backoff.ms |
60000 (1 min) |
Delay before the first retry |
retry.forwarding.backoff-multiplier |
2.0 |
Multiplier applied to the backoff for each subsequent retry |
retry.forwarding.max-backoff.ms |
3600000 (1 hour) |
Upper bound on retry delay |
retry.scheduler.interval.ms |
60000 (1 min) |
How often the scheduler checks for retry-eligible transactions |
The retry scheduler uses SELECT ... FOR UPDATE SKIP LOCKED to safely support multiple AP instances processing retries concurrently.
Forwarding is protected by a circuit breaker (phoss-ap-forwarder) that temporarily halts forwarding attempts when repeated failures are detected, preventing cascading failures.
| Property | Default | Description |
|---|---|---|
circuit-breaker.failure-threshold |
5 |
Consecutive failures before the circuit opens |
circuit-breaker.open-duration.ms |
60000 (1 min) |
How long the circuit stays open before allowing a test request |
circuit-breaker.half-open-max-attempts |
1 |
Number of test requests allowed in half-open state |
It is appreciated if you star the GitHub project if you like it.
Donation link: https://paypal.me/PhilipHelger
- Home
- News and noteworthy
- Running phoss AP
- Architecture Overview
- API Specification
- Configuration Properties
- Code Lists
- Database Design Notes
- Maven Module Structure
- Runtime Extensions
- OpenTelemetry Integration
- Security Considerations
- Peppol Specifics
- Testing Without Peppol Network
- Known Users
- Migrating from phase4-peppol-standalone
- Contributing