# Pymes en España: leads de Google Maps con email (dominio MX) (`rianone/gmaps-lead-gen-hispano-http`) Actor

Busca pymes españolas en Google Maps por tipo de negocio y ciudad y devuelve un lead por lugar: nombre, categoría, dirección, teléfono, web, rating, reseñas y email de contacto público con dominio MX (no valida el buzón individual). Solo mercado español; sin navegador, rápido y barato.

- **URL**: https://apify.com/rianone/gmaps-lead-gen-hispano-http.md
- **Developed by:** [Riccardo Pongetti](https://apify.com/rianone) (community)
- **Categories:** Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 lugar emitidos

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

## Pymes en España: leads de Google Maps con email (dominio MX)

Actor Apify que convierte una lista de búsquedas ("tipo de negocio + ciudad española")
en una **lista de leads de pymes españolas con su email de contacto público y el estado
del dominio de ese email (registros MX)**.

Un item por lugar:

```
nombre, categoria, direccion, ciudad, pais, telefono, web, rating, numeroResenas,
email, emailDominioMx, emailDominioMxDetalle, emailAlternativos,
emailMetodoVerificacion, placeId, url, latitud, longitud
```

**Solo mercado español** (`country` = `ES`). No usa navegador, así que es rápido y barato.

### Por qué es barato (diseño)

No usa navegador. Dos peticiones HTTP por búsqueda:

1. `GET /maps/search/<búsqueda>?hl=es&gl=ES` — el HTML incluye un `<link>` de prefetch a
   `/search?tbm=map&...&pb=...` con la búsqueda ya serializada.
2. `GET` de ese link — devuelve `)]}'` + JSON con **los 20 lugares de la primera página**
   (nombre, dirección, teléfono, web, rating, reseñas, Place ID, país, coordenadas).

Navegar con Playwright costaba ~2,9 s por lugar (página de listado + página de detalle);
este enfoque cuesta ~0,065 s por lugar de crawl y no necesita instalar Chromium
(imagen Docker pequeña, 1 GB de memoria suficiente).

Después, para cada negocio **con web**, se piden (best-effort) la home y 1 página de
contacto típica y se extraen los correos **publicados** por el propio negocio. Cada petición
(y cada salto de redirect) pasa por la política anti-SSRF de `src/url_safety.py`: solo
`http`/`https`, DNS resuelto y **rechazo de loopback, rangos privados, link-local
(169.254.x.x), multicast, reservadas y CGNAT**, y TLS verificado.

### Qué comprueba `emailDominioMx` (y qué NO)

| Método | Qué comprueba | Coste | ¿Funciona en Apify? |
|---|---|---|---|
| `mx` (por defecto) | El **dominio** del email tiene registros MX (ese dominio puede recibir correo) | ~10 ms | Sí |
| `smtp` | `RCPT TO` sin enviar mensaje, **además** del MX | ~1-6 s | No: la plataforma bloquea el puerto 25 → estado `desconocido` |
| `ninguna` | — | 0 | — |

**Importante:** el actor **no** comprueba que el buzón individual exista o sea entregable.
Por eso el campo se llama `emailDominioMx` y el evento `email-dominio-mx`, no
"email verificado". Valores del campo:

- `mx_presente` — el dominio tiene registros MX (puede recibir correo).
- `sin_mx` — el dominio no tiene registros MX (no puede recibir correo).
- `null` / vacío — no se pudo determinar (timeout DNS) o no se pidió verificación.

Con el método `smtp` se añaden `emailSmtpEstado` (`smtp_aceptado`, `smtp_rechazado`,
`smtp_temporal`, `desconocido`) y `emailMetodoVerificacion` = `mx+smtp`. Las respuestas
4xx de SMTP (450, 452…) son **temporales**: no se etiquetan como rechazo permanente; solo
los 5xx (550, 551, 553, 554…) son `smtp_rechazado`.

### Entrada

- `searchTerms` (obligatorio): p. ej. `["clínica dental Madrid", "panadería Sevilla"]`.
  **Google Maps devuelve hasta ~20 resultados por búsqueda**; para más leads, añade
  búsquedas por ciudad/barrio (50 búsquedas ≈ 1.000 leads).
- `maxResults` (0 = sin límite), `country` (solo `ES`), `extraerEmail`,
  `metodoVerificacion`, `maxEmailsPorNegocio`, `maxPaginasWeb`, `emailConcurrencia`,
  `timeoutWebSegundos`, `usarProxy`, `proxyGrupo`, `exportFiles`, `exportLanguage`.

### Salida

- Dataset: un item por lugar.
- Key-value store: `RESULT-sintesis.json` (resumen del run), `leads-es.csv` (RFC-4180)
  y `leads-es.xlsx` (cabecera congelada, autofiltro, números reales).

### Precio (pay-per-event, precios ACTIVOS)

- `lugar` — 0,002 $ por lugar emitido.
- `email-dominio-mx` — 0,003 $ por lugar con email cuyo **dominio tiene MX**.
- `informe` — 0,01 $ por run.

### Local (sin token)

```bash
uv venv .venv --python 3.13
uv pip install --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest tests -q               # 59 tests, sin red
.venv/bin/python -m src.main --local examples/input.json
```

### Coste medido (run real en plataforma, plan free)

Run `9BA8vn8qnTWFE2z6Z` (22-09-2026, proxy datacenter, 1 GB RAM): 5 búsquedas → **99 lugares,
54 con email de dominio con MX**, 81,4 s, **0,0226 CU = 0,00539 $**.

| | medido | por 1.000 lugares |
|---|---|---|
| Coste total (CU + storage + transferencia) | 0,00539 $ / 99 | **0,0545 $** |
| Coste CU | 0,0226 CU / 99 | 0,228 CU = 0,0457 $ |
| Emails con dominio MX | 54 (55 % de lugares) | ~545 |
| **Coste por email con MX** | — | **0,0001 $** |

Proxy datacenter incluido en el plan (0 $ de proxy residencial). Detalle completo en
[`INFORME-COSTE.md`](./INFORME-COSTE.md).

> Estimación de cobro PPE (aritmética, **no ingreso observado**): un volumen como el del run
> de medida (99 lugares, 54 emails, 1 run) serían 99×0,002 + 54×0,003 + 0,01 = **0,37 $**.
> Para 1.000 lugares en 50 runs (≈545 emails con MX): 2,00 + 1,635 + 0,50 = **4,135 $**.
> El coste de producción de esos 1.000 lugares es ~0,055 $.

### Roadmap

1. **Más de 20 lugares por búsqueda**: rejilla de viewports (el `pb` de Google admite
   centro/zoom) para multiplicar resultados por término y bajar el coste por 1.000.
2. **Verificación del buzón individual**: integración con un verificador de dirección real
   (API externa) como campo aparte, sin renombrar el actual.
3. **Filtros de calidad**: descartar negocios sin web, o sin teléfono, o con rating bajo.
4. **Enriquecimiento**: redes sociales, horarios, "cerrado permanentemente" (Google lo
   informa en el payload: `p[203]`).
5. **Verticales**: plantillas de búsquedas por sector (clínicas, talleres, hostelería).

### Limitaciones

- Los selectores/estructura del payload de Google cambian sin aviso: el parser está aislado
  en `src/maps_http.py` y hay un fixture real en `tests/fixtures/` para detectarlo en tests.
- `emailDominioMx` valida el **dominio**, no el buzón: un `mx_presente` no garantiza que la
  dirección concreta reciba correo.
- Queda un TOCTOU mínimo entre la resolución DNS y la conexión (DNS rebinding); cerrarlo del
  todo exige fijar la IP resuelta en la conexión.
- La extracción de email es best-effort: muchas pymes no publican correo (usan formulario).
- Uso responsable: datos públicos de Google Maps; respeta los ToS y el RGPD al usar los leads.

# Actor input Schema

## `searchTerms` (type: `array`):

Lista de búsquedas tipo 'tipo de negocio + ciudad española'. Google Maps devuelve hasta ~20 resultados por búsqueda; para más leads, añade búsquedas por ciudad/barrio.

## `maxResults` (type: `integer`):

Tope de lugares a emitir en el run (control de gasto).

## `country` (type: `string`):

País de la búsqueda (parámetro gl de Google). Solo mercado español.

## `extraerEmail` (type: `boolean`):

Visita la web de cada lugar con sitio y busca correos publicados (mailto/regex, best-effort). Solo emails públicos.

## `metodoVerificacion` (type: `string`):

mx = el dominio tiene registros MX (rápido y fiable en Apify). smtp = chequeo SMTP sin enviar mensaje (puede fallar: el puerto 25 sale bloqueado en la plataforma). ninguna = no verificar.

## `maxEmailsPorNegocio` (type: `integer`):

Número máximo de correos candidatos a devolver por negocio (el primero va en 'email' y el resto en 'emailAlternativos').

## `maxPaginasWeb` (type: `integer`):

Homepage + (n-1) páginas de contacto típicas. Más páginas = más coste de tiempo.

## `emailConcurrencia` (type: `integer`):

Número de webs de negocios que se piden en paralelo en la fase de emails.

## `timeoutWebSegundos` (type: `number`):

Tiempo máximo por petición a la web de un negocio. Bajarlo abarata el run (las webs lentas se descartan).

## `usarProxy` (type: `boolean`):

Recomendado para evitar bloqueos de Google. El proxy solo se usa para Google; las webs de los negocios se piden directamente.

## `proxyGrupo` (type: `string`):

Vacío = datacenter por defecto. RESIDENTIAL si Google bloquea el tráfico datacenter (más caro).

## `exportFiles` (type: `boolean`):

Guarda leads-<pais>.csv y leads-<pais>.xlsx en el key-value store del run.

## `exportLanguage` (type: `string`):

Parámetro hl de Google (idioma de los resultados).

## Actor input object example

```json
{
  "searchTerms": [
    "clínica dental Madrid"
  ],
  "maxResults": 200,
  "country": "ES",
  "extraerEmail": true,
  "metodoVerificacion": "mx",
  "maxEmailsPorNegocio": 3,
  "maxPaginasWeb": 2,
  "emailConcurrencia": 8,
  "timeoutWebSegundos": 6,
  "usarProxy": true,
  "proxyGrupo": "",
  "exportFiles": true,
  "exportLanguage": "es"
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

## `csv` (type: `string`):

No description

## `xlsx` (type: `string`):

No description

# API

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

## JavaScript example

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

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

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("rianone/gmaps-lead-gen-hispano-http").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("rianone/gmaps-lead-gen-hispano-http").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 '{}' |
apify call rianone/gmaps-lead-gen-hispano-http --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,rianone/gmaps-lead-gen-hispano-http"
        }
    }
}
```

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/PQ5hFUwY82lV0JgaM/builds/WDtUZagp0J3v4RZNt/openapi.json
