# Mentions Legales Scraper - French SIREN, RCS, TVA & Contacts (`scrapersdelight/mentions-legales-contact-scraper`) Actor

Turn French business domains into registry-grade B2B leads from each site's legally-mandated mentions legales page: SIREN, SIRET, RCS + greffe, TVA, capital social, APE, directeur de la publication, hebergeur, email, phone and address. No login, no API key.

- **URL**: https://apify.com/scrapersdelight/mentions-legales-contact-scraper.md
- **Developed by:** [Scrapers Delight](https://apify.com/scrapersdelight) (community)
- **Categories:** Lead generation, Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$4.00 / 1,000 per legal-notice lead returneds

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

## 🇫🇷 Mentions Légales Scraper — French company leads with a Luhn-checked SIREN

Paste a list of French business domains (or point this Actor at another Actor's dataset) and get back **one clean B2B/KYB lead per domain**, read off each site's statutory **mentions légales** page: the legal entity name and form, the **SIREN checked against the Luhn algorithm**, the SIRET, the **RCS entry + greffe**, the TVA intracommunautaire number, the capital social, the APE/NAF code, the *directeur de la publication*, the **hébergeur**, and a contact email, phone and postal address — for **$4 per 1,000 leads**.

**Why a legal notice and not a directory?** Because **LCEN art. 6-III** (loi n° 2004-575) obliges every commercial French website to publish one. The corpus is therefore *the whole French commercial web* — not one directory's member list — and every field is a public disclosure the company published about itself, by law.

***

### ✅ What does the Mentions Légales Scraper do?

For each domain you give it, two plain HTTP hops. **No browser, no login, no API key.**

1. **Discovery.** Fetch the homepage and **rank every `<a href>`** on both its href *and* its anchor text — exact "Mentions légales" (100) beats a text match (90) beats an `href` match (85) beats "Informations légales" (80) beats "Legal notice" (70), with CGV / CGU / cookies / privacy links **demoted to 30** so a combined footer link can still be a last resort without outranking the real thing. If nothing is linked, fall through to the **XML sitemap**, the **WordPress REST page index**, then **15 conventional paths**.
2. **Resolution.** Fetch the winning candidate, **gate it on a statutory content marker** — a bare HTTP 200 is not enough — then read the fields out of the page **text** with anchored, case-sensitive patterns.

#### The four things a naive legal-notice scraper gets wrong

- 🎯 **Guessing `/mentions-legales` is not the job.** On the recon corpus a naive path guess hit **2 of 10** domains. Ranked footer anchors won **73 of the 76** leads in the benchmark below; guessed paths won 2 and the sitemap won 1. The engineering is in the *ranking*, not the guessing.
- 🧮 **It derives the SIREN from the TVA number, then Luhn-checks it.** A French TVA number is `FR` + a 2-digit key + the SIREN, so it is an exact algebraic container for the registry id. On `delitaly.fr` a plain RCS regex pulled a 9-digit window *out of the TVA string* and emitted SIREN `624223821` for a company whose real SIREN is `422382184`. This Actor prefers TVA → labelled SIREN → SIRET → RCS, records the disagreement in `sirenConflict`, and emits `sirenValid` so you can see the checksum result rather than trust it silently.
- 📍 **The postcode comes from the parsed address, never from the page.** A page-wide `\d{5}` scan once emitted Beverly Hills' `90210` for a company in Toulouse (`31100`). `postalCode` and `city` are only ever read out of an address block that also carried a street.
- 🏛 **An RCS number that disagrees with the SIREN is dropped, not published.** By French law the RCS registration number *is* the company's SIREN. When they differ, the RCS statement on the page belongs to another entity — usually the web agency that built the site — so publishing it as this company's register entry would be wrong. The greffe city is kept, the mismatched number is moved to `rcsConflict`.

***

### 📊 Measured, from real runs

Everything below is from actual runs, not estimates.

#### End-to-end over 150 real French SME domains

Sourced from OpenStreetMap (`shop` / `office` / `craft` entries carrying a `website` tag) across Paris, Toulouse, Bordeaux, Nantes, Strasbourg, Marseille and Lille — a realistic buyer list, not a hand-picked one. Apify **datacenter** proxy, concurrency 12, default settings.

| Outcome | Domains | Share | Billed? |
|---|---:|---:|:--:|
| ✅ `ok` — a parsed, billable lead | **76** | **51 %** | **yes** |
| 🔁 `duplicate` — same SIREN as a row already delivered | 3 | 2 % | no |
| ⚪ `no_legal_page` — the site publishes none | 53 | 36 % | no |
| 💀 `unreachable` — dead host (NXDOMAIN / TLS / refused) | 15 | 10 % | no |
| 🔴 `blocked` by an anti-bot wall | 2 | 1 % | no |

**134 of the 149 domains were reachable; 79 of those 134 (59 %) published a findable legal notice**, and 76 became billable leads after the SIREN-level dedupe. One duplicated input line was collapsed *before it cost a request*. A second run of the same list a few hours earlier returned **81** leads — expect a few points of run-to-run variance from transient host failures.

A tighter, curated 76-domain recon corpus scored higher — **46 of 62 reachable domains = 74 %**. Both numbers are real; which one you see depends on how clean your list is. Budget somewhere between them.

#### Per-field fill, over the 76 delivered leads

| Field | Fill | | Field | Fill |
|---|---:|---|---|---:|
| `companyName` | **97 %** | | `socialLinks` | 72 % |
| `addressLine` | **92 %** | | `legalForm` | 63 % |
| `postalCode` | **92 %** | | `shareCapital` | 58 % |
| `city` | **92 %** | | `publicationDirector` | 50 % |
| `email` | 76 % | | `hostingProviderAddress` | 46 % |
| `phone` | 76 % | | `privacyPolicyUrl` | 45 % |
| `siren` | 71 % | | `siret` | 41 % |
| `hostingProvider` | 71 % | | `rcsNumber` | 38 % |
| `vatNumber` | 34 % | | `cgvUrl` | 30 % |
| `rcsCity` | 29 % | | `apeCode` | 24 % |
| `legalRepresentative` | 20 % | | `dpoContact` | 11 % |

**Any registry id (SIREN, SIRET or TVA): 71 % · any contact (email or phone): 92 % · postal address: 92 %.**
Median row carries **11** of the 17 value fields (p90 = 14, best = 15).

The SIREN was derived from the TVA number on 26 rows, from the SIRET on 15, from the RCS entry on 7 and from an explicit label on 6. **53 of the 54 SIRENs passed the Luhn checksum**; the one failure is flagged `sirenValid: false` rather than dropped. The cross-field guards fired for real: **1 `sirenConflict`** and **5 `rcsConflict`** rows would have shipped another company's register entry without them.

#### Transport

Sustained-load tested on the winning transport (got-scraping over the Apify **datacenter** proxy, ~350 sequential calls in 4 independent batches): **61 of 62 reachable domains usable = 98.4 %**, with **zero 403s, zero CAPTCHAs and zero challenge interstitials** across the SME corpus. Residential exit nodes changed nothing there, so datacenter is the default and residential is a per-block escalation, not the norm. **Chromium is never launched.** 512 MB, 847 requests for 149 domains ≈ 5.7 requests per domain; 20 blocks escalated to residential, 2 recovered.

Per-domain latency on delivered rows: **p50 4.5 s, p90 13.5 s, max 36.3 s** — which is why `perDomainTimeoutSecs` exists.

***

### ⚠️ Honest limits — read these before you buy

- **Roughly a third of reachable French SME sites publish no legal notice at all.** That is not a parser gap: those domains were re-probed through the sitemap (287- and 500-URL sitemaps read), the WordPress REST index, 15 guessed paths and `/`, `/contact`, `/cgv`, `/conditions-generales-de-vente`, `/a-propos`. They carry no SIREN, SIRET, TVA, RCS, capital or *directeur de la publication* anywhere on the site. They are LCEN-non-compliant micro-businesses. **You are never charged for them.**
- **Stale lists have dead hosts.** 15 of 149 domains (10 %) did not resolve at all — verified identical from a home IP, a datacenter IP and a residential-FR IP, so these are stale directory entries, not blocks. Never charged.
- **A CDN-fronted enterprise tail is genuinely walled.** Of 10 large French brands probed, datacenter cleared 6 and residential-FR cleared 7; `leroymerlin.fr`, `fnac.com`, `decathlon.fr` and `boulanger.com` refused both. This Actor is built for the SME long tail, which is unaffected. `Escalate to residential on block` is on by default and is what recovers the middle of that band.
- **This Actor does not confirm the SIREN against INSEE/Sirene.** It reports what the company published, plus a checksum. A registry cross-check is a different data source and a different job.
- **`legalRepresentative` (gérant / président) fills on ~20 % of pages** — French legal notices name the *directeur de la publication* far more often than the company officer.

***

### 🧾 Output

One row per domain. `flattenOutput` (default **on**) gives you flat columns for Sheets / Clay; turn it off for nested `registry{} / people{} / contact{} / hosting{} / policies{}` objects.

| Field | Description |
|---|---|
| `domain` | Registrable domain (eTLD+1, `www`-stripped) — the fallback stable id. |
| `mentionsLegalesUrl` | The exact page the data came from. |
| `discoveryChannel` | `anchor` · `sitemap` · `wpjson` · `guess` · `input` — how the page was found. |
| `companyName` | The legal entity name. |
| `legalForm` | `SAS` · `SASU` · `SARL` · `EURL` · `SA` · `SNC` · `SCI` · `SELARL` · `SCOP` · `EI` · `association loi 1901` · `micro-entreprise`. Matched case-sensitively, longest form first (`SAS` is never read as `SA`). |
| `siren` | The 9-digit registry id. |
| `sirenValid` | **Luhn checksum result.** `false` means the page carries a typo — the row is flagged, never silently dropped. |
| `sirenSource` | `vat` · `label` · `siret` · `rcs` — which statement the SIREN came from. |
| `sirenConflict` | A second, disagreeing SIREN found on the page (the cross-field trap, surfaced instead of hidden). |
| `siret` | The 14-digit establishment id. |
| `vatNumber` | TVA intracommunautaire, normalised (`FR 62 422 382 184` → `FR62422382184`). |
| `rcsCity` / `rcsNumber` | Greffe (register court) and the RCS registration number, cross-checked against the SIREN. |
| `rcsConflict` | An RCS number that disagreed with the SIREN — kept visible, not published as the company's. |
| `shareCapital` / `shareCapitalCurrency` | Capital social as a **number** (`"2.500 €"` → `2500`, `EUR`). |
| `apeCode` | APE / NAF activity code (`7733Z`). |
| `publicationDirector` | Directeur / directrice de la publication. |
| `legalRepresentative` | Gérant · président · représentant légal · dirigeant. |
| `hostingProvider` / `hostingProviderAddress` | The hébergeur — OVH, Scaleway, Ionos, o2switch, Gandi, Infomaniak, Hostinger, Wix, Shopify, Cloudflare, AWS… A resellable technographic signal. |
| `dpoContact` | Délégué à la protection des données (GDPR/KYB buyers). |
| `addressLine` / `postalCode` / `city` | Registered address, parsed as a block. |
| `email` / `emails[]` | Contact addresses, `mailto:` preferred, obfuscated `(at)` forms decoded. |
| `phone` / `phones[]` | Normalised to E.164 (`01 23 45 67 89` → `+33123456789`). |
| `socialLinks[]` | LinkedIn / Facebook / Instagram / X / YouTube / TikTok / Vimeo profiles on the page. |
| `cgvUrl` / `privacyPolicyUrl` | The site's CGV and politique de confidentialité. |
| `stableId` | The Luhn-valid SIREN when present, else the registrable domain. **The dedupe key.** |
| `fieldCount` · `status` · `missReason` · `fetchedAt` · `elapsedMs` | Row quality, outcome and provenance. |

Example row (`share-wood.fr`, real output):

```json
{
  "domain": "share-wood.fr",
  "mentionsLegalesUrl": "https://share-wood.fr/mentions-legales/",
  "discoveryChannel": "anchor",
  "companyName": "SHARE-WOOD",
  "legalForm": "SAS",
  "siren": "433115904", "sirenValid": true, "sirenSource": "vat",
  "vatNumber": "FR35433115904",
  "rcsCity": "Paris", "rcsNumber": "433115904",
  "shareCapital": 214410.5, "shareCapitalCurrency": "EUR",
  "apeCode": "7733Z",
  "publicationDirector": "Romain ABBÜHL",
  "hostingProvider": "SCALEWAY",
  "hostingProviderAddress": "8 RUE DE LA VILLE L EVEQUE, 75008 PARIS",
  "addressLine": "47, Boulevard Burel, 13014 Marseille",
  "postalCode": "13014", "city": "Marseille",
  "email": "contact@share-wood.fr", "phone": "+33465430300",
  "stableId": "433115904", "status": "ok"
}
```

***

### ⌨️ Input

Five ways to hand over a list, because agencies do not all work the same way.

| Field | What it is |
|---|---|
| **`domains`** | One entry per company. A bare domain (`donatek.fr`), a homepage URL, **or a direct mentions-légales URL** — a direct URL skips the homepage hop entirely and costs one request. |
| **`startUrls`** | The standard Apify request-list editor, so Make / Zapier / Google Sheets can hand over a URL list natively. |
| **`sourceDatasetId`** + **`domainFieldName`** | Read the domain list straight out of another Actor's dataset — the real agency workflow: enrich the output of a Maps or directory scrape. |
| **`domainsFileUrl`** | A CSV / TXT / JSON / JSONL file at a URL, for lists too big to paste. The column is found by header name or by looking like a domain. |
| **`skipDomains`** + **`previousDatasetId`** | Suppression. Point `previousDatasetId` at last month's run and this month's re-run never re-buys a lead you already paid for. |

Then the knobs that matter:

| Field | Default | What it does |
|---|---|---|
| `maxItems` | 1000 | Hard cap on rows **delivered and billed**. `0` = unlimited. |
| `maxDomains` | 0 | Cap on domains **attempted** — a different number, because yield is ~51-74 %. |
| `discoveryChannels` | all 4 | A real cost lever: anchors alone cost ~2 requests/domain, the full cascade up to 14. |
| `maxDiscoveryRequestsPerDomain` | 12 | Hard ceiling on the guessed-path channel. |
| `perDomainTimeoutSecs` | 60 | Per-domain deadline, so one slow host cannot stall the run (recon saw 130 s and 152 s outliers). |
| `requestConcurrency` | 10 | 1–30. |
| `proxyConfiguration` | Apify datacenter | The measured winner. Bring your own proxies if you prefer. |
| `proxyCountry` | none | `FR` / `BE` / `CH` / `LU` / `MC` / `CA`. **Note:** Apify datacenter proxies cannot be country-pinned (they answer HTTP 407), so choosing a country switches the run to residential exit nodes. |
| `escalateToResidentialOnBlock` | true | One residential-FR retry on a real 403 or challenge — never on a dead host, never on a 404. |
| `requireRegistryId` / `requireContact` / `minFieldsRequired` | off | Quality floors. **A row filtered out here is never billed.** |
| `emailPolicy` | all | `role-only` (contact@, info@…) or `exclude-role` (named mailboxes only). |
| `dedupeBy` | SIREN, else domain | Or `domain`, `siren`, or `none`. |
| `includeMissRows` | false | Emit an **unbilled** row with `status` + `missReason` for every dead / walled / notice-less domain, for coverage accounting. |
| `includeRawText` / `includeRawHtml` | false | Attach the source for audit or your own LLM re-parse. |
| `flattenOutput` | true | Flat columns vs nested objects. |
| `respectRobotsTxt` | false | Exposed as a switch. Measured: **0 of 10** French sites disallow the legal-notice path. |
| `customUserAgent` / `extraHttpHeaders` | — | Bring your own fingerprint. |

Example input:

```json
{
  "domains": ["donatek.fr", "sapio-arts.fr", "https://pao-ceramique.fr/mentions-legales"],
  "maxItems": 1000,
  "requireRegistryId": false,
  "includeMissRows": false
}
```

***

### 💰 Pricing

**Pay per event — one event, `mentions-legales-scraped`, at $0.004 per delivered lead ($4 per 1,000).**

Rows are delivered **and** billed atomically through the same call, so if you hit a charge cap you never keep a row you were not billed for, and never pay for a row you did not receive.

**You are not charged for:**

- a domain whose host is dead,
- a domain blocked by an anti-bot wall,
- a domain that publishes no legal notice,
- a duplicate collapsed by `dedupeBy`,
- a row filtered out by `requireRegistryId` / `requireContact` / `minFieldsRequired`,
- any `includeMissRows` coverage row.

| | Price per row |
|---|---:|
| Generic EU imprint extractors | $0.0025 |
| Domain → SIREN lookups | $0.0045 – $0.006 |
| **This Actor** | **$0.004** |
| Our own DACH Impressum scraper | $0.015 |

At 51–74 % yield, a **1,000-domain list** costs roughly **$2.00 – $3.00** in event fees.

***

### ❓ FAQ

**What is a "mentions légales" page?**
The legal notice every commercial French website must publish under LCEN art. 6-III / art. 19 (loi n° 2004-575 pour la confiance dans l'économie numérique). It identifies the publisher: company name, legal form, share capital, registered address, SIREN/RCS, TVA number, the *directeur de la publication* and the site's host.

**Is this legal to scrape?**
These pages are statutory public disclosures that exist precisely to be read, and they are meant to be indexed — **0 of 10 French sites we checked disallow the path in `robots.txt`**. You remain responsible for each site's Terms of Service, and for handling any personal data in the output (a named *directeur de la publication*, a personal email) under the GDPR. See the legal note below.

**Why is `siren` only ~71 % and not 100 %?**
Because ~29 % of legal notices name a company and an address but no registry id — commonly *auto-entrepreneurs* and associations. Turn on **`requireRegistryId`** and those rows are neither delivered nor billed.

**Can I run it on Belgian, Swiss or Canadian French-language sites?**
Yes — set `proxyCountry` and, if you want, `tldFilter`. The registry patterns (SIREN/SIRET/RCS/TVA FR) are French, so a `.be` or `.ch` site will return contact and address data but usually no French registry id.

**How is a duplicate handled?**
Two ways. A repeated *input line* is collapsed at queue time, before it costs a single request. Two *different* domains that turn out to share one SIREN are collapsed at delivery time, before `pushData` — so a duplicate can never double-charge.

**Can I get only rows with an email?**
Set `requireContact: true`. Rows without an email or phone are then neither delivered nor billed.

**How do I re-run monthly without paying twice?**
Put the previous run's dataset id in `previousDatasetId`. Every `stableId`, `domain` and `siren` it contains is suppressed.

**Does it need a browser or a residential proxy?**
No. Datacenter HTTP measured **98.4 %** usable on the French SME corpus with zero CAPTCHAs. Residential is a per-block escalation for the CDN-fronted enterprise tail only.

**Does it use the INSEE/Sirene API?**
No. It reports what the company itself published, plus an offline Luhn checksum. No external API calls, no keys, no rate limits.

**What happens if my list is entirely dead domains?**
The run exits **cleanly** with zero billed rows and a status message telling you why. It only fails loudly when the transport itself is broken — 80 %+ of domains refused with a challenge — because that is a proxy problem, not an empty market.

**Can I export to Google Sheets / Clay / HubSpot?**
Yes. Leave `flattenOutput` on and use Apify's CSV / Excel / JSON export or the dataset API.

***

### ⚖️ Legal

This Actor reads **statutory public disclosures** that French law requires websites to publish, over ordinary HTTP requests to ordinary public pages. It does not log in, does not bypass any anti-abuse control, does not solve CAPTCHAs, and does not forge authentication. `robots.txt` compliance is available as an input switch (measured: 0 of 10 French sites disallow the legal-notice path).

Output may contain **personal data** — a named *directeur de la publication*, a named gérant, a personal email address. Under the GDPR you are the controller of what you do with it: have a lawful basis, honour erasure requests, and respect each site's Terms of Service. Scraping and using this data is your responsibility, not Apify's and not this Actor's.

# Actor input Schema

## `domains` (type: `array`):

One entry per company. Accepts all three shapes: a bare domain ("donatek.fr"), a homepage URL ("https://www.sapio-arts.fr") or a direct mentions-légales URL ("https://x.fr/mentions-legales"). The Actor finds each site's legal-notice page and parses the statutory fields out of it — one lead per domain. Leave empty to run the built-in French demo batch.

## `startUrls` (type: `array`):

The same domain list handed over as URLs, so Make, Zapier, Clay or a Google Sheet can pass it natively (a link to a text/CSV file of URLs also works). Merged with "French domains".

## `sourceDatasetId` (type: `string`):

Dataset ID of a previous Actor run. Each item's domain/website column is read and enriched — the real agency workflow: run a directory or Maps scraper first, then pipe its output here.

## `domainFieldName` (type: `string`):

Which field of the source dataset (or which CSV column of the list file) holds the domain. Leave empty to auto-detect domain / website / websiteUrl / url / site / homepage. Dotted paths like "company.website" work.

## `domainsFileUrl` (type: `string`):

URL of a CSV, TXT, JSON or JSONL file holding the domains — for 100k-row lists that will not paste into the editor. The column is picked with "Domain field / CSV column".

## `skipDomains` (type: `array`):

Domains to skip outright — accounts you already own, competitors, do-not-contact entries. Matched on the registrable domain, so "www.x.fr/page" and "x.fr" are the same entry.

## `previousDatasetId` (type: `string`):

Dataset ID of an earlier run of THIS Actor. Its stableId / siren / domain values are loaded as a suppression list, so a monthly re-run never re-delivers — and never re-charges you for — a company you already bought.

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

Hard cap on rows DELIVERED AND BILLED this run. Distinct from "Max domains": roughly 3 of 4 reachable French sites publish a legal notice, so 1,000 domains yield ~740 billed rows. 0 = unlimited.

## `maxDomains` (type: `integer`):

Cap on domains ATTEMPTED, applied before any request. Use it to sample a big list cheaply. 0 = attempt every domain supplied.

## `maxDiscoveryRequestsPerDomain` (type: `integer`):

Hard cap on discovery + confirmation requests per domain, counted after the homepage. This is the knob that stops one slow host burning two minutes of a run. Anchor discovery alone costs 2 requests; the full cascade can reach 14. Recon measured the yield plateau at about 10.

## `perDomainTimeoutSecs` (type: `integer`):

Wall-clock deadline for one domain, discovery included. Measured latency on delivered rows: p50 5.6 s, p90 14.0 s, max 28.8 s — 60 s leaves headroom without letting a pathological host stall the pool.

## `requestConcurrency` (type: `integer`):

How many domains are worked in parallel. Higher is faster; keep it modest to stay polite to small business sites.

## `requestTimeoutSecs` (type: `integer`):

Timeout for a single HTTP request.

## `maxRequestRetries` (type: `integer`):

Retries per request, each on a FRESH proxy IP (got-scraping's own retry reuses the flagged IP, which is useless against a soft block).

## `discoveryChannels` (type: `array`):

Which channels may be used to locate the legal-notice page, tried in this order. Anchor ranking alone won 25 of 26 successful discoveries in recon, so turning the rest off is a real cost lever on a big list. wp-json is what finds WordPress sites whose footer carries no legal link at all.

## `followWwwAndRootVariants` (type: `boolean`):

If the homepage fails, retry the other host form (www.x.fr ↔ x.fr) before declaring the domain unreachable.

## `deepJsDiscovery` (type: `boolean`):

Last-resort channel for a footer that only exists after client-side render. No Chromium is launched — this build is deliberately browser-free, because a browser image would quadruple the compute cost of every run for a case recon measured as rare. Instead the Actor reads what a render would have read FROM: the page's inline JSON payloads and its external JavaScript bundles, searching them for a legal-notice URL. Costs up to 6 extra requests per domain and is never charged separately. (Renamed from useBrowserFallback, which read as if it launched a browser; the old key is still accepted.)

## `respectRobotsTxt` (type: `boolean`):

Fetch and honour each site's robots.txt before requesting anything. Off by default because it costs one extra request per domain and measured 0 of 10 French sites (SME and enterprise) disallow the legal-notice path — those pages exist to be indexed.

## `proxyConfiguration` (type: `object`):

Proxy settings. Apify DATACENTER is the default and is the measured winner: 98.4% usable transport on the French SME corpus, zero 403s, zero CAPTCHAs. Residential changed nothing there, so it is not the default.

## `proxyCountry` (type: `string`):

Pin the exit country for francophone sites outside France that geo-tailor their content. MEASURED: Apify datacenter proxies cannot be pinned to a country (they answer HTTP 407), so choosing a country here switches the run to RESIDENTIAL exit nodes in that country. Leave on None for the cheaper, faster datacenter path that the French SME corpus does not need a country for.

## `escalateToResidentialOnBlock` (type: `boolean`):

On a 403 / challenge (never on a dead host or a 404), retry the request once on RESIDENTIAL + country-FR. Measured: this is what recovers CDN-fronted French brands such as blablacar.fr. Small SME lists never trigger it.

## `customUserAgent` (type: `string`):

Override the browser User-Agent sent on every request. Leave empty for the built-in Chrome 124 fingerprint.

## `extraHttpHeaders` (type: `object`):

Additional request headers, merged over the defaults (e.g. a From: header identifying your crawler).

## `tldFilterMode` (type: `string`):

Many French SMEs are not on .fr (.com, .paris, .bzh, .eu, .alsace). Choose whether the TLD list below includes or excludes.

## `tldFilter` (type: `array`):

The TLD list the mode above applies to, without the dot: fr, com, paris, bzh, eu.

## `requireRegistryId` (type: `boolean`):

Deliver (and bill) only companies whose legal notice carries a SIREN, SIRET or TVA number, and whose SIREN passes the Luhn checksum. A page whose registry id fails the checksum is filtered out here, never billed. Measured: 83% of parsed pages carry at least one id.

## `requireContact` (type: `boolean`):

Deliver (and bill) only companies with an email or a phone number. Measured: 89% of parsed pages carry at least one.

## `minFieldsRequired` (type: `integer`):

Quality floor: a row must carry at least this many of the 17 value fields (company, legal form, SIREN, SIRET, TVA, RCS city + number, capital, APE, director, representative, host, address, postcode, city, email, phone) before it is delivered and billed. 0 = no floor.

## `emailPolicy` (type: `string`):

Agencies split hard on whether contact@ / info@ counts as a lead. "Role only" keeps just those; "Exclude role" keeps only named mailboxes.

## `dedupeBy` (type: `string`):

Which key collapses duplicates BEFORE anything is pushed or charged. The default uses a Luhn-valid SIREN when the page carries one and the registrable domain otherwise, so two domains owned by the same company collapse to one billed row — and a repeated input line is dropped at queue time, before it costs a request. "No deduplication" genuinely means none: you get, and pay for, one row per input line.

## `validateSiren` (type: `boolean`):

Verify every SIREN against the Luhn checksum and emit sirenValid. A failure is FLAGGED, never dropped — La Poste (356000000) is a legitimate national exception.

## `deriveSirenFromVat` (type: `boolean`):

A French TVA number is FR + a 2-character key + the 9-digit SIREN, so the TVA is an exact container for it. Keeping this on fixes a real cross-field bug: on one probe site an RCS regex pulled a DIFFERENT 9-digit window out of the TVA string and produced a wrong SIREN.

## `extractHostingProvider` (type: `boolean`):

The site's host (OVH, o2switch, Wix, Shopify, Cloudflare…) plus its address. 87% fill — a technographic signal hosting resellers and agencies buy on its own.

## `extractDpo` (type: `boolean`):

Délégué à la protection des données / data protection officer contact, for GDPR and KYB buyers.

## `extractSocials` (type: `boolean`):

LinkedIn, Facebook, Instagram, X, YouTube and TikTok links present on the legal-notice page.

## `extractPolicyUrls` (type: `boolean`):

The site's terms (CGV/CGU) and privacy-policy URLs, linked from the legal notice.

## `includeRawText` (type: `boolean`):

Attach the parsed page as plain text (capped at 40,000 characters) so you can audit a field or re-parse it with your own LLM.

## `includeRawHtml` (type: `boolean`):

Attach the legal-notice page's raw HTML (capped at 200,000 characters). Makes the dataset much larger.

## `includeMissRows` (type: `boolean`):

Emit a row for every domain that produced no lead — dead host, blocked, publishes no legal notice, filtered out, a duplicate, or suppressed by skipDomains / previousDatasetId — with its status and missReason, so you can do coverage accounting. These rows are NEVER charged.

## `flattenOutput` (type: `boolean`):

On: one flat row, ready for Google Sheets, Clay or a CSV export. Off: fields grouped into registry {}, people {}, contact {}, hosting {} and policies {} objects.

## Actor input object example

```json
{
  "domains": [
    "donatek.fr",
    "sapio-arts.fr",
    "pao-ceramique.fr",
    "coupdeciseaux.fr",
    "entreprise-caudron.fr"
  ],
  "maxItems": 1000,
  "maxDomains": 0,
  "maxDiscoveryRequestsPerDomain": 12,
  "perDomainTimeoutSecs": 60,
  "requestConcurrency": 10,
  "requestTimeoutSecs": 30,
  "maxRequestRetries": 2,
  "discoveryChannels": [
    "anchor",
    "sitemap",
    "wpJson",
    "pathGuess"
  ],
  "followWwwAndRootVariants": true,
  "deepJsDiscovery": false,
  "respectRobotsTxt": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "proxyCountry": "none",
  "escalateToResidentialOnBlock": true,
  "tldFilterMode": "none",
  "requireRegistryId": false,
  "requireContact": false,
  "minFieldsRequired": 0,
  "emailPolicy": "all",
  "dedupeBy": "siren-then-domain",
  "validateSiren": true,
  "deriveSirenFromVat": true,
  "extractHostingProvider": true,
  "extractDpo": true,
  "extractSocials": true,
  "extractPolicyUrls": true,
  "includeRawText": false,
  "includeRawHtml": false,
  "includeMissRows": false,
  "flattenOutput": true
}
```

# Actor output Schema

## `records` (type: `string`):

The dataset of parsed mentions-legales leads — one item per domain that published a legal notice.

# 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 = {
    "domains": [
        "donatek.fr",
        "sapio-arts.fr",
        "pao-ceramique.fr",
        "coupdeciseaux.fr",
        "entreprise-caudron.fr"
    ],
    "maxItems": 1000,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/mentions-legales-contact-scraper").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 = {
    "domains": [
        "donatek.fr",
        "sapio-arts.fr",
        "pao-ceramique.fr",
        "coupdeciseaux.fr",
        "entreprise-caudron.fr",
    ],
    "maxItems": 1000,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/mentions-legales-contact-scraper").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 '{
  "domains": [
    "donatek.fr",
    "sapio-arts.fr",
    "pao-ceramique.fr",
    "coupdeciseaux.fr",
    "entreprise-caudron.fr"
  ],
  "maxItems": 1000,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call scrapersdelight/mentions-legales-contact-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapersdelight/mentions-legales-contact-scraper"
        }
    }
}

```

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/ryVgyxItjhSGusW2V/builds/rYJJsdnICN48NSTup/openapi.json
