# Wallapop Scraper — Phones & Battery Health (`reventadata/wallapop-scraper`) Actor

Searches Wallapop for used phones and keeps only listings that clear a battery-health floor you set. Reads battery health, screen and battery replacement, price, distance and shipping from each ad, and drops anything advertised as damaged.

- **URL**: https://apify.com/reventadata/wallapop-scraper.md
- **Developed by:** [reventadata](https://apify.com/reventadata) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 listings

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

> **Unofficial.** This Actor is not affiliated with, authorised by, endorsed by, or in any
> way officially connected to Wallapop. "Wallapop" is a trademark of its respective owner
> and is used here only to describe what this tool reads. It reads the same public pages a
> browser does.

Wallapop Phone Finder searches [Wallapop](https://es.wallapop.com), Spain's largest
second-hand marketplace, for **used phones**, and returns only the listings worth opening. It
reads each seller's own Spanish text, pulls out the stated **battery health**, flags replaced
screens and batteries, and throws away cracked screens, dead units, parts sales, phone cases
and repair-shop adverts before anything reaches your dataset.

> **This is a phone tool, not a general Wallapop search.** The filters are second-hand phone
> vocabulary. Searching bicycles or guitars will silently drop good listings unless you turn
> `excludeAccessories` off.

> **Optional: it can also read a second website.** Switch on `benchmarkAgainstShop` and the
> Actor makes one extra request per keyword to [Cash Converters](https://www.cashconverters.es),
> a Spanish second-hand chain, to work out what the same phone costs from a shop. It is **off by
> default**, it only ever adds columns, and it never removes a listing. If that lookup fails,
> your Wallapop results are unaffected and the run says so.

### What does Wallapop Phone Finder do?

A normal Wallapop search for a used phone is mostly noise. Sort by price and the first three
pages are €1 silicone cases. Filter by price and you still get "for parts", "screen cracked",
and shops advertising battery replacements. This Actor removes all of that, and adds the one
field that actually decides a second-hand phone purchase — battery health — which Wallapop
does not expose as a filter at all.

It calls Wallapop's own JSON search endpoint, so results are the same data the website shows,
not a fragile HTML scrape.

### Why use Wallapop Phone Finder?

- **Buy a used phone without reading 200 adverts.** Set a battery floor and a price cap.
- **Track prices** for a model over time by scheduling daily runs.
- **Spot new listings early** — schedule a daily run and sort the dataset by `createdAt`.

### How to use Wallapop Phone Finder

1. Enter one or more **search keywords**, for example `iphone 13 mini`.
2. Set **near latitude/longitude** and a **radius** for the local pass.
3. Set **minimum price** above zero. This matters more than it sounds — see Tips.
4. Set a **maximum price** and **minimum battery health**, for example `85`.
5. Leave proxy on **Apify Proxy**. Optional, but it spreads out bigger sweeps.
6. Click **Start** and read the Matches table.

### Input

| Field | Type | What it does |
|---|---|---|
| `searchQueries` | array | One search per keyword, repeated around every origin. |
| `cities` | array | Origins for pass 2. Search **origins**, not filters — more means wider coverage. |
| `searchNearby` | boolean | Pass 1: search close to home and keep anything inside the radius. |
| `nearLatitude` / `nearLongitude` | number | Centre of the local search. Defaults to Madrid. |
| `radiusKm` | integer | Enforced by Wallapop, and re-checked here to fill in `distanceKm`. |
| `searchAllSpainWithShipping` | boolean | Pass 2: sweep the rest of the country from 12 city origins. |
| `requireShipping` | boolean | Whether pass 2 keeps only sellers who post. On by default. Off shows every distant listing; `sellerAllowsShipping` still tells you which is which. |
| `minPrice` / `maxPrice` | integer | Price band in EUR. |
| `minBattery` | integer | Drops listings below this stated battery health. |
| `allowUnknownBattery` | boolean | Keep listings that never mention battery health. |
| `excludeDamaged` | boolean | Drops cracked screens, dead units, parts sales. |
| `excludeAccessories` | boolean | Drops cases, protectors, spare parts, repair adverts. |
| `conditions` | array | Wallapop's own condition filter, server-side. One extra request per condition. |
| `benchmarkAgainstShop` | boolean | Add a shop reference price from Cash Converters, a chain that grades its own stock and prices identically nationwide. Off by default; one extra request per keyword. |
| `requireKeywordMatch` | boolean | Drops listings whose title lacks a word of your query. Leave on. |
| `maxItems` | integer | Hard cap on rows for the whole run. |
| `orderBy` | string | Sort order. Leave on relevance — `newest` narrows the feed sharply. |
| `proxyConfiguration` | object | Optional. Leave as Apify Proxy. |

```json
{
    "searchQueries": ["iphone 13 mini"],
    "searchNearby": true,
    "nearLatitude": 40.4168,
    "nearLongitude": -3.7038,
    "radiusKm": 100,
    "searchAllSpainWithShipping": true,
    "requireShipping": true,
    "minPrice": 90,
    "maxPrice": 190,
    "minBattery": 85,
    "conditions": ["as_good_as_new"],
    "excludeDamaged": true,
    "proxyConfiguration": { "useApifyProxy": true }
}
```

### Output

```json
{
    "stage": "shipping",
    "batteryHealth": 87,
    "batteryReplaced": false,
    "shopPriceFrom": 283.95,
    "percentOfShopPrice": 60,
    "savingVsShop": 113.95,
    "screenReplaced": false,
    "distanceKm": 308.2,
    "price": 170,
    "currency": "EUR",
    "title": "iPhone 13 mini 128GB negro",
    "description": "- Capacidad batería al 87%\n- Color negro (medianoche)\n…",
    "city": "Coslada",
    "region": "Comunidad de Madrid",
    "postalCode": "28820",
    "reserved": false,
    "sellerAllowsShipping": true,
    "isRefurbished": false,
    "hasWarranty": false,
    "createdAt": 1755600000000,
    "url": "https://es.wallapop.com/item/iphone-13-mini-128gb-negro-1292117598",
    "imageUrl": "https://cdn.wallapop.com/images/…",
    "matchedKeyword": "iphone 13 mini"
}
```

You can download the dataset in various formats such as JSON, HTML, CSV, or Excel.

#### Data fields

| Field | Meaning |
|---|---|
| `stage` | Which pass found it: `nearby` or `shipping`. |
| `batteryHealth` | Percentage parsed from the seller's text. `null` if never stated. |
| `batteryReplaced` | The seller says the battery is non-original or was swapped. Read it next to `batteryHealth`: an aftermarket battery often reports 100% while performing worse than an original at 88%. `false` means not stated, not confirmed original. |
| `screenReplaced` | The seller says the screen is new or was replaced. "Pantalla nueva" is ambiguous between a pristine screen and a swapped one, so this is a prompt to read the advert, not a verdict. |
| `shopPriceFrom` / `shopPriceMedian` / `shopCheapestGrade` / `shopUnitsSeen` | What the same phone costs at Cash Converters right now. A reference, never a filter. |
| `savingVsShop` / `percentOfShopPrice` | The gap to that shop price. A listing far below the trade price is what you are hunting for — and also what a scam looks like. Read it with `batteryReplaced` and `screenReplaced`, not on its own. |
| `distanceKm` | Distance from your near-point, computed here. |
| `price` / `currency` | Asking price. |
| `title` / `description` | The seller's own words, unmodified. |
| `city` / `region` / `postalCode` | Approximate location Wallapop publishes. |
| `reserved` | True if the seller already marked it reserved for someone. |
| `sellerAllowsShipping` | Whether this seller posts. Wallapop's `item_is_shippable` is about the category, not the seller. |
| `isRefurbished` / `hasWarranty` | Wallapop's own flags. |
| `createdAt` | Listing creation timestamp, milliseconds. |
| `url` / `imageUrl` | Direct links. |
| `matchedKeyword` | Which of your keywords found it. |

### How much does it cost to scrape Wallapop?

Pricing is pay-per-event: a charge when the run starts, and a charge per result written to
the dataset. You are billed for what you get, not for time.

For scale, the underlying compute of the default sweep (12 origins, one keyword) measures
around **$0.008**, and a 20-keyword sweep across all 12 cities — 240 requests, five minutes —
measures **$0.021**.

Two things multiply that: each extra keyword, and each entry in `conditions` — Wallapop takes
one condition per request, so selecting three conditions triples the requests. The run log
prints the planned feed count before each pass.

### Tips

- **Always set `minPrice` above zero.** Wallapop's cheapest results are accessories and repair
  services. This single setting removes most of the noise before the filters even run.
- **`cities` are search origins, not location filters.** Results still come from all over
  Spain. One feed is capped at 40 items, so adding origins is how you widen coverage.
- **Use `conditions: ["as_good_as_new"]` when the phone has to look new.** Wallapop applies
  it server-side, so the feed's 40 slots are spent on candidates instead of being filtered
  away afterwards.
- **Keep the query short.** Every word of it must appear in the *title*, so
  `iphone 13 mini 128gb` drops listings that only mention the capacity in the description.
  Search `iphone 13 mini` and let the other filters do the narrowing.
- **Turn on `allowUnknownBattery` if you get too few results.** Most sellers never state
  battery health, and those listings are dropped by default when a floor is set.
- **Schedule it daily** and sort the dataset by `createdAt` to see only what is new.

### FAQ and limitations

**Why does it drop listings Wallapop returned?** Wallapop's keyword search is fuzzy — a
search for `iphone 13 mini` also returns iPhone 8s, iPhone 11s and multi-model shop adverts.
`requireKeywordMatch` keeps only titles containing every word of your query.

**Is the battery health reliable?** No — it is whatever the seller typed. Treat it as a
shortlist, not a fact. Always ask for a photo of Settings → Battery → Battery Health before
paying.

**Is a proxy required?** No. Wallapop's endpoint rejects any request missing the
`X-DeviceOS` header, which this Actor always sends; the IP itself is not the problem.
A proxy is still useful for spreading out a large sweep.

**Does it paginate?** No. The endpoint caps a feed at 40 items and rejects an offset
parameter. Coverage comes from sweeping multiple origins and keywords instead.

**Legality.** This Actor reads only public listing data that Wallapop serves to any visitor.
It stores no personal data beyond the seller-supplied text shown on a public advert. You are
responsible for using the output in line with Wallapop's Terms of Service and applicable law.

# Actor input Schema

## `searchQueries` (type: `array`):

One search per phone model. Every keyword is repeated around every origin below.

## `cities` (type: `array`):

Origins for the country-wide shipping pass. Wallapop searches around a coordinate, so these widen coverage rather than restrict it. Leave empty to sweep all of them.

## `searchNearby` (type: `boolean`):

Look close to home first and keep anything inside the radius, whether or not the seller posts.

## `nearLatitude` (type: `number`):

Centre of the local search. Defaults to Madrid (40.4168).

## `nearLongitude` (type: `number`):

Defaults to Madrid (-3.7038).

## `radiusKm` (type: `integer`):

Enforced by Wallapop via its own distance\_in\_km parameter, and re-checked here from each listing's coordinates to fill in the distance field.

## `searchAllSpainWithShipping` (type: `boolean`):

Sweep the whole country from 12 city origins, not just the radius around your near-point. Whether distant sellers must post is a separate switch below.

## `requireShipping` (type: `boolean`):

On by default: a listing 700 km away that the seller will not post is not buyable. Turn it off to see every distant listing anyway - useful for price research, or if you are willing to travel or arrange your own courier. Either way the sellerAllowsShipping field tells you which is which.

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

Keep this above zero. Wallapop's cheapest results are phone cases and repair-service adverts, not devices.

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

Optional. Leave empty for no ceiling. Applied by Wallapop itself, so setting it also narrows what the feed returns.

## `minBattery` (type: `integer`):

Read from the seller's own text. Set 0 to disable. Listings that never mention battery health are dropped unless 'Keep listings with no stated battery' is on.

## `allowUnknownBattery` (type: `boolean`):

Most sellers never state battery health. Turn this on to see them anyway and ask yourself.

## `excludeDamaged` (type: `boolean`):

Drops anything describing a cracked screen, a dead unit, or a sale for parts. "Sin grietas" and other denials are not treated as damage.

## `excludeAccessories` (type: `boolean`):

Drops cases, screen protectors, spare parts and repair-shop adverts by title. A phone listed "+ funda" is kept - the case is included, not the product. These are Spanish phone-market phrases; turn this off if you search anything that is not a phone.

## `requireKeywordMatch` (type: `boolean`):

Wallapop's keyword search is fuzzy - searching 'iphone 13 mini' also returns iPhone 8s and multi-model shop adverts. This drops any listing whose title does not contain every word of your query. Turn it off only for broad exploration.

## `benchmarkAgainstShop` (type: `boolean`):

Looks up what the same phone costs at Cash Converters, a chain that grades its own stock and prices identically nationwide. Adds a reference price and the gap to it on every row. One extra request per keyword, and it never removes a listing - a price far below the trade price is what you are hunting for, and also what a scam looks like.

## `conditions` (type: `array`):

Wallapop's own condition filter, applied server-side. Leave empty for any. It accepts one value per request, so each condition selected costs one extra request per origin. 'As good as new' is the tightest useful setting.

## `orderBy` (type: `string`):

Leave on relevance. 'newest' quietly restricts the feed to a recent window - the same query returns 7 items sorted newest against 40 by relevance - so use it only for a scheduled run watching for new adverts.

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

Hard cap on rows pushed for the whole run.

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

Optional. Wallapop lets an unproxied caller through as long as the request carries the X-DeviceOS header, which this Actor always sends. Rotating the address still spreads out a large sweep.

## Actor input object example

```json
{
  "searchQueries": [
    "iphone"
  ],
  "cities": [
    "madrid",
    "barcelona",
    "valencia",
    "sevilla",
    "zaragoza",
    "malaga",
    "bilbao",
    "murcia",
    "palma",
    "alicante",
    "las-palmas",
    "vigo"
  ],
  "searchNearby": true,
  "nearLatitude": 40.4168,
  "nearLongitude": -3.7038,
  "radiusKm": 100,
  "searchAllSpainWithShipping": true,
  "requireShipping": true,
  "minPrice": 1,
  "minBattery": 0,
  "allowUnknownBattery": false,
  "excludeDamaged": true,
  "excludeAccessories": true,
  "requireKeywordMatch": true,
  "benchmarkAgainstShop": false,
  "conditions": [],
  "orderBy": "most_relevance",
  "maxItems": 500,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `matches` (type: `string`):

Surviving listings: price, stated battery health, distance from the search point, whether the seller posts, and a link to the advert.

# 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 = {
    "searchQueries": [
        "iphone"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("reventadata/wallapop-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 = { "searchQueries": ["iphone"] }

# Run the Actor and wait for it to finish
run = client.actor("reventadata/wallapop-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 '{
  "searchQueries": [
    "iphone"
  ]
}' |
apify call reventadata/wallapop-scraper --silent --output-dataset

```

## MCP server setup

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