# Belgian E-Invoicing Change Monitor: Peppol & Software (`codeclouds/be-einvoice-change-monitor`) Actor

Monitors the official Belgian B2B e-invoicing sources and reports what changed: official news and FAQ pages, the FPS Finance list of compliant software, and Peppol certified service providers — with dates, change type and source provenance.

- **URL**: https://apify.com/codeclouds/be-einvoice-change-monitor.md
- **Developed by:** [Dennis](https://apify.com/codeclouds) (community)
- **Categories:** Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 document records

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Belgian E-Invoicing Change Monitor (e-invoice, Peppol, compliant software)

Since **1 January 2026**, every VAT-liable Belgian enterprise must send, receive and process **structured B2B e-invoices** (law of 6 February 2024). That single obligation created three official sources that finance teams, accountants and ERP integrators now have to follow — and this Actor watches all three for you:

1. **The official portal** `einvoice.belgium.be` (FPS BOSA) — news items, every FAQ subpage and the software guidance article, in Dutch, French and English.
2. **The official compliant-software list** — the FPS Finance XLSX with 469 registered products: vendor, VAT number, whether the product can **send / receive / process** structured invoices and the date from which that holds.
3. **The Peppol certified service-provider register** (`peppol.org`) — 726 providers with their Access Point and Service Metadata Publisher status and the responsible Peppol authority.
   Instead of "another page dump", every record tells you **what changed, since when, and why** — with the source date, the exact field that changed, a content hash, and a short literal excerpt as evidence. Run it once to get the full picture, or schedule it with `alleenWijzigingen: true` to receive only what is new or modified.

### When should an AI agent use this?

- "What changed on the official Belgian e-invoicing portal since our last run?"
- "Which software products were **newly registered** as compliant for Belgian e-invoicing?"
- "Is there Belgian e-invoicing software that can **receive but not send**, and from which date?"
- "Did the FPS change any `send as from` / `receive as from` / `treat as from` date for a vendor?"
- "Which vendor dropped a capability (e.g. `kanVerwerken` went from true to false)?"
- "Are there new **Peppol certified service providers for Belgium**?"
- "Build me a compliance timeline for the Belgian B2B e-invoicing obligation: dates + source per milestone."
- "Which compliant products support Dutch, French **and** English?"

### What this Actor does

- **Follows the official portal per language** (`nl`, `fr`, `en`): the news list, all FAQ subpages and the software-solutions article. Each page yields title, source date (`<time datetime>` from the CMS), normalised body text (hash input) and an effective date when the source itself states one. Note that the portal spans two official domains — most pages live on `einvoice.belgium.be`, the Dutch FAQ pages on `efactuur.belgium.be` — and every record reports the domain it actually came from in `bron`.
- **Reads the official XLSX** without a spreadsheet service: it resolves the file through the portal's own link (the filename carries a date, e.g. `…-26062026.xlsx`), reads `sharedStrings.xml` + the first sheet, and maps columns **by header name** so a column reorder cannot break it. The FPS publishes the same list in **three languages with three different filenames and three different header names** (`Company name` / `Naam van de onderneming` / `Nom de l'entreprise`) — all three are mapped, and language values (`Dutch;French;English` / `Nederlands;Frans;Engels`) are normalised to ISO codes.
- **Follows the Peppol register** and maps the 8 table columns by name, so `Accai Software`'s `AP Certified` status is machine-readable instead of copy-pasted. 64 of the 726 providers are registered for Belgium.
- **Change detection across runs** through a named key-value store (`be-einvoice-change-monitor-snapshot`, L15). Every record gets a `wijzigingstype` and, when something changed, the concrete fields: `kanOntvangen: false → true`.
- **Conservative effective dates**: `effectiefVanaf` is only filled when a trigger word (*vanaf / as from / since / à partir / obligatoire …*) and a date occur in the **same sentence**. No guessing, no LLM interpretation.
- **Short evidence, not a page copy**: each document record carries a `bronfragment` of at most `fragmentLengte` characters (default 200). The full page text is never delivered — see [Legal](#legal-and-source-terms).
- **Polite and fault-tolerant**: honest user agent, serial requests with the `Crawl-delay: 2` that the portal's robots.txt asks for, retry with backoff on 5xx/429 only, and per-source fault isolation — a failing source never crashes the run and never overwrites the stored state.
- **Never bypasses** a login, CAPTCHA, paywall or robots.txt rule. It also refuses to interpret the rules for you: it reports what the official sources publish, nothing more.

### Quick start

Paste one of these into the input and run. Start small (10 records ≈ $0.05) to explore.

**1. What changed in the rules? (the classic monitor run, for a schedule):**

```json
{ "bron": "portaal", "talen": ["nl", "en"], "alleenWijzigingen": true, "maxDocumenten": 25 }
```

**2. Full current rule set, all three languages (ad-hoc / agent use):**

```json
{ "bron": "alle", "talen": ["nl", "fr", "en"], "alleenWijzigingen": false, "maxItems": 200 }
```

**3. Which compliant software is available in Belgium?**

```json
{ "bron": "softwarelijst", "alleenBelgischeBtw": true, "maxItems": 400 }
```

**4. Only the Peppol service providers for Belgium:**

```json
{ "bron": "peppol", "alleenServiceprovidersVoorBelgie": true, "maxItems": 100 }
```

> For a scheduled run keep the input **identical** every time. Change-detection compares the current run with the stored state, and switching languages or `bron` mid-flight would make an entire source look "removed". The Actor detects that itself (scope signature) and then simply does not report removals.

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `bron` | `alle` | `portaal` | `softwarelijst` | `peppol` | `alle` | Which source(s) to follow. The software article is always fetched, because it is also the page that links the official XLSX. |
| `talen` | array of `nl` | `fr` | `en` | `["nl","fr","en"]` | Portal language versions to follow. Each language costs extra source requests. |
| `alleenWijzigingen` | boolean | `false` | `false` = full current state with a change label per record (best for a single call or an AI agent); `true` = only new/changed/removed records (best for a schedule). |
| `nieuwsPaginas` | integer 0–3 | `1` | How many news-list pages to walk per language. `0` skips news entirely. |
| `maxDocumenten` | integer 1–300 | `40` | Cap per category (news, FAQ) per language. |
| `sinds` | date (optional) | — | Only deliver portal documents whose source date is on or after this day. Not applied to software/Peppol records (they have no publication date). |
| `fragmentLengte` | integer 0–400 | `200` | Cap of the literal excerpt. `0` = no excerpt. |
| `alleenBelgischeBtw` | boolean | `true` | Keep only Belgian VAT numbers in the software list (≈300 of 469 rows). |
| `alleenServiceprovidersVoorBelgie` | boolean | `false` | Keep only Peppol providers with country `Belgium`. |
| `contactgegevens` | boolean | `false` | Also deliver contact names/e-mails. Off by default: those are personal data (GDPR). |
| `crawlDelayMs` | integer 0–30000 | `2000` | Minimum pause between source requests (robots.txt asks for 2 s). |
| `maxItems` | integer 1–5000 | `1000` | Total record cap for the run. |

### Output

One dataset item per followed document, per software row and per service provider. The three record kinds share one core, so a single dataset works for a compliance feed:

```json
{
  "recordSoort": "software",
  "bronId": "BE0693727271|EENVOUDIGFACTUREREN|eenvoudigfactureren.be",
  "bron": "softwarelijst-fps-finance",
  "bronUrl": "https://einvoice.belgium.be/sites/default/files/uploads/Lijst software leveranciers/123-e-invoice-software-26062026.xlsx",
  "isOfficieleBron": true,
  "titel": "Eenvoudigfactureren (EENVOUDIGFACTUREREN BV)",
  "wijzigingstype": "inhoud",
  "wijzigingsdetails": ["kanOntvangen: false → true"],
  "contentHash": "560efc04…",
  "eerderGezienOp": "2026-09-18T08:12:44.001Z",
  "vastgelegdOp": "2026-09-25T16:36:13.181Z",
  "bedrijf": "EENVOUDIGFACTUREN BV",
  "btwNummer": "BE0693727271",
  "isBelgischeBtw": true,
  "applicatie": "Eenvoudigfactureren",
  "kanVerzenden": true,
  "kanOntvangen": true,
  "kanVerwerken": true,
  "verzendenVanaf": null,
  "ontvangenVanaf": null,
  "talen": ["Dutch", "French", "English"],
  "urlEindgebruiker": "https://eenvoudigfactureren.be/"
}
```

A portal document record instead carries `taal`, `documentSoort` (`nieuws` / `faq` / `artikel`), `publicatiedatum`, `effectiefVanaf`, `bronfragment` and `contentHash` of the page text:

```json
{
  "recordSoort": "document",
  "bronId": "https://einvoice.belgium.be/en/news/period-tolerance-during-first-three-months-2026",
  "bron": "einvoice.belgium.be",
  "taal": "en",
  "documentSoort": "nieuws",
  "titel": "Period of tolerance during the first three months of 2026",
  "wijzigingstype": "nieuw",
  "publicatiedatum": "2025-12-18",
  "effectiefVanaf": "2026-01-01",
  "bronfragment": "Introducing compulsory e-invoicing between Belgian enterprises liable to tax as from 1er January 2026 is an important step in the digitalisation of our economy."
}
```

#### Change types

| `wijzigingstype` | Meaning |
|---|---|
| `nieuw` | Portal document seen for the first time by this monitor. |
| `toegevoegd` | New row in the software list or new Peppol service provider. |
| `inhoud` | Text or row data changed — `wijzigingsdetails` names the fields (`kanVerzenden: false → true`). |
| `datum` | Only the source date changed (a CMS re-publish without a text change). |
| `verwijderd` | Row or provider no longer present in the source list. |
| `bron-weg` | Portal document disappeared from the listing. |
| `onveranderd` | Identical to the previous run (only delivered when `alleenWijzigingen` is `false`). |

### Use cases

- **Compliance feed for a finance department**: run nightly with `alleenWijzigingen: true` and post the `wijzigingssignaal` records into Slack/Teams. No more "did the FPS change anything?".
- **Software selection for an ERP migration**: filter the software list on `kanOntvangen` / `kanVerwerken` and `ontvangenVanaf`, and watch which vendors add capabilities.
- **Supplier/partner due diligence**: check whether a vendor's product is on the official list, which Peppol authority certifies them, and from which date they claim to be ready.
- **Regulatory timeline for audits**: collect the `effectiefVanaf` dates the portal itself states, with the source link and excerpt per milestone.
- **Agent alerting**: hand the dataset to an agent with a rule like "alert when `wijzigingstype` is `inhoud` and `effectiefVanaf` changed".

### Legal and source terms

Belgian and EU public sources are covered by different rules, so this Actor is deliberately conservative:

- **`einvoice.belgium.be` (FPS BOSA)** — `robots.txt` allows content pages and asks for `Crawl-delay: 2` (the Actor's default). Its general conditions state that the *texts* on the site may be reproduced **at no cost for non-commercial use** only. A paid Apify Actor is commercial, so this Actor delivers **metadata plus one short excerpt (max 400 characters, 200 by default)** and never the full page text. Images and videos on that site require written permission and are not used at all.
- **The FPS Finance XLSX** is a public data file linked from the official article. It is used as **row data** (vendor, VAT number, capability flags, dates) — the workbook itself is not republished. The list is published per language; the Actor reads the version of the **first language in `talen`** and covers all three.
- **`peppol.org`** has an open `robots.txt` and publishes factual register data. Contact names and e-mail addresses are personal data under the GDPR and are only included when you set `contactgegevens: true`.
- **Not used, on purpose:** the Belgian State Gazette (`ejustice.just.fgov.be`) because its `robots.txt` disallows `/eli/` and `/cgi/`; `financien.belgium.be` because it sits behind a bot-check; EU e-invoicing / VAT-in-the-Digital-Age registers because they require registration. No alternative route is used to get around any of them.
- **No advice.** This Actor reports what official sources publish — dates, capability flags, change types and links. It is not legal, fiscal or accounting advice, and `effectiefVanaf` is only ever a date the source itself states.

### Pricing (Pay Per Event)

| Event | Price |
|---|---|
| `document-record` | $0.003 per delivered official portal document |
| `software-record` | $0.0035 per delivered compliant-software record |
| `serviceprovider-record` | $0.003 per delivered Peppol service provider |
| `wijzigingssignaal` | $0.01 per record that is new, changed, added, removed or disappeared |

A full three-language portal run is roughly $0.10; a Belgian-only software snapshot is roughly $1.00. Start with `maxItems: 10` if you just want to look around.

### FAQ

**Does this tell me whether my company is compliant?**
No. It reports what the official sources publish. Nobody — neither this Actor nor the excerpt it delivers — draws a compliance conclusion for you.

**Why is the excerpt so short?**
Because the portal's conditions limit reproducing its texts to non-commercial use. The excerpt (one short sentence) is evidence for the change; the full text stays with the source.

**How do I avoid phantom changes?**
Keep the input identical between scheduled runs. If you change `talen`, `bron` or the list filters, the Actor recognises the different scope and stops reporting removals instead of pretending your sources vanished.

**Why do I sometimes see the same product twice in the software list?**
The official spreadsheet contains a few repeated rows and a few different products that share a product name (for example *Sage BOB* and *Sage Cloud Demat Invoicing*). Every source row is tracked separately — never merged — so nothing is hidden and each row keeps its own change history.

**What does `kanVerzenden: null` mean?**
The source answers "No, but will be possible in the future" or "No, that is not planned". That is not the same as a plain "No", and the Actor keeps the distinction instead of flattening it.

**What happens when a source fails?**
Each source is isolated: the other two still deliver, the failures land in `RUN_SUMMARY.bronfouten` and the log. The stored state is then deliberately **not** updated — otherwise the next successful run would report everything from the failed source as `verwijderd`. The cost is at most one missed update.

**Can it run without the KV store?**
Yes, but then every record is reported as `nieuw` on every run. Change detection needs the named key-value store that Apify provides per Actor.

### Keywords

belgium, belgique, belgië, e-invoicing, einvoice, e-facturatie, facturation électronique, peppol, b2b, btw, vat, compliance, finance software, accounting, erp, invoice, monitor, change detection, official data, fps finance, bosa

### Changelog

- **0.1** — Initial version: official portal (news, FAQ, software article) in NL/FR/EN, FPS Finance compliant-software XLSX, Peppol certified service-provider register, cross-run change detection with field-level details, conservative effective-date extraction, short source excerpts.

### Related Actors

- [Flanders Water Extraction Ban Monitor](https://apify.com/codeclouds/be-captatieverbod-monitor) — another Belgian official-data monitor, useful as a template for the same "official source + change signal" pattern.

# Actor input Schema

## `bron` (type: `string`):

Welke bron deze run volgt. 'portaal' = nieuws/FAQ/artikelen van einvoice.belgium.be (en vindt de link naar de softwarelijst). 'softwarelijst' = de officiele xlsx-lijst met conforme e-invoicingsoftware. 'peppol' = gecertificeerde Peppol-serviceproviders op peppol.org. 'alle' = alle drie.

## `talen` (type: `array`):

Taalversies van het officiele portaal die gevolgd worden. Elke taal kost extra bronrequests (robots.txt schrijft Crawl-delay 2 voor). 'nl' + 'en' dekt de meeste compliance-tekst; voeg 'fr' toe voor volledige drietaligheid.

## `alleenWijzigingen` (type: `boolean`):

false = volledige actuele stand, elk record met zijn wijzigingstype (handig voor een losse aanroep of AI-agent). true = alleen nieuwe, gewijzigde en verdwenen records t.o.v. de vorige run; zet dit aan voor een scheduled run. De vergelijking gebeurt via een named key-value store die over runs heen bewaard wordt.

## `nieuwsPaginas` (type: `integer`):

Aantal pagina's van de nieuwsoverzichtslijst per taal die worden doorlopen (0 = geen nieuws volgen, alleen FAQ/artikel). 1 = alleen de nieuwste items.

## `maxDocumenten` (type: `integer`):

Bovengrens op het aantal opgehaalde documenten per categorie (nieuws, FAQ) per taal. Houdt een run betaalbaar bij een grote FAQ-portfolio.

## `sinds` (type: `string`):

Optionele YYYY-MM-DD. Lever alleen portaaldocumenten waarvan de bron-datum op of na deze dag valt. Geldt niet voor software- en Peppol-records (die hebben geen publicatiedatum).

## `fragmentLengte` (type: `integer`):

Maximale lengte in tekens van het korte letterlijke bewijsfragment per document. 0 = geen fragment. Bewust kort: de voorwaarden van het portaal staan tekstreproductie alleen toe voor niet-commercieel gebruik.

## `alleenBelgischeBtw` (type: `boolean`):

Filter de officiele softwarelijst op Belgische btw-nummers. De bron bevat ruim 320 Belgische en ruim 100 buitenlandse leveranciers; zet op false om alles te zien.

## `alleenServiceprovidersVoorBelgie` (type: `boolean`):

Filter de Peppol-lijst op serviceproviders uit België (bronterm 'Belgium').

## `contactgegevens` (type: `boolean`):

Lever ook de naam/e-mail van een contactpersoon (Peppol) en de klantcontact-mail van de softwareleverancier. Standaard uit omdat dit persoonsgegevens zijn (AVG).

## `crawlDelayMs` (type: `integer`):

Minimale pauze tussen twee bronrequests. robots.txt van einvoice.belgium.be schrijft Crawl-delay: 2 voor; niet verlagen bij publiek gedeelde IP's.

## `maxItems` (type: `integer`):

Bovengrens op het totaal aantal geleverde records in deze run.

## Actor input object example

```json
{
  "bron": "alle",
  "talen": [
    "nl",
    "fr",
    "en"
  ],
  "alleenWijzigingen": false,
  "nieuwsPaginas": 1,
  "maxDocumenten": 40,
  "fragmentLengte": 200,
  "alleenBelgischeBtw": true,
  "alleenServiceprovidersVoorBelgie": false,
  "contactgegevens": false,
  "crawlDelayMs": 2000,
  "maxItems": 1000
}
```

# Actor output Schema

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

Results stored in the default dataset.

# 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 = {
    "talen": [
        "nl",
        "fr",
        "en"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("codeclouds/be-einvoice-change-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 = { "talen": [
        "nl",
        "fr",
        "en",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("codeclouds/be-einvoice-change-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 '{
  "talen": [
    "nl",
    "fr",
    "en"
  ]
}' |
apify call codeclouds/be-einvoice-change-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,codeclouds/be-einvoice-change-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/txtf9eJnLJdx7v01V/builds/t6aBrpG6kZkGPmxxw/openapi.json
