# Peppol Participant Lookup & Change Monitor (`automa-flow/peppol-network-reachability-monitor`) Actor

Check Peppol participant IDs in bulk against live SML/SMP metadata. Get network publication status, serving SMP and advertised document types. Monitor supplier publication and capability changes between runs, with explicit per-participant failures and API-ready results.

- **URL**: https://apify.com/automa-flow/peppol-network-reachability-monitor.md
- **Developed by:** [Vadim Bezrukov](https://apify.com/automa-flow) (community)
- **Categories:** Business, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.50 / 1,000 verified participant checks

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Peppol Participant Lookup & Change Monitor

Verify whether suppliers and customers are **actually published on the live Peppol
network**, see their current document-type capabilities, and detect
publication, SMP and capability changes over time.

Give it a list of Peppol participant identifiers. For each one it answers the
question your ERP actually needs answered before it enables e-invoice routing:

> Is this participant published on the Peppol network right now, which SMP
> serves it, and which document types does it currently advertise?

### Why not just check the Peppol Directory?

The Peppol Directory is a useful discovery tool, and this Actor is not a
criticism of it. But publication in the Directory is **voluntary and
asynchronous**, so Directory presence and network publication are two different
facts:

```text
Peppol Directory  ->  a company chose to list itself, at some point
Peppol SML/SMP    ->  current publication and advertised document types
```

Network discovery is a prerequisite for routing, not a delivery test. This Actor
does not fetch ServiceMetadata endpoints, validate Access Point certificates,
send invoices or confirm the recipient's business acceptance rules.

During the research for this Actor, participant `0192:930751340` was **listed in
the Peppol Directory** and returned **NXDOMAIN from the production SML** - it was
not published on the network at all. That is one participant out of a small
sample; it illustrates why Directory presence alone is not a routing check.

This Actor reads the network's own discovery chain instead:

```text
participant identifier
  -> SML DNS/NAPTR lookup      (is it published at all?)
  -> SMP base URL              (who serves it?)
  -> SMP ServiceGroup          (which document types are advertised?)
  -> normalized observation + fingerprint + change events
```

This covers the publication and ServiceGroup stages of network discovery,
not the complete Access Point delivery workflow.

Free tools such as [Peppol Lookup](https://peppol.org/tools-support/peppol-lookup-service/)
and [peppol.tools](https://www.peppol.tools/) are useful for manual checks or direct
API integrations. This Actor adds Apify-native batches, persisted change detection,
explicit per-item diagnostics and exportable datasets. It does not sell exclusive
access to Peppol metadata.

### What you get

If a process restarts after publication begins, it fails with
`PUBLICATION_REPLAY_BLOCKED` instead of replaying potentially charged rows.
Inspect that run's dataset and start a new run. Automatic continuation of a
partially published run is not supported; this is a conservative billing guard,
not an exactly-once delivery guarantee.

One row per participant, with a per-item status, so one bad identifier never
spoils the batch:

| `check_status` | Meaning |
| --- | --- |
| `SUCCESS` | The chain completed. The observation is trustworthy. |
| `NOT_FOUND` | The SML authoritatively has no record of this participant. |
| `PARTIAL` | Authoritative evidence exists but is incomplete or inconsistent. |
| `FAILED` | The check could not be completed. No claim is made either way. |

| `network_status` | Meaning |
| --- | --- |
| `PUBLISHED` | Reachable, and advertising at least one document type. |
| `PUBLISHED_NO_DOCUMENT_TYPES` | Reachable, but advertising nothing it can receive. |
| `NOT_PUBLISHED` | Not on the network. No Access Point can route to it. |
| `UNKNOWN` | This check established nothing. **Never read `UNKNOWN` as absence.** |

**A DNS timeout is never reported as `NOT_PUBLISHED`.** Neither is a SERVFAIL, an
SMP timeout, an SMP 5xx, or an HTTP 200 that turns out to be an error page. Those
are `FAILED`, with an error code and a `retryable` flag. This distinction is the
whole point of the Actor: a monitor that reports outages as unpublished
participants would have you disable routing for healthy suppliers.

### Sample input

```json
{
  "participants": [
    "9925:BE0848934496",
    "0192:930751340",
    "iso6523-actorid-upis::0208:0891323496"
  ],
  "network": "production",
  "monitorChanges": true,
  "emitUnchanged": true
}
```

Identifiers may be written as `9925:BE0848934496` or in full as
`iso6523-actorid-upis::9925:BE0848934496`. The four-digit prefix is the ISO 6523
scheme code (`9925` Belgian VAT, `0192` Norwegian organisation number, `0208`
Belgian company number, `0106` Dutch KvK, `0195` Singapore UEN, and so on).

A bare company number with no scheme code is **rejected, not guessed at** -
inferring the scheme would produce a confident answer about the wrong company.
Duplicates are removed before any lookup, so the same participant is never
charged twice, however it is spelled.

### Sample output

```json
{
  "participant_id": "iso6523-actorid-upis::9925:BE0848934496",
  "participant_scheme": "9925",
  "participant_value": "BE0848934496",
  "network": "production",
  "check_status": "SUCCESS",
  "network_status": "PUBLISHED",
  "smp_url": "https://smp.peppol.org",
  "smp_host": "smp.peppol.org",
  "document_types": [
    "busdox-docid-qns::urn:fdc:peppol:end-user-statistics-report:1.1::EndUserStatisticsReport##urn:fdc:peppol.eu:edec:trns:end-user-statistics-report:1.1::1.1",
    "busdox-docid-qns::urn:fdc:peppol:transaction-statistics-report:1.0::TransactionStatisticsReport##urn:fdc:peppol.eu:edec:trns:transaction-statistics-reporting:1.0::1.0"
  ],
  "document_type_count": 2,
  "changed": true,
  "change_types": ["FIRST_OBSERVATION"],
  "fingerprint": "sha256:...",
  "source": "Peppol SML/SMP",
  "source_url": "https://smp.peppol.org/iso6523-actorid-upis%3A%3A9925%3ABE0848934496",
  "lookup_domain": "3kbwcdjpjlznbhgy2qydzak2gvaadf323uwokpt44l6l4jm3mpyq.iso6523-actorid-upis.participant.sml.prod.tech.peppol.org",
  "source_checked_at": "2026-09-05T12:00:00Z",
  "scraped_at": "2026-09-05T12:00:00Z",
  "error_category": null,
  "error_message": null
}
```

`lookup_domain` is the exact DNS name that was queried, so you can reproduce any
answer by hand:

```bash
dig +short NAPTR 3kbwcdjpjlznbhgy2qydzak2gvaadf323uwokpt44l6l4jm3mpyq.iso6523-actorid-upis.participant.sml.prod.tech.peppol.org
```

For a participant that is not published, `source_url` is an RFC 4501 `dns:` URI
naming the lookup that was actually performed. The Actor never fabricates an
`https://` URL for a resource it did not fetch.

### Monitoring

With `monitorChanges` on (the default), each participant is compared against its
own last **successful** observation:

| Change type | Fires when |
| --- | --- |
| `FIRST_OBSERVATION` | No baseline yet. |
| `PUBLISHED` | Was verified not published, now published. |
| `UNPUBLISHED` | Was published, now verified not published. |
| `SMP_CHANGED` | Still published, but a different SMP now serves it. |
| `DOCUMENT_TYPE_ADDED` | New document types published. |
| `DOCUMENT_TYPE_REMOVED` | Document types withdrawn. |
| `NO_CHANGE` | Nothing customer-visible moved. |

Three guarantees make the change stream trustworthy:

- **A failed or partial check never overwrites a good baseline.** An SMP outage
  cannot manufacture an `UNPUBLISHED` event on the following run.
- **Changes come only from conclusive observations.** A `FAILED` row never
  produces a change event.
- **A mass-unpublish pattern is refused, not believed.** If most of the
  participants that were published at the last check suddenly come back
  unpublished, the run reports them as `FAILED`, flags the run summary, and
  preserves every baseline. An SML incident is far likelier than your whole
  supplier base leaving the network overnight.

Set `emitUnchanged` to `false` and a scheduled run returns changes plus diagnostic
rows for failed or partial checks. Verified unchanged checks are also charged and
remain accessible in the run's `UNCHANGED_CHECKS` Key-Value Store report. This
setting filters output, not payment. `RUN_SUMMARY.billableParticipants` counts
accepted checks across both outputs. A `PENDING` receipt after an interruption
does not prove a charge: reconcile it against the run's event counts. Do not
resurrect that run; start a fresh run to avoid replaying uncertain billing.
When `monitorChanges` is off, all results are returned regardless of `emitUnchanged`.

Baselines advance only after an observation is delivered (and accepted for charging
on pay-per-event runs). Reaching the charge limit preserves the previous baseline,
so the undelivered change can be returned on a later run.

State uses uniquely keyed KVS snapshots. Concurrent runs merge the latest successful
observation per participant and network; a completed write compacts only snapshots
it has already read. Storage operations therefore vary with overlapping runs.
Legacy state without a network prefix cannot be assigned safely to production or
test and is ignored; the next successful check establishes a fresh baseline.

#### Schedule it

Create a Task with your watchlist, then schedule it daily or weekly. The stored
baseline is keyed per participant and per network, so a schedule keeps working
unattended.

#### Webhook on changes

Set `emitUnchanged` to `false`, point an Apify webhook at `ACTOR.RUN.SUCCEEDED`,
and read the output. The `changes` view selects columns, not rows; filter
`changed == true` in your consumer for movements, and handle error rows separately.

```text
https://api.apify.com/v2/datasets/{datasetId}/items?view=changes&clean=true
```

#### API

```bash
curl -X POST "https://api.apify.com/v2/acts/<username>~peppol-network-reachability-monitor/run-sync-get-dataset-items?token=<APIFY_TOKEN>" \
  -H 'Content-Type: application/json' \
  -d '{"participants": ["9925:BE0848934496"], "network": "production"}'
```

Because every row carries a stable `source_id`, `scraped_at` and `fingerprint`,
runs from different weeks join to each other without any extra work.

### Use with AI agents through Apify MCP

#### Discover and inspect without an account

Use this Streamable HTTP MCP URL for anonymous Actor search and documentation:

```text
https://mcp.apify.com?tools=search-actors,fetch-actor-details
```

Ask your agent to search for `Peppol participant lookup`, then use
`fetch-actor-details` with `automa-flow/peppol-network-reachability-monitor`
to inspect its input, output and pricing. These tools do not run the Actor.

#### Run with your own Apify account

Expose the Actor as a typed tool using this separate URL:

```text
https://mcp.apify.com?tools=automa-flow/peppol-network-reachability-monitor
```

Connect using OAuth in a compatible client (recommended), or set the
`Authorization: Bearer <APIFY_TOKEN>` header using **your own** Apify API token
from your client's secret settings. Never put a token in the URL, a prompt or
version control. Runs are billed to the connected account at the listed price.

An anonymous HTTP 401 from this direct Actor URL, the default MCP URL or
`?tools=actors` is expected: those configurations include authenticated tools.
It does not mean the public Actor is missing from search. For anonymous discovery,
use the explicit read-only URL above. See [Apify MCP authentication and anonymous
access](https://docs.apify.com/integrations/mcp#anonymous-access).

Example prompt:

```text
Run automa-flow/peppol-network-reachability-monitor for participants
9925:BE0848934496 and 0192:930751340 on the production network. For each, say
whether it is published, which SMP serves it and how many document types it
advertises. Keep NOT_PUBLISHED strictly separate from FAILED, and tell me which
ones I should not enable Peppol routing for yet.
```

The Actor answers a technical reachability question; `PUBLISHED` is not a guarantee
of delivery, and nothing it returns is tax, accounting or legal advice.

### Use cases

Start from a saved example and replace its sample participant IDs:

- [Verify participants before configuring routing](https://apify.com/automa-flow/peppol-network-reachability-monitor/examples/verify-peppol-participants): one-off production lookup without updating monitoring baselines.
- [Monitor partner reachability changes](https://apify.com/automa-flow/peppol-network-reachability-monitor/examples/monitor-peppol-partner-changes): first observations establish baselines; later runs report changes and errors. Verified unchanged checks are still charged and saved in `UNCHANGED_CHECKS`.
- [Check the Peppol test network](https://apify.com/automa-flow/peppol-network-reachability-monitor/examples/check-peppol-test-network): integration checks with baselines separate from production.

These examples do not create schedules or notifications. Monitoring baselines
are shared across this Actor's tasks in your account, separately per network.

Other workflows:

- **Supplier onboarding** - confirm a supplier is reachable before enabling
  Peppol routing, instead of finding out from a rejected invoice.
- **Customer reachability checks** - verify a customer can receive the document
  type you intend to send.
- **ERP and e-invoicing integration QA** - assert that your own participants are
  published and advertising the right capabilities after a deployment.
- **Peppol migration monitoring** - watch a country or customer base move onto
  the network as mandates take effect.
- **Scheduled partner-master verification** - keep supplier master data honest.
- **An agent or API tool** for "can this participant receive Peppol documents?"

### Pricing

Pay **$0.0015 per check ($1.50 per 1,000 verified participants)**, charged
**once per unique participant per run** that receives a
trustworthy terminal answer:

| Billed | Not billed |
| --- | --- |
| `SUCCESS` / `PUBLISHED` | Invalid identifiers |
| `SUCCESS` / `PUBLISHED_NO_DOCUMENT_TYPES` | Duplicates |
| `NOT_FOUND` / `NOT_PUBLISHED` | `PARTIAL` and `FAILED` results |
| | DNS timeouts, SERVFAILs, SMP timeouts, SMP 5xx |
| | Retries |
| | Participants held back by the mass-failure guard |
| Verified unchanged observations, including `emitUnchanged=false` | Unfunded checks |

A verified `NOT_PUBLISHED` is billed because establishing it takes the same
authoritative lookup as a positive answer - and knowing that a supplier *cannot*
receive Peppol documents is the answer you came for.

You are never charged for the Actor's own retries or internal requests.
Platform usage is included. There is no startup or additional dataset-item fee.
Set a maximum cost per run; the minimum selectable cap is $0.0015, not a minimum
charge. Your available Apify credit may lower the effective cap. Unfunded results
become free `MAX_TOTAL_CHARGE_REACHED` diagnostics and do not advance the baseline.
The cap also bounds source work: if it covers only ten checks, only the first
ten unique inputs are attempted; later inputs receive billing diagnostics.
Source failures within that reserved batch are free but do not trigger backfill.
Use a cap covering your complete watchlist for comprehensive monitoring.

Examples: 10 verified checks cost $0.015; 100 cost $0.15; 1,000 cost $1.50.
Checking 1,000 participants daily for 30 days costs at most $45, even when none
change; hourly checks for 30 days cost at most $1,080. Prefer daily batches unless
your workflow needs a shorter interval. Every scheduled run has its own cap.
Free lookup APIs and cheaper Directory exports exist; this Actor adds Apify-native
batch status, persisted change detection and a consistent integration contract.

### Performance

Hosted on Apify on 2026-09-05, build 0.1.2, default 512 MB:

| Participants | Run time | Peak process RAM |
| --- | ---: | ---: |
| 1 | 6.2 s | 59.1 MiB |
| 10 | 20.9 s | 64.0 MiB |
| 100 | 25.0 s | 65.9 MiB |
| 1,000 | 73.0 s | 136.8 MiB |

The 1,000-participant run touched **51 distinct SMP providers** and made 974
SMP requests, with no source retries. Available account credit limited it to
884 paid results; 114 billing-limit diagnostics and two rejected SMP URLs were
free. Runtime varies by provider and network; these are observations, not an SLA.
No browser or proxy was used. Historical local `benchmark.json` measured Python
allocations with tracemalloc, not hosted process RAM; it is not a capacity claim.

### Limitations

- **`PUBLISHED` does not guarantee end-to-end invoice delivery.** It means the
  participant is discoverable and its SMP advertises the capability. The
  receiving Access Point, the participant's own systems and the document's
  business validity are all outside what any discovery lookup can see.
- **`NOT_PUBLISHED` means "not discoverable through the validated production
  lookup path at check time"**, not that the company does not exist, is not a
  Peppol user, or cannot be reached another way.
- This is a point-in-time observation. The network changes; re-run to re-check.
- MVP reads the ServiceGroup only. Per-document-type endpoint metadata,
  certificates and process identifiers are not collected.
- The Actor does not resolve a company name or a bare registration number to a
  participant identifier. You supply the identifier.
- Peppol Directory business cards - company names, addresses, contact people -
  are deliberately **not** collected. Reachability monitoring does not need them.

### Source and freshness

Data comes from the Peppol network's public discovery infrastructure:

- the Service Metadata Locator, production zone
  `participant.sml.prod.tech.peppol.org` (and the test zone
  `participant.sml.test.tech.peppol.org`);
- the participant's own Service Metadata Publisher, over its standard anonymous
  read-only REST binding.

Every check queries the discovery infrastructure instead of returning a stored
baseline as a fresh result. DNS resolver caches and source-side propagation can
delay visible changes; `source_checked_at` records the check time, not a guarantee
of instantaneous global consistency.

### Legal and affiliation

- This Actor is **independent and not affiliated with OpenPeppol**, and is not
  certified or endorsed by OpenPeppol or any Peppol Authority.
- It observes public Peppol network discovery and service-metadata
  infrastructure, using the same anonymous lookups any Access Point performs. No
  account, no Peppol certificate, no authentication and no access control is
  involved.
- Results describe **technical publication and capability state at observation
  time**.
- The Actor sends no Peppol documents, uses no management APIs, and performs no
  writes to the SML or any SMP.
- Participant identifiers are processed only as you supply them. They are never
  enumerated, discovered or enriched.
- This is **not tax, accounting or legal advice**.

# Actor input Schema

## `participants` (type: `array`):

One identifier per line, written either as 9925:BE0848934496 or in full as iso6523-actorid-upis::9925:BE0848934496. The four-digit prefix is the ISO 6523 scheme code (9925 Belgian VAT, 0192 Norwegian organisation number, 0208 Belgian company number, 0106 Dutch KvK, and so on). A bare company number is rejected rather than guessed at. Duplicates are removed before any lookup, so the same participant is never charged twice. Up to 1,000 per run.

## `network` (type: `string`):

Which Peppol network to look the participants up on. Production is the live network your invoices actually travel over. Test is the Peppol test network, for integration work. Baselines for change detection are kept separately per network.

## `monitorChanges` (type: `boolean`):

Compare each participant against its last successful check and report FIRST\_OBSERVATION, PUBLISHED, UNPUBLISHED, SMP\_CHANGED, DOCUMENT\_TYPE\_ADDED and DOCUMENT\_TYPE\_REMOVED. A failed or partial check never overwrites a good baseline, so an SMP outage cannot manufacture an unpublish event. Turn it off for a one-off lookup that stores nothing.

## `emitUnchanged` (type: `boolean`):

When off, the default Dataset contains changes plus error diagnostics. Verified unchanged checks are still charged and saved in the run's UNCHANGED\_CHECKS report. This is an output filter, not a billing discount. Funded successful checks preserve the baseline; failures and unfunded checks never overwrite it.

## Actor input object example

```json
{
  "participants": [
    "9925:BE0848934496",
    "0192:930280070",
    "iso6523-actorid-upis::0208:0891323496"
  ],
  "network": "production",
  "monitorChanges": true,
  "emitUnchanged": true
}
```

# Actor output Schema

## `results` (type: `string`):

Participant observations and diagnostics; changes-only mode moves verified unchanged observations to the separate report.

## `changes` (type: `string`):

Publication, SMP and capability movements, ready for a webhook consumer.

## `published` (type: `string`):

Who is reachable and what they can receive; filter network\_status client-side.

## `diagnostics` (type: `string`):

Failure-focused projection; filter check\_status client-side.

## `unchangedChecks` (type: `string`):

Available when changes-only mode suppresses observations. SETTLED includes accepted checks; PENDING after interruption requires reconciliation with run event counts.

## `runSummary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "participants": [
        "9925:BE0848934496"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("automa-flow/peppol-network-reachability-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "participants": ["9925:BE0848934496"] }

# Run the Actor and wait for it to finish
run = client.actor("automa-flow/peppol-network-reachability-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "participants": [
    "9925:BE0848934496"
  ]
}' |
apify call automa-flow/peppol-network-reachability-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automa-flow/peppol-network-reachability-monitor"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/7jh5AwuGNGbHuBdjK/builds/15MpdRd8mgwTNdMec/openapi.json
