# Milanuncios Scraper — Spain Classifieds Ads & Prices (`khadinakbar/milanuncios-scraper`) Actor

Scrape Milanuncios.com Spain classifieds by keyword, category, province, or URL. Flat MCP-ready rows: title, EUR price, city/province, seller type, images, dates. HTTP-only via **INITIAL\_PROPS** + Spanish residential proxy. Cookieless, no login.

- **URL**: https://apify.com/khadinakbar/milanuncios-scraper.md
- **Developed by:** [Khadin Akbar](https://apify.com/khadinakbar) (community)
- **Categories:** E-commerce, Lead generation, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 listing scrapeds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## Milanuncios Scraper — Spain Classified Ads & Prices

Use this Apify Actor to scrape **public Milanuncios Spain classified ads** by keyword, category, province, or Milanuncios search/detail URL. Resellers, dealers, price monitors, and AI agents get flat MCP-ready rows with EUR price, city/province, seller type, images, and dates — cookieless, no login, HTTP-only.

Use this Actor when you need Spanish second-hand inventory from Milanuncios into JSON. When the next workflow is Spanish real-estate portals or another marketplace, continue with a sibling Actor instead.

### Best fit for this Actor

- You need **public Milanuncios listings** across Spain (phones, cars, property, pets, and other categories).
- You want flat fields (`title`, `priceEur`, `city`, `province`, `sellerType`, `url`, `imageUrl`) plus optional detail enrichment.
- You want one MCP-ready tool with terminal `OUTPUT.outcome` and Pay per event billing.
- For Idealista/Fotocasa property-only workflows, Wallapop, or private seller phones, route to a different tool.

### Use case: Madrid iPhone sourcing sheet

A reseller preparing a weekend buy list runs this Actor with `keyword: "iphone"`, `category: "telefonos-moviles"`, `province: "madrid"`, and `maxItems: 20`. Each row has `title`, `priceEur`, `city`, and `url`. They sort by price, open the best five ad pages, then turn on `fetchDetails` for a shortlist of three to pull seller score, attributes, and contact flags before messaging.

### Quick start input

```json
{
  "keyword": "iphone",
  "category": "telefonos-moviles",
  "province": "madrid",
  "maxItems": 5,
  "maxPages": 1,
  "fetchDetails": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"],
    "apifyProxyCountry": "ES"
  }
}
```

Also supports `startUrls` (search or `-{id}.htm` detail), price filters, seller type, sort, and `stripSellerPII`. Prefill keeps quality tests at five iPhone mobiles listings.

### What data you receive

One dataset row per unique public ad.

```json
{
  "listingId": "536827480",
  "title": "IPhone",
  "priceEur": 15,
  "currency": "EUR",
  "condition": "mejorable",
  "sellerType": "private",
  "city": "Sabadell",
  "province": "Barcelona",
  "category": "iPhone",
  "categoryPath": "Telefonía > Móviles > Teléfonos móviles > iPhone",
  "url": "https://www.milanuncios.com/iphone/iphone-536827480.htm",
  "imageUrl": "https://images.milanuncios.com/api/v1/example.jpg",
  "publishDate": "2026-05-04T13:56:22Z",
  "scrapedAt": "2026-09-21T12:00:00.000Z"
}
```

| Field | Description |
|-------|-------------|
| title, priceEur, condition | Listing card basics |
| city, province, url, imageUrl | Where and how to open it |
| sellerType | `private` or `professional` |
| description, attributes, seller\* | When `fetchDetails` or direct detail URLs |
| publishDate / updateDate | Timestamps when Milanuncios exposes them |

### Pricing

Pay per event plus platform usage (`isPPEPlatformUsagePaidByUser: true`). Open the live Pricing tab in Apify Console for the current source of truth.

| Event | When | Price |
|---|---|---:|
| `apify-actor-start` | Every run | $0.00005 |
| `listing-scraped` | Each accepted search-card row | $0.005 |
| `listing-enriched` | Each accepted detail/enrich row | $0.008 |

A 5-listing search without enrich is about `$0.02505` in events plus Residential ES proxy/compute. Invalid input and valid-empty searches leave listing events unbilled.

### How it works

1. Primary path: HTTP Cheerio reads Milanuncios embedded `window.__INITIAL_PROPS__` (search cards ~41/page; detail `props.ad`). **Apify Residential ES is required** — datacenter IPs get Akamai interruptions.
2. Search pagination uses `?pagina=N` up to `maxPages` / `maxItems`.
3. Each accepted row is charged then pushed (`Actor.charge` → `pushData`).
4. Terminal `OUTPUT` / `RUN_SUMMARY` use COMPLETE, PARTIAL, VALID\_EMPTY, INVALID\_INPUT, UPSTREAM\_FAILED, or CONFIG\_ERROR.

### Honest contract and best results

- Public Milanuncios listing pages only. No Milanuncios account, private chat, or unlocked seller phone numbers.
- Empty search results → `VALID_EMPTY` (succeeded, zero listing charges).
- Total block with zero rows → `UPSTREAM_FAILED` (honest upstream outage).
- Keep `maxItems` small while testing; raise it only after a keyword+category returns useful cards.
- Prefer category + province over bare keywords for cleaner inventory.
- Leave `fetchDetails` off for discovery; turn it on for shortlists that need seller/attribute fields.
- Always pin `apifyProxyCountry: "ES"` — non-ES exits often fail Akamai checks.
- Enable `stripSellerPII` when you need enriched attributes without private-seller identity fields.

### Why agents choose this Actor

- One tool for keyword builders, category/province filters, and paste-ready Milanuncios URLs.
- Predictable PPE with usage pass-through and a hard `maxItems` ceiling.
- Flat fields stay inside a small token budget for MCP clients.
- Terminal `OUTPUT.outcome` makes empty, upstream-outage, and invalid-input states machine-readable.

### Agent checklist

1. Supply `keyword` + optional `category` / `province`, or Milanuncios `startUrls`.
2. Cap `maxItems` to the budget you can spend at `$0.005` (or `$0.008` when enriched).
3. Keep Residential ES in `proxyConfiguration`.
4. After the run, read `OUTPUT.outcome`, then the dataset.
5. Zero rows with `VALID_EMPTY` means the query matched nothing public — change keyword, category, or province.
6. When `OUTPUT.outcome` is `UPSTREAM_FAILED`, retry once with a fresh Residential ES session before escalating.

### Use through the API

```bash
curl "https://api.apify.com/v2/acts/khadinakbar~milanuncios-scraper/runs?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "keyword": "iphone",
    "category": "telefonos-moviles",
    "province": "madrid",
    "maxItems": 5,
    "proxyConfiguration": {
      "useApifyProxy": true,
      "apifyProxyGroups": ["RESIDENTIAL"],
      "apifyProxyCountry": "ES"
    }
  }'
```

When the Actor completes, read dataset items from the default dataset and the `OUTPUT` record from the default key-value store.

### Use with AI agents through Apify MCP

> Scrape Milanuncios Spain listings for "iphone" in category telefonos-moviles, province madrid. Return up to 10 rows with title, EUR price, city, and URL. Keep Residential ES. Stay on public listing fields only.

Point the MCP client at <https://mcp.apify.com>?tools=khadinakbar/milanuncios-scraper, call `khadinakbar/milanuncios-scraper`, then read the dataset and `OUTPUT.outcome`.

### Builder's note

I built this Actor on Milanuncios embedded `__INITIAL_PROPS__` after live probes showed datacenter exits return Akamai 403 while Residential ES returns 200 with search `adListPagination.adList.ads` and detail `props.ad`. In my testing, CSS-only scrapes were brittle across category templates, so the Actor reads the JSON blob first and finishes `UPSTREAM_FAILED` when every route is unavailable rather than inventing listings. My goal is truthful billing: `fetchDetails` respects `maxItems` via a pending-detail queue so enrich runs stay inside the billed row cap.

### Connect the workflow

When the next market is Spanish property portals after Milanuncios discovery, continue with [Idealista Scraper](https://apify.com/khadinakbar/idealista-scraper).

For Fotocasa property research after Milanuncios discovery, feed titles into [Fotocasa All-in-One Scraper](https://apify.com/khadinakbar/fotocasa-all-in-one-scraper).

### FAQ and responsible use

**Is this affiliated with Milanuncios?** Milanuncios is a trademark of its owner. This independent Actor is not affiliated with, associated with, or endorsed by Milanuncios or Adevinta.

**Is scraping allowed?** You are responsible for complying with Milanuncios Terms of Service, robots rules, and applicable laws (including GDPR when processing seller personal data). Use only for legitimate purposes on publicly available data.

**Why was my run empty?** A valid keyword with zero public matches finishes `SUCCEEDED` / `VALID_EMPTY`. Missing input finishes `INVALID_INPUT`. When every search route is unavailable the outcome is `UPSTREAM_FAILED`.

**Does it return emails or phones?** No. Public listing fields and contact *flags* only — it does not unlock private seller phone numbers.

Open the Actor **Issues** tab for bugs. This Actor is independent research tooling, not legal, investment, or sourcing advice.

# Changelog

This Actor's version history is a separate document: https://apify.com/khadinakbar/milanuncios-scraper/changelog.md

# Actor input Schema

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

Milanuncios search or ad URLs to scrape. Search examples: https://www.milanuncios.com/telefonos-moviles/iphone.htm or https://www.milanuncios.com/anuncios-en-madrid/. Detail URLs ending in -{id}.htm return one enriched row. Non-milanuncios.com hosts are rejected. When set, startUrls take priority over keyword/category/province builders.

## `keyword` (type: `string`):

Keyword used when startUrls is empty. Slugified into the Milanuncios path (e.g. iphone → /anuncios/iphone.htm or /{category}/iphone.htm). Combine with category and province for narrower searches.

## `category` (type: `string`):

Optional Milanuncios category path segment (e.g. telefonos-moviles, coches-de-segunda-mano, inmobiliaria). Must match a known slug — typos fail the run instead of searching all Spain.

## `province` (type: `string`):

Optional Spanish province slug as used in Milanuncios URLs (madrid, barcelona, vizcaya, las\_palmas, ciudad\_real). Narrows results via path segments like /anuncios-en-madrid/.

## `minPrice` (type: `integer`):

Minimum price filter mapped to Milanuncios desde query parameter. Leave empty for no floor.

## `maxPrice` (type: `integer`):

Maximum price filter mapped to Milanuncios hasta query parameter. Leave empty for no ceiling.

## `sellerType` (type: `string`):

Filter private (particular) vs professional sellers via Milanuncios vendedor=part|prof. Choose any to skip the filter.

## `adType` (type: `string`):

Offer to sell vs wanted/demand ads (demanda=n|s). Defaults to any.

## `onlyShippable` (type: `boolean`):

When true, sets isShippable=true on the Milanuncios search request.

## `sort` (type: `string`):

Milanuncios sort: relevance (default), date (newest), cheapest, or expensive.

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

Hard cap on dataset rows charged this run. Prefill 5 for a cheap quality check. Hard maximum 2000.

## `maxPages` (type: `integer`):

Maximum Milanuncios result pages to crawl per search URL (~41 ads/page). Stops earlier when maxItems is reached.

## `fetchDetails` (type: `boolean`):

Visit each ad page for seller profile, structured attributes, favorites/shares, and contact flags. Adds one residential request per listing and charges listing-enriched ($0.008) on top of listing-scraped.

## `stripSellerPII` (type: `boolean`):

For GDPR-sensitive pipelines: redact private seller name/id and coarsen coordinates/zip on detail-enriched rows. Professional shop identity is kept. Applies when fetchDetails is on or detail URLs are used.

## `requireKeywordInTitle` (type: `boolean`):

Client-side filter: keep only ads whose title or description contains every word from keyword. Useful because Milanuncios search can be fuzzy.

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

Milanuncios blocks datacenter and non-Spain IPs (Akamai). Default is Apify RESIDENTIAL with country ES. Do not switch to datacenter unless you accept UPSTREAM\_FAILED outcomes.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.milanuncios.com/telefonos-moviles/iphone.htm"
    }
  ],
  "keyword": "iphone",
  "sellerType": "any",
  "adType": "any",
  "onlyShippable": false,
  "sort": "relevance",
  "maxItems": 5,
  "maxPages": 1,
  "fetchDetails": false,
  "stripSellerPII": false,
  "requireKeywordInTitle": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "ES"
  }
}
```

# Actor output Schema

## `listings` (type: `string`):

No description

## `OUTPUT` (type: `string`):

No description

## `RUN_SUMMARY` (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 = {
    "startUrls": [
        {
            "url": "https://www.milanuncios.com/telefonos-moviles/iphone.htm"
        }
    ],
    "keyword": "iphone",
    "maxItems": 5,
    "maxPages": 1,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "ES"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("khadinakbar/milanuncios-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 = {
    "startUrls": [{ "url": "https://www.milanuncios.com/telefonos-moviles/iphone.htm" }],
    "keyword": "iphone",
    "maxItems": 5,
    "maxPages": 1,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "ES",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("khadinakbar/milanuncios-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 '{
  "startUrls": [
    {
      "url": "https://www.milanuncios.com/telefonos-moviles/iphone.htm"
    }
  ],
  "keyword": "iphone",
  "maxItems": 5,
  "maxPages": 1,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "ES"
  }
}' |
apify call khadinakbar/milanuncios-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,khadinakbar/milanuncios-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/qyoHUYazgrcPK6qNi/builds/NBusoJglcmr9P77Cd/openapi.json
