# Aviso Legal Scraper — Spain CIF, Registro Mercantil & Contacts (`scrapersdelight/aviso-legal-contact-scraper`) Actor

Turn Spanish company domains into verified B2B/KYB leads from each site's statutory Aviso Legal (Ley 34/2002): legal name, CIF/NIF with the official AEAT check digit, Registro Mercantil province + tomo/folio/hoja, domicilio social, email and phone.

- **URL**: https://apify.com/scrapersdelight/aviso-legal-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

from $5.00 / 1,000 per aviso legal 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

## 🇪🇸 Aviso Legal Scraper — Spanish company leads with a validated CIF

Paste a list of Spanish company domains (or point this at another Actor's dataset) and get back **one clean B2B/KYB lead per domain**, read off each site's **statutory Aviso Legal** page: the legal entity name, the **CIF/NIF checked against the official AEAT check-digit algorithm**, the Registro Mercantil entry (provincia + tomo / folio / hoja / inscripción), the domicilio social, and a role contact email and phone — for **$5 per 1,000 leads**.

**Why an Aviso Legal and not a directory?** Because **Ley 34/2002 (LSSI-CE) art. 10** obliges every commercial Spanish website to publish one. The corpus is therefore *the whole Spanish web*, not one directory's member list — and the data is a legally-mandated public disclosure the company published about itself.

***

### ✅ What does the Aviso Legal Scraper do?

For each domain you give it, two plain HTTP hops (no browser on the default path):

1. **Discovery.** Fetch the homepage and collect **every** candidate whose `href` *or its anchor text* matches the aviso-legal family (`aviso legal`, `avís legal`, `nota legal`, `información legal`, `mencions legals`, `textos legales`, `legal notice`…), plus a raw-HTML pass for string-literal hrefs a DOM parse misses. If nothing is linked, walk a bounded URL-guess ladder.
2. **Resolution.** Fetch each candidate, gate it on a **strict statutory marker**, extract the fields, **score** the page, and keep the **best-scoring** one.

Then it reads the fields off the page **text** with anchored patterns — never a loose regex over raw HTML — and validates the tax id locally.

#### The four things it does that a naive imprint scraper gets wrong

- 🎯 **It never stops at the first page that loads.** `tous.com` links `/us-es/legal-warning.html` from its footer. That page is real, it says "Aviso Legal", and it carries a **New York address and a US EIN**. This Actor scores every candidate (valid CIF +3 · Registro Mercantil +2 · domicilio social +1 · locale preference as a tiebreak) and comes back with the Spanish entity — `S. TOUS S.L.`, **CIF B08332074**, Registro Mercantil de **Barcelona**, Tomo 42.837 / Folio 89 / Hoja B-161.832, CP 08243 Manresa.
- 📍 **It ties the address to the company whose CIF it took.** A single Aviso Legal routinely lists several entities — a Spanish parent, a foreign subsidiary, a separate data-protection contact. `tous.com` writes the parent's address as running prose (*"con domicilio en Carretera de Vic…"*) and saves the literal words *"domicilio social"* for **TOUS USA INC.** Matching on the formal phrase alone pairs a Spanish CIF with a Rockefeller Center address. This Actor collects **every** address on the page and keeps the one **nearest the validated tax id** — the same anchor it uses for the company name.
- 🔒 **It refuses a page that merely contains the letters "CIF".** A loose marker "recovered" `mango.com`, `eroski.es` and `misako.com` in testing — all three were SPA shells or raw CSS text, and re-extraction returned **zero fields on all three**. A page is accepted only if it names the statute, the Registro Mercantil or the domicilio social, **or** carries a tax id that passes the AEAT check digit.
- 🧾 **It validates the CIF, offline.** The full official algorithm — NIF modulo-23 letter, NIE X/Y/Z prefix mapping, CIF double-odd-digit control with the letter/digit rules per first letter. **Zero external calls.** Every one of the **31 tax ids** extracted in the benchmark run below passed.

***

### 📊 Measured, from real runs

Everything here is from actual runs, not estimates.

#### End-to-end over 50 varied Spanish company domains

Shops, insurers, industrial co-ops, logistics, telecoms, retail — datacenter proxy, concurrency 8, default settings plus the opt-in extended path ladder.

| Outcome | Domains | Share |
|---|---:|---:|
| ✅ `ok` — a resolved, billable lead | **32** | **64 %** |
| 🟡 `partial` — page resolved, under the quality bar (**free**) | 2 | 4 % |
| ⚪ `no_aviso_legal_found` (**free**) | 7 | 14 % |
| 🔴 `blocked` by an anti-bot wall (**free**) | 9 | 18 % |

**34 of 50 domains (68 %) resolved a verified Aviso Legal page; 32 of 50 (64 %) produced a billable lead** — 50 domains in 2 min 51 s, and the run charged **32 events for 32 `ok` rows**.

#### Per-field fill, over the 32 resolved leads

| Field | Fill | | Field | Fill |
|---|---:|---|---|---:|
| `legalName` | **100 %** (32/32) | | `domicilioSocial` | **94 %** (30/32) |
| `legalForm` | **100 %** (32/32) | | `email` | 69 % (22/32) |
| `avisoLegalUrl` | **100 %** (32/32) | | `folio` | 69 % (22/32) |
| `taxId` | **97 %** (31/32) | | `registroMercantilProvincia` | 69 % (22/32) |
| `taxIdValid` — and **31/31 passed** | **97 %** | | `phone` | 69 % (22/32) |
| `taxIdLegalForm` (from the CIF letter) | 97 % (31/32) | | `tomo` | 62 % (20/32) |
| `province` | 97 % (31/32) | | `hoja` | 53 % (17/32) |
| `postalCode` | 91 % (29/32) | | `inscripcion` | 34 % (11/32) |
| `city` | 84 % (27/32) | | `dpoEmail` (LOPD address) | 25 % (8/32) |

#### Where the page was found

| Route | Domains |
|---|---:|
| Footer / body link on the homepage | 26 |
| URL-guess ladder (`/aviso-legal` ×4, `/aviso-legal/` ×1) | 5 |
| Extended ladder (`/condiciones-generales`) | 1 |

That last row is a real win worth knowing about: **eroski.es** has no `/aviso-legal` path and no aviso-legal link in its raw HTML, but its full legal block — `EROSKI S.Coop.`, Barrio de San Agustín s/n, 48230 Elorrio (Bizkaia), `dpo@eroski.es` — sits on `/condiciones-generales`. Turn on **Extended path ladder** when coverage matters more than request count. Cost control: a domain costs the homepage fetch plus **4.0 legal-page candidates on average** (measured over 40 domains; worst case 11, and `maxRequestsPerDomain` caps it at 12).

#### Transport

Sustained-load test on the winning transport (got-scraping over the Apify **datacenter** proxy): **30 of 31 sequential calls usable = 96.8 %** — 28 first-attempt, 2 recovered on a retry with a **fresh proxy IP**. The single failure was one domain hammered repeatedly; in real use a buyer touches each domain once.

Measured on the escalation ladder: direct home IP `200 / 691,872 b`, datacenter proxy `200 / 691,890 b` (identical fields extracted), residential-ES `200 / 760,285 b`. **The cheapest rung already returns real data**, so datacenter is the default and residential is only an override.

***

### 🧾 Output

One row per input domain.

| Field | Description |
|---|---|
| `domain` | Registrable domain (eTLD+1) — the stable id. One domain = one row = one charge. |
| `website` | The company homepage. |
| `avisoLegalUrl` | The page the data came from. |
| `discoveryMethod` | `footer` · `guess:/aviso-legal` · `extended:/…` · `contact-page` · `render:…` · `none`. |
| `legalName` | Denominación social. |
| `legalForm` | `S.L.` · `S.A.` · `S.L.U.` · `S.A.U.` · `S.Coop.` · `S.Coop.V.` · `S.C.P.` · `A.I.E.` · … |
| `taxId` | CIF / NIF / NIE, normalised (`A-46.581.666` → `A46581666`). |
| `taxIdType` | `CIF` · `NIF` · `NIE`. |
| `taxIdValid` | **Official AEAT check digit.** `true` = a real, well-formed Spanish fiscal id. |
| `taxIdLegalForm` | Entity type decoded from the CIF's first letter (A = S.A., B = S.L., F = cooperativa, G = asociación/fundación, N = entidad extranjera, …). |
| `registroMercantilProvincia` | The register province, resolved against the closed set of 52 (Spanish, Catalan, Basque and Galician spellings all map to one canonical name). |
| `tomo`, `folio`, `hoja`, `inscripcion` | The Registro Mercantil entry. |
| `domicilioSocial` | The registered-office line. |
| `postalCode`, `city`, `province` | Postal address, province derived from the CP (01–52). |
| `email` | Role contact address (`info@`, `legal@`, `contacto@`, `lopd@`…). |
| `dpoEmail` | The dedicated data-protection address when the site publishes one (`lopd@`, `protecciondatos@`, `dpo@`) — a direct line to the compliance owner. |
| `emailMxValid` | Only when **Verify MX** is on: does the email's domain actually accept mail? |
| `phone` | Spanish phone, normalised to `+34XXXXXXXXX`. |
| `administrador` | Administrador / representante legal — **only** when you enable personal names. |
| `status` | `ok` · `partial` · `no_aviso_legal_found` · `blocked` · `error` · `not_attempted`. Only `ok` is ever charged. |
| `fetchedAt` | ISO timestamp. |
| `emails[]`, `phones[]`, `allUrlsTried`, `candidateScore`, `renderedWithBrowser` | Output mode **Full**. |
| `rawText` | Only when **Include the raw Aviso Legal text** is on. |

Two dataset views ship with it: **Spanish company leads** (the flat lead) and **Registry / KYB view** (the Registro Mercantil entry plus the validated fiscal id).

***

### ⚙️ Input

Bring your domains any of five ways — they are merged, normalised and deduplicated before a single request goes out:

| Input | What it is |
|---|---|
| **Company domains** | `["suavinex.com", "https://www.joyeriasuarez.com", "polvillo.es/aviso-legal"]` |
| **Start URLs** | The Apify Start-URLs shape, so the Console link-list UX and file uploads work. |
| **Source dataset ID** | Chain straight off a Google-Maps-Spain / Páginas Amarillas run. Pick the field with **Source dataset field** (default `website`, auto-falls back through `url`, `domain`, `web`, `site`, `homepage`, `link`, `companyWebsite`). |
| **Domain list URL** | A public `.txt` / `.csv` / `.tsv` for very large lists. |
| **Exclude domains** | Your suppression list — applied **before** any fetch, so an excluded domain never costs a request and can never be billed. |

Then 31 more knobs, grouped in the Console: limits & performance, network & anti-block, discovery, enrichment & data quality, filters & output. The ones worth knowing:

- **Score every candidate page** *(on by default — leave it on)* — the `tous.com` fix above.
- **Preferred locales** — used to **score** candidates and to try locale variants; never to rewrite a URL, because hard-forcing `/es-es/` **404s** on joyeriasuarez.com.
- **Extended path ladder** — the `/condiciones-generales` class. Opt-in, because it costs extra requests per unresolved domain.
- **Filter by legal form (CIF first letter)** — `["A"]` for sociedades anónimas only, `["F"]` for cooperativas, `["G"]` for asociaciones y fundaciones. CIF-keyed prospecting, straight off the validated id.
- **Filter by province / postal-code prefix** — Madrid + Barcelona only: `provinceFilter: ["Madrid","Barcelona"]` or `postalCodeFilter: ["28","08"]`.
- **Minimum fields for a billable lead** — how many of `{taxId, legalName, email, phone}` a page must yield before the row counts as `ok` and is charged. Default 2. **You set your own quality bar, and it is also the billing bar.**
- **Render JavaScript** — opt-in headless Chromium for the client-side-rendered-footer class. **Set the run memory to 2048 MB** when you enable it; below 1536 MB the Actor logs a warning and stays on HTTP rather than risk an out-of-memory kill.

```json
{
  "domains": ["joyeriasuarez.com", "suavinex.com", "chocolatesatienza.com"],
  "provinceFilter": ["Bizkaia", "Alicante"],
  "legalFormFilter": ["B"],
  "minFieldsRequired": 2,
  "outputMode": "lead"
}
```

***

### 💵 Pricing

**Pay per event — $0.005 per resolved lead** (`$5 / 1,000`).

- You are charged **only** for a row that resolved an Aviso Legal page **and** met your `minFieldsRequired` bar (`status: ok`).
- `blocked`, `no_aviso_legal_found`, `partial` and `error` rows are still delivered so you can see what happened to every domain you submitted — and they are **never charged**.
- Rows are **delivered and billed atomically** (`pushData(row, event)`), so a charge cap truncates delivery too. You can never be billed for a row you did not receive, and never receive a row you were not billed for.
- Rows dropped by a filter, by `dedupeByTaxId` or by `onlyResolved` are never pushed and never billed.
- The **opt-in** browser fallback bills its own event, `aviso-legal-rendered`, at **$0.008** — it costs a full Chromium, so it is priced separately and only fires when you turn it on.

***

### 🚧 Honest limits

**This is an enrichment tool, not a discovery tool.** It does not find Spanish companies for you; it turns a domain list you already have into legal-entity records.

**Four classes of domain do not resolve, and all four come back free.** Measured on the 50-domain benchmark:

| Class | Domains in the benchmark | What happens |
|---|---|---|
| Enterprise CF/WAF | `idealista.com`, `dynos.es`, `comsa.com`, `mahou-sanmiguel.com`, `cofas.es`, `urssa.es`, `batz.com`, `pccomponentes.com` | 403 on **both** datacenter **and** residential-ES — residential is **not** the fix, which is why the escalation toggle is off by default. |
| Fastly proof-of-work interstitial | `elganso.com` | HTTP 202 instead of the page. Not solvable over plain HTTP. |
| Rate limit | `kavehome.com` | 429. Lower the concurrency and retry later. |
| Client-side-rendered footer | `mango.com`, `misako.com` | 200 OK, but the aviso-legal href exists nowhere in the raw HTML and there is no `__NEXT_DATA__` / `__NUXT__` to mine. This is what **Render JavaScript** is for. |
| No published Aviso Legal on a discoverable path | `monbus.es`, `silennis.com`, `tallerescillero.es`, `casadellibro.com`, `espritdog.com` | Try the **Extended path ladder** and **Fall back to /contacto**. |

**Block behaviour is not deterministic, in both directions.** Across three full benchmark passes `pccomponentes.com` was 403, then resolved cleanly (`PC COMPONENTES Y MULTIMEDIA S.L.U.`, **B73347494**, Registro Mercantil de Murcia, Tomo 2236 / Folio 52 / Hoja MU-52949) off `/pages/aviso-legal`, then 403 again; `cruzcampo.es` and `elganso.com` each went the other way and resolved; `eroski.es` went from an unresolvable SPA shell to a clean lead via the extended ladder. So the 64 % headline is one sample, not a constant — expect a few points either way, and **re-running a `blocked` list later costs nothing, because blocked rows are free.**

**Extraction is best-effort per field.** Aviso Legal pages are free-form prose on thousands of unrelated stacks; a field the page does not publish comes back `null`. The fill table above is the honest distribution, not a promise. `city` in particular is a secondary field parsed out of an address line and can keep a trailing word on unusual layouts.

**An empty address field is deliberate.** `domicilioSocial` must look like an address — a street-type word, a Spanish postal code, or a street number — before it is returned. Spanish legal notices are full of the *word* "domicilio" in boilerplate that is not an address at all ("los tribunales del domicilio del consumidor", or a site quoting LSSI art. 10's own wording back at you), and three domains in the 50-domain benchmark used to return that sentence fragment as their registered office. Where a page prints a real address but never uses the word — "registered office at", "con sede en", or just the address sitting beside the CIF — the postal code is used as the anchor instead, which is what carries this field to 94 %.

**`taxIdValid` proves the id is well-formed, not that the company is trading.** The AEAT check digit is arithmetic, not a registry lookup. For live registry status you want a BORME / Registro Mercantil source — that is a different data path and a separate Actor.

**No SMTP.** `emailMxValid` says the email's domain accepts mail; it does not say the mailbox exists. Nothing on this platform can say that honestly.

**A slow domain cannot silently eat your run.** One domain behind a flaky wall can spend `maxRequestsPerDomain × maxRetriesPerRequest × requestTimeoutSecs` before giving up — measured, eight identical 3-domain runs on build 0.1.5 took 7.7 – 136.3 s, and the 136.3 s run returned exactly the same three rows as the 7.7 s one. Three clocks now bound that: each request is **cancelled at its own `requestTimeoutSecs`** (measured: a stalled site ran 22.0 s against a 20 s timeout the HTTP client failed to enforce, and only the cancel stopped it), each **domain** gets 60 s of wall clock to widen its search before the Actor stops enlarging it, and the run stops starting new domains at **80 % of the run's own timeout**, delivering the remainder as `not_attempted` (free) with a warning naming the count. Re-measured on 0.1.8, same input, 14 runs: median ~10 s, **worst 64.8 s**. Raise the run timeout or split the list to finish `not_attempted` domains.

***

### ⚖️ Legal & fair use

The **Aviso Legal is a statutory public disclosure**, mandated by **Ley 34/2002 de Servicios de la Sociedad de la Información y de Comercio Electrónico (LSSI-CE), artículo 10** — the company itself is legally required to publish exactly these identifying details, in a permanent, freely and directly accessible form. That makes it the most defensible starting point there is for Spanish B2B prospecting: no login, no private profile, no paywall.

- **robots.txt** is per-domain. The **Respect robots.txt** toggle is exposed so you can enforce it on your runs; check the specific domains you care about.
- **Personal data is OFF by default.** `administrador` / representante legal names (`includePersonalNames`) and `nombre.apellido@` addresses (`includePersonalEmails`) are both opt-in. Turning them on makes you the data controller for those fields under the **RGPD/LOPDGDD**.
- **Outreach compliance is yours.** Spanish commercial email is governed by LSSI-CE art. 21 and the RGPD; you are responsible for lawful basis, opt-out and record-keeping when you contact anyone from this data.
- You are responsible for complying with each site's Terms of Service.

# Actor input Schema

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

One entry per Spanish company. Paste bare domains ("suavinex.com"), full homepage URLs ("https://www.joyeriasuarez.com") or a direct Aviso Legal URL. Everything is normalised to the registrable domain (eTLD+1), so one domain = one row = one charge.

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

The same input in Apify's Start-URLs shape, so the Console link-list UX and "link list from a file" uploads work. Merged with Company domains.

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

Chain straight off another Actor's dataset — e.g. a Google Maps Spain or Páginas Amarillas run — instead of pasting a list. Every item's website field becomes an input domain.

## `sourceDatasetField` (type: `string`):

Which field of the source dataset holds the website. Falls back automatically through website, url, domain, web, site, homepage, link, companyWebsite.

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

Public URL of a newline-delimited .txt or a .csv/.tsv of domains, for very large lists. A CSV header naming the source field (or website/url/domain) picks the column; otherwise the most domain-looking column is used.

## `excludeDomains` (type: `array`):

Already-contacted or unwanted domains. Applied BEFORE any request is made, so an excluded domain never costs a fetch and can never be billed.

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

Hard cap on domains processed this run — your billing guard. Default 1000. Set 0 to process the whole input list.

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

Domains fetched in parallel. Measured stable at 6-8; ECONNRESETs start appearing as this climbs toward 20.

## `maxRequestsPerDomain` (type: `integer`):

Caps the discovery ladder (homepage + footer candidates + URL guesses). Measured average is 5.5 requests per domain.

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

Per-request timeout.

## `maxRetriesPerRequest` (type: `integer`):

Attempts per URL, each on a FRESH proxy IP. Measured: 2 of 30 sustained-load calls only succeeded on attempt 2 or later, and the HTTP client's own retry reuses the flagged IP — so this loop is what buys the 96.8% success rate.

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

Apify DATACENTER proxy is the default and is MEASURED sufficient — it returned byte-identical pages to a direct connection across the 50-domain corpus. Residential is available as an override but does not fix the enterprise-WAF failures.

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

Country pin. Applies ONLY when the proxy above uses the RESIDENTIAL group (datacenter pools ignore a country pin).

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

Retry a 403/429 on RESIDENTIAL-ES. Default OFF because it was MEASURED not to fix the enterprise-WAF class (pccomponentes.com is 403 on both datacenter and residential-ES) — it costs residential traffic for nothing on that class.

## `renderJavaScript` (type: `boolean`):

Opt-in headless-Chromium pass for sites whose footer is client-side rendered, where the Aviso Legal link exists nowhere in the raw HTML (mango.com, eroski.es, misako.com). REQUIRES the run memory to be set to 2048 MB — below 1536 MB the Actor logs a warning and stays on HTTP rather than risk an out-of-memory kill. Rendered rows bill the separate, higher aviso-legal-rendered event.

## `userAgent` (type: `string`):

Advanced: replace the default desktop Chrome User-Agent.

## `customHeaders` (type: `object`):

Advanced: extra request headers merged over the defaults (Accept-Language is es-ES,es;q=0.9,en;q=0.7).

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

Read each site's robots.txt and skip URLs its wildcard user-agent group disallows. Off by default; the Aviso Legal page is a statutory public disclosure under Ley 34/2002 art. 10 and is normally crawlable.

## `avisoLegalPaths` (type: `array`):

Tried when the homepage exposes no Aviso Legal link. Measured: 4 of 50 domains resolve here (/aviso-legal x3, /aviso-legal/ x1). Extend or replace freely.

## `extendedPaths` (type: `array`):

A second, slower tier tried only when the first ladder finds nothing. Measured win: forumsport.com's full LSSI block (CIF A48450456, Registro Mercantil de Bizkaia, Tomo 95) lives on /condiciones-generales, not on any /aviso-legal path. Empty by default because it costs extra requests per unresolved domain.

## `scoreAllCandidates` (type: `boolean`):

Evaluate EVERY discovered Aviso Legal candidate and keep the highest-scoring one (valid CIF +3, Registro Mercantil +2, domicilio social +1, locale preference as a tiebreak) instead of accepting the first page that loads. Keep this ON: first-hit-wins is what made tous.com resolve to /us-es/ and return a New York address and a US EIN instead of the Spanish entity.

## `preferredLocales` (type: `array`):

Ordered locale preference used to SCORE candidate URLs, and to add locale variants as extra candidates. Never used to rewrite a URL outright — hard-forcing /es-es/ 404s on joyeriasuarez.com. The empty entry means "a path with no locale segment".

## `contactPageFallback` (type: `boolean`):

If no Aviso Legal page resolves, try the contact page for an email and phone only. The row is marked status=partial and is delivered FREE — it is never charged.

## `validateTaxId` (type: `boolean`):

Run the official AEAT check-digit algorithm on every extracted tax id and emit taxIdValid + taxIdType. Zero external calls. Measured: 25 of 25 tax ids extracted across the probe corpus PASSED.

## `verifyMx` (type: `boolean`):

DNS MX lookup on the extracted email's domain; emits emailMxValid. Adds a DNS round-trip per unique domain.

## `includePersonalNames` (type: `boolean`):

Extract the administrador / representante legal person name. Personal data under the GDPR — OFF by default; enabling it makes you the controller for that field.

## `includePersonalEmails` (type: `boolean`):

Include name.surname@ style addresses. OFF by default — only role addresses (info@, legal@, lopd@, contacto@ …) are returned.

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

How many of {taxId, legalName, email, phone} a page must yield before the row counts as a resolved lead (status=ok) and is therefore charged. Anything below the bar is delivered as status=partial, free.

## `legalFormFilter` (type: `array`):

Keep only companies whose CIF starts with one of these letters — the CIF-keyed prospecting lever. A = S.A., B = S.L., F = cooperativa, G = asociación/fundación, J = sociedad civil, N = entidad extranjera, U = UTE, W = establecimiento permanente. Empty = keep everything.

## `provinceFilter` (type: `array`):

Keep only rows whose Registro Mercantil province — or, failing that, the province derived from the postal code — is in this list. Accepts Spanish, Catalan, Basque and Galician names (Vizcaya = Bizkaia, Gerona = Girona). Empty = keep everything.

## `postalCodeFilter` (type: `array`):

Explicit CP prefixes, e.g. \["28", "08"] for Madrid and Barcelona, or \["280"] for a tighter cut. Empty = keep everything.

## `onlyResolved` (type: `boolean`):

Drop blocked / not-found / partial rows entirely. OFF by default so you can see exactly what happened to every domain you submitted — those rows are always free.

## `dedupeByDomain` (type: `boolean`):

Collapse subdomains onto their registrable domain (eTLD+1) before fetching. Exact duplicate hostnames are ALWAYS collapsed regardless of this switch — that is the double-charge guard.

## `dedupeByTaxId` (type: `boolean`):

Collapse several brand domains that share one validated CIF into a single row. Off by default because you usually want a row per domain you submitted.

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

Attach the page's plain text (rawText) for audit or LLM post-processing. Off by default — it multiplies dataset size.

## `outputMode` (type: `string`):

"Lead" is one flat row per domain. "Full" adds emails\[], phones\[], allUrlsTried, the candidate score and the render flag.

## Actor input object example

```json
{
  "domains": [
    "joyeriasuarez.com",
    "suavinex.com",
    "chocolatesatienza.com"
  ],
  "startUrls": [],
  "sourceDatasetId": "",
  "sourceDatasetField": "website",
  "domainsFileUrl": "",
  "excludeDomains": [],
  "maxItems": 1000,
  "requestConcurrency": 8,
  "maxRequestsPerDomain": 12,
  "requestTimeoutSecs": 20,
  "maxRetriesPerRequest": 3,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "proxyCountry": "none",
  "escalateToResidentialOnBlock": false,
  "renderJavaScript": false,
  "userAgent": "",
  "customHeaders": {},
  "respectRobotsTxt": false,
  "avisoLegalPaths": [
    "/aviso-legal",
    "/aviso-legal/",
    "/es/aviso-legal",
    "/pages/aviso-legal",
    "/aviso-legal.html",
    "/legal",
    "/avisolegal",
    "/aviso_legal",
    "/nota-legal",
    "/informacion-legal"
  ],
  "extendedPaths": [],
  "scoreAllCandidates": true,
  "preferredLocales": [
    "es-es",
    "es",
    "ca",
    "eu",
    "gl",
    ""
  ],
  "contactPageFallback": false,
  "validateTaxId": true,
  "verifyMx": false,
  "includePersonalNames": false,
  "includePersonalEmails": false,
  "minFieldsRequired": 2,
  "legalFormFilter": [],
  "provinceFilter": [],
  "postalCodeFilter": [],
  "onlyResolved": false,
  "dedupeByDomain": true,
  "dedupeByTaxId": false,
  "includeRawText": false,
  "outputMode": "lead"
}
```

# Actor output Schema

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

The dataset of Spanish company leads extracted from each domain's Aviso Legal page (one item per domain).

# 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": [
        "joyeriasuarez.com",
        "suavinex.com",
        "chocolatesatienza.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/aviso-legal-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": [
        "joyeriasuarez.com",
        "suavinex.com",
        "chocolatesatienza.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/aviso-legal-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": [
    "joyeriasuarez.com",
    "suavinex.com",
    "chocolatesatienza.com"
  ]
}' |
apify call scrapersdelight/aviso-legal-contact-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapersdelight/aviso-legal-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/hMPwgLBJROLZqk09r/builds/HJjEhIISbDbxJRNPS/openapi.json
