# EU Impressum & Legal Notice Extractor: VAT, Register, Directors (`southern-data/legal-notice-extractor`) Actor

Extract legal company details from EU Impressum / mentions légales / aviso legal / note legali / colofon pages: legal name, form, address, directors, VAT ID (VIES-checked), trade register, email, phone. Per-field confidence. $0.003 per site found.

- **URL**: https://apify.com/southern-data/legal-notice-extractor.md
- **Developed by:** [Southern Data](https://apify.com/southern-data) (community)
- **Categories:** Lead generation, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.40 / 1,000 company details extracteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## EU Impressum & Legal Notice Extractor: VAT, Register, Directors

Give it a list of **European company websites**. For each one it finds the legally required company disclosure page (**Impressum**, **mentions légales**, **aviso legal**, **note legali**, **colofon**, legal notice) and returns the company details as **one clean row**: legal name, legal form, registered address, managing directors / representatives, VAT ID (format-checked and optionally confirmed in the EU's official **VIES** service), trade-register court and number, email and phone. Every field comes with a **confidence** level and the **source URL**.

- **Domain in, company details out.** `mymuesli.com`, `https://www.agatha.fr`, a direct link to the legal page (`https://acme.de/impressum`) or an email address (`info@acme.de` → acme.de).
- **Built for DE, AT, CH, FR, IT, ES and NL.** Knows Handelsregister / Firmenbuch / UID / RCS & SIREN / REA & P.IVA / Registro Mercantil & CIF / KvK & BTW formats and their usual labels.
- **VAT IDs validated.** Check digit verified for DE, AT, FR, IT, ES, NL, BE, LU, PL, DK, PT, SE, FI, SK and Swiss UID (pattern check for the other EU states); optional live check in **VIES** (European Commission, free and public) with the registered name and address where the country publishes them.
- **Honest output.** `high` / `medium` / `low` confidence per field, notes that explain anything missing, and nothing charged when no company details are found.
- **Polite.** Reads up to 4 pages per site by default (max 8), one at a time, respects `robots.txt`, identifies itself with its own user agent, and never bypasses bot protection or logins.

### Pricing

| Event | Price | When |
|---|---|---|
| `legal-notice-extracted` | **$0.003** | A website where company details were found (legal name, a valid VAT ID or a register number) |

**Store discount tiers:** free-plan and Bronze users pay the prices above. Silver gets 10% off and Gold (and higher) gets 20% off.

**1,000 websites cost at most $3.00**, and usually less, because you only pay for sites where details were found. **Free:** dead / parked / blocked / unreachable sites, sites with no readable legal notice, address-only notices (sole traders), invalid input, duplicates and anything not processed. Set **Maximum cost per run** and the Actor stops cleanly there; the rest of the list goes to the `REMAINING` record, free.

### Quick start

1. Click **Try for free**.
2. Paste websites, one per line, **or** a link to a CSV / TXT file or a Google Sheet (shared as "Anyone with the link can view") in **websitesFileUrl**. A column named `website`, `url`, `domain`, `homepage` (or similar) is used automatically.
3. Click **Start**. Open the **Output** tab: *Overview*, *With confidence*, *VAT & register*, *Not found / not readable* or *All fields (CSV export)*. Download as CSV, Excel or JSON.

Via API:

```bash
curl -X POST "https://api.apify.com/v2/acts/southern-data~legal-notice-extractor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"websites":["mymuesli.com","https://www.agatha.fr","pastasgallo.es"]}'
```

### Example row

A real row (input `mymuesli.com`), trimmed:

```json
{
  "domain": "mymuesli.com",
  "legalNoticeUrl": "https://www.mymuesli.com/policies/legal-notice",
  "companyName": "mymuesli AG", "companyNameConfidence": "high",
  "legalForm": "AG",
  "addressFull": "Sailerwöhr 16, 94032 Passau, DE", "addressConfidence": "high",
  "representatives": "Max Wittrock", "representativeRole": "Vorstand",
  "vatId": "DE256344371", "vatIdFormatValid": true, "viesStatus": "valid",
  "registerCourt": "Amtsgericht Passau", "registerType": "HRB", "registerNumber": "HRB 11421",
  "email": "support@mymuesli.com",
  "companyCountry": "DE", "language": "de",
  "chargedEvents": "legal-notice-extracted"
}
```

French rows add `siren` and often `shareCapital`; Spanish rows give the Registro Mercantil entry (`Tomo …, Folio …, Hoja …`); Dutch rows give the KvK number.

### What each field means

| Field | Meaning |
|---|---|
| `companyName`, `legalForm` | Legal entity name as written on the page, and its form (GmbH, AG, SAS, SARL, S.r.l., S.p.A., S.L., S.A., B.V. …) |
| `street`, `postalCode`, `city`, `country`, `addressFull` | Registered / postal address given in the notice |
| `representatives`, `representativeRole` | Managing directors, board, gérant, amministratore, administrador, bestuurder… as labelled on the page |
| `vatId`, `vatIdFormatValid`, `vatIdCheck` | VAT ID normalised to `CC123…`; `checksum` = check digit verified, `format` = pattern only (countries without a public check digit) |
| `viesStatus`, `viesName`, `viesAddress` | `valid` / `invalid` / `unavailable` / `not_applicable` (Swiss UID, non-EU). Germany and Spain never return name/address in VIES |
| `registerCourt`, `registerType`, `registerNumber`, `siren` | e.g. Amtsgericht München + HRB 12345; Landesgericht Graz + FN 123456a; RCS Paris + 828 378 570; KvK 55248926; REA BO-253236 |
| `email`, `phone` | Contact details from the legal page (homepage footer as fallback) |
| `…Confidence` | `high` = labelled and close to the company's core details; `medium` = found without a label or on a secondary page; `low` = a guess, check it |
| `legalNoticeUrl`, `foundBy` | Source page and how it was found: `link`, `guessed-path`, `homepage` (footer), `input-url` |
| `notes` | Plain-language explanation of anything unusual or missing |
| `siteStatus` | `ok`, `dead`, `parked`, `blocked`, `unreachable`, `robots_disallowed`, `http_error`, `tls_error`, `redirects_to_platform`, `not_html` |

### Accuracy (tested 1 Nov 2026)

Measured on a **held-out set of 56 real business websites** (8 per country: DE, AT, CH, FR, IT, ES, NL; small firms and large brands), never used for tuning, checked field-by-field by hand. 11 of the 56 couldn't be read at all (dead, bot-protected, unreachable). These were free.

| Field | First run on unseen sites (build 1.0.4) | After fixes (build 1.0.8, tuned on those errors, so optimistic) |
|---|---|---|
| Legal name exactly right (when given) | 60% | 97–100% |
| Legal form | 95% | 100% |
| Address (street + postcode) right | 84% | 92% |
| VAT ID right (when given) | 100% (88% found) | 100% (96% found) |
| Register number right | 100% (95% found) | 100% (100% found) |
| Representatives | 25% found | 4/4 found on the sites where they're named |
| Sites with a legal entity where details were found | 86% | 83% (two now report "refused automated access" instead of guessing) |

**By country / language:** Austria and Germany are strongest (standardised Impressum: name, address, UID, Firmenbuch/HRB almost always right). France is good (RCS/SIREN reliable; names sometimes appear only in prose or with the form first). Spain is good for name, CIF and Registro Mercantil, weaker for addresses written inside long sentences. Italy often has only a footer line (`Ragione sociale – P.IVA – REA`), so the address is often missing. Switzerland usually has no VAT/register number in the notice, so expect name + address only. Dutch sites often have no colofon (KvK and BTW numbers usually sit in the footer, on the contact page or in the terms); in the test only 2 of 8 Dutch sites published a legal entity at all, and the Actor found 1 of them. Treat NL accuracy as unproven.

Representatives are the weakest field, because many notices don't name them. In the test they came back for 4 of 7 Austrian, 2 of 5 French and 1 of 6 German sites, and for none of the Swiss, Italian, Spanish or Dutch ones. When a name was returned it was right in the final build; in the first run 1 of 2 was wrong.

**Treat `low` confidence values as hints, not facts.** Accuracy files: `test/accuracy/` in the source.

### Limits (read before buying)

- **JavaScript-only sites** (single-page apps that render the legal notice in the browser) can't be read: the Actor does not run a browser. They are flagged `jsHeavy` with a note and are free.
- **Bot protection is never bypassed.** Sites behind Cloudflare/Akamai checks, or that answer HTTP 403 to automated visitors, are reported as `blocked` (or with a note on the legal page) and are free.
- **robots.txt is respected.** If a site disallows crawlers, it isn't read (`robots_disallowed`, free).
- **Not a registry lookup.** Data comes from the company's own website and may be out of date. For legally binding checks, verify against the official register (Handelsregister, Firmenbuch, Infogreffe, Registro Mercantil, KvK, Registro Imprese, Zefix). VIES confirms only that a VAT number is active; its name can lag behind renames.
- **Groups and agencies:** if a site's notice names a parent company, a franchise owner or a publisher, that's what you get. The web designer, hoster, data-protection officer and supervisory authority are filtered out, but unusual layouts can still confuse it (check `low` confidence).
- **Sole traders / freelancers** often give only a person's name and address. The address and contacts are returned, but nothing is charged unless a company name, VAT ID or register number is found.
- **Text in images or PDFs isn't read.** Some notices show the address or VAT ID as a picture (anti-spam) or link a PDF.
- **Branch vs head office:** if a notice lists a branch address before the registered office, you may get the branch (seen on 1 of 35 test sites).
- **Other EU countries** (BE, LU, PL, PT, Nordics…) work for VAT IDs and many names but weren't part of the accuracy test.

### Personal data and GDPR

Legal notices are **disclosures required by law** (e.g. § 5 DDG in Germany, § 25 MedienG / § 5 ECG in Austria, LCEN art. 6 in France, art. 7 D.Lgs. 70/2003 in Italy, LSSI art. 10 in Spain). They are public so that customers and authorities can identify who runs a website. They can still contain **personal data**, mainly the names of managing directors and sole traders and sometimes direct emails and phone numbers. **You are responsible for using this data lawfully**: have a legal basis (usually legitimate interest for B2B use), respect objection and unsubscribe requests and local direct-marketing rules (e.g. UWG § 7 in Germany), keep only what you need, and don't use it to profile private individuals. This Actor only reads public pages and doesn't store your results outside your own Apify account.

### Daily self-test

The Actor has a built-in health check (`selfTest: true`). It tests the VAT, register and name rules on fixed samples, crawls a built-in test site in full, checks that VIES answers, and does two live reads (example.com and a host that can't exist). It's free. It runs every morning, so a regression in VIES or a parsing rule is caught before customers see it.

### Support

Found a site where the details are wrong? Open an issue with the URL and the row. Fixes for common layouts usually ship within days.

# Actor input Schema

## `websites` (type: `array`):

One per line: "mymuesli.com", "https://www.eataly.it/", a direct link to the legal page ("https://acme.de/impressum" — read first), or an email address (its website is read). Duplicates are removed. Facebook / Instagram / marketplace links are not company websites and are listed as rowStatus=invalid_input (free).

## `websitesFileUrl` (type: `string`):

Public URL of a .txt or .csv file, or a Google Sheets link shared as "Anyone with the link can view". Uses the column headed website / url / domain / homepage / email if there is one; otherwise the column with the most websites. Up to 100,000 rows.

## `checkVies` (type: `boolean`):

Look up each EU VAT ID found in the European Commission's official VIES service (free, public). Adds viesStatus (valid / invalid / unavailable) and, where the member state discloses it, the registered name and address (Germany and Spain never do). Swiss CHE numbers are not in VIES. Turn off for slightly faster runs.

## `maxPagesPerSite` (type: `integer`):

Pages read on each website: the homepage, then the legal-notice link (Impressum, mentions légales, aviso legal, note legali, colofon…) or, if none is linked, the usual paths such as /impressum. 4 is enough for nearly all sites; up to 8. The price is per website, not per page.

## `defaultCountry` (type: `string`):

Optional 2-letter code (DE, AT, CH, FR, IT, ES, NL…). Only used for .com / .eu / .shop sites that don't reveal their country, to read local phone numbers and pick the right legal-notice paths. Country domains and the VAT prefix take priority.

## `maxWebsites` (type: `integer`):

Read only the first N websites — handy for a cheap trial on a big file. The rest are skipped (not charged) and listed in the REMAINING record.

## `maxConcurrency` (type: `integer`):

How many different websites are read at the same time (pages within one website are always read one at a time, politely). 8 is a good default for 512 MB memory.

## `pageTimeoutSecs` (type: `integer`):

Give up on a page that hasn't loaded in this time. A homepage timeout makes the website 'unreachable' (free). Each website stops after 2 minutes in total.

## `selfTest` (type: `boolean`):

Ignore the inputs above and run the built-in health check instead: VAT/register/name rules, a full crawl of a test website, a VIES lookup and two live sites. Writes a SELF_TEST record, adds no dataset rows, charges nothing, and FAILS the run if anything is broken — schedule it daily as a free monitor.

## Actor input object example

```json
{
  "websites": [
    "mymuesli.com",
    "eataly.it",
    "hema.nl"
  ],
  "checkVies": true,
  "maxPagesPerSite": 4,
  "maxWebsites": 0,
  "maxConcurrency": 8,
  "pageTimeoutSecs": 30,
  "selfTest": false
}
```

# Actor output Schema

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

One row per input website. Views: Overview, With confidence, VAT & register, Not found / not readable, All fields.

## `register` (type: `string`):

VAT IDs (with VIES result) and trade-register entries per website.

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

RUN_SUMMARY record: sites read, how many had company details / VAT / register numbers, status counts, charged events and any stop reason.

## `remaining` (type: `string`):

REMAINING record: inputs not read in this run (spend cap or maxWebsites), ready to paste into the next run. Only present when some were left.

## `selfTest` (type: `string`):

SELF_TEST record: results of the health check. Only present when the Actor runs with selfTest: true.

# 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 = {
    "websites": [
        "mymuesli.com",
        "eataly.it",
        "hema.nl"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("southern-data/legal-notice-extractor").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 = { "websites": [
        "mymuesli.com",
        "eataly.it",
        "hema.nl",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("southern-data/legal-notice-extractor").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 '{
  "websites": [
    "mymuesli.com",
    "eataly.it",
    "hema.nl"
  ]
}' |
apify call southern-data/legal-notice-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,southern-data/legal-notice-extractor"
        }
    }
}
```

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/OGfcBPMoAac8vYBrC/builds/9RqXuFSU2pNfAfDsh/openapi.json
