# Classified Listings (`pintostudio/classified-listings`) Actor

Search and track listings across Facebook Marketplace, OLX, Wallapop, Leboncoin and Custojusto through one unified API — one request shape, five sources, normalized results, plus price alerts that notify you when something new matches.

- **URL**: https://apify.com/pintostudio/classified-listings.md
- **Developed by:** [Pinto Studio](https://apify.com/pintostudio) (community)
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $8.00 / 1,000 results

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

## Classifieds Listings Search Actor

The Classifieds Listings Search Actor is a web scraping tool deployed on the Apify platform that
searches and extracts listing data from **Facebook Marketplace**, **OLX**, **Wallapop**,
**Leboncoin** and **Custojusto** through one unified input, normalizing every result into the same
listing shape regardless of source.

### Overview

None of these five sites publish an official public API, so every integration here is a
reverse-engineered one — real endpoints and page structures discovered and verified live, not
guesses. The Actor supports four operations: searching one or many sources at once, fetching a
single listing's full detail, calling a source's own unique endpoint (category trees, etc.), and
a schedulable "alert check" that reports only genuinely new listings since the last run.

### Features

- **Five sources, one shape**: every listing comes back with the same fields, whichever site it's from
- **Multi-source search**: query several sources in one run, merged and sorted together
- **Price filtering & sorting**: `minPrice`/`maxPrice`, and `relevance`/`price_asc`/`price_desc`/`date`
- **Listing detail**: fetch one listing's full description, images and seller info
- **Per-source extras**: category trees (Leboncoin, Custojusto), a richer raw listing record (OLX),
  a dedicated vehicles search (Wallapop)
- **Price alerts**: a schedulable operation that only reports listings it hasn't seen before,
  with optional webhook delivery when new matches show up
- **Resilient by design**: a source failing never fails the whole run — partial results are normal

### Input Configuration

The `operation` field decides which other fields matter. All operations accept the proxy fields
at the bottom (strongly recommended — see [Rate Limits and Best Practices](#rate-limits-and-best-practices)).

#### `operation: "search"` (default)

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `sources` | array | all 5 | Which sites to search: `facebook`, `olx`, `wallapop`, `leboncoin`, `custojusto` |
| `query` | string | — | Search keywords |
| `category` | string | — | Source-specific category id/slug |
| `location` | string | — | Source-specific location (see [Parameter Options](#parameter-options)) |
| `minPrice` / `maxPrice` | integer | — | Price range, in the source's own currency |
| `sortBy` | string | `relevance` | `relevance` | `price_asc` | `price_desc` | `date` |
| `page` | integer | `1` | Page number (has no effect on Facebook) |
| `limit` | integer | `30` | Max listings per source, 1-100 |
| `cursor` | string | — | Wallapop-only opaque pagination token (see below) |
| `compareBySource` | boolean | `false` | Include min/max/avg price stats grouped by source |

#### `operation: "listingDetail"`

| Parameter | Type | Description |
|-----------|------|-------------|
| `source` | string | One of the 5 source ids |
| `listingId` | string | For Wallapop/Facebook: the plain id. For OLX/Custojusto/Leboncoin: the listing's full `url` from a search result instead — these sites need the slug, not just the numeric id |

#### `operation: "sourceExtra"`

| Parameter | Type | Description |
|-----------|------|-------------|
| `source` | string | One of the 5 source ids |
| `extraName` | string | `categories` (Leboncoin, Custojusto) | `rawState` (OLX) | `cars` (Wallapop) |
| `extraParams` | object | Parameters for that extra, e.g. `{"id": "<olx-listing-url>"}` |

#### `operation: "alertCheck"`

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `sources`, `query`, `category`, `location`, `minPrice`, `maxPrice`, `sortBy` | — | — | Same as `search` |
| `webhookUrl` | string | — | If new matches are found, they're `POST`ed here as `{ query, sources, matches }` |
| `alertStateKey` | string | `default` | Key this saved search's "seen listing ids" are kept under — use a different key per saved search |

#### Proxy configuration (all operations)

| Parameter | Description |
|-----------|-------------|
| `baseProxyUrl` | A generic HTTP(S) proxy URL, e.g. `http://user:pass@host:port` |
| `rotatingResidentialProxyUrl` | A residential proxy that rotates IP per connection |
| `spainResidentialProxyUrl` | A residential proxy pinned to Spain (Wallapop) |
| `portugalResidentialProxyUrl` | A residential proxy pinned to Portugal (OLX, Custojusto) |
| `impitProxyMethodUrl` | A service that fetches a URL with a real browser TLS fingerprint — needed for reliable Leboncoin/Wallapop access |
| `wallapopAppVersion`, `wallapopDeviceId`, `wallapopDeviceOs` | Advanced overrides for Wallapop's required headers |
| `facebookLocationId` | Advanced override for the default Facebook location scope |
| `requestBudgetMs` | Advanced override for the per-request time budget |

### Parameter Options

#### Sources (`sources` / `source`)

- `facebook` — Facebook Marketplace (international)
- `olx` — OLX (Portugal)
- `wallapop` — Wallapop (Spain)
- `leboncoin` — Leboncoin (France)
- `custojusto` — Custojusto (Portugal)

#### Location (`location`)

Format is source-specific:

- OLX / Custojusto: a city name
- Leboncoin: a zip code or city
- Wallapop / Facebook: `"lat,lon"` (e.g. `"40.4168,-3.7038"`); Facebook also accepts a raw
  Facebook-internal location id

#### Sort order (`sortBy`)

- `relevance` — default
- `price_asc` — lowest price first
- `price_desc` — highest price first
- `date` — newest first

### Usage Examples

#### Basic search across all sources

```json
{
  "query": "iphone 13"
}
```

#### Search two sources with price filtering

```json
{
  "operation": "search",
  "sources": ["olx", "custojusto"],
  "query": "iphone 13",
  "minPrice": 100,
  "maxPrice": 400,
  "sortBy": "price_asc",
  "limit": 20
}
```

#### Compare prices across sources

```json
{
  "operation": "search",
  "query": "playstation 5",
  "compareBySource": true
}
```

#### Get one listing's full detail

```json
{
  "operation": "listingDetail",
  "source": "wallapop",
  "listingId": "wzvg1qglddzl"
}
```

#### Call a source's unique endpoint

```json
{
  "operation": "sourceExtra",
  "source": "wallapop",
  "extraName": "cars",
  "extraParams": { "brand": "Renault", "minYear": 2018 }
}
```

#### Set up a price alert (run this on a schedule)

```json
{
  "operation": "alertCheck",
  "sources": ["olx", "custojusto"],
  "query": "iphone 13",
  "maxPrice": 400,
  "alertStateKey": "my-iphone-alert",
  "webhookUrl": "https://your-server.example.com/hooks/new-listing"
}
```

### Output Format

#### `search`

```json
{
  "operation": "search",
  "query": { "query": "iphone 13", "minPrice": 100, "maxPrice": 400, "page": 1, "limit": 20, "sortBy": "price_asc" },
  "sources": {
    "olx": { "ok": true, "count": 20, "hasMore": true },
    "custojusto": { "ok": true, "count": 18, "hasMore": true }
  },
  "errors": [],
  "count": 38,
  "listings": [
    {
      "id": "custojusto:45469559",
      "sourceId": "45469559",
      "source": "custojusto",
      "title": "iPhone 14 Midnight 128GB com Caixa e Pin",
      "description": "iPhone 14 Midnight (Meia-noite)...",
      "price": 300,
      "currency": "EUR",
      "url": "https://www.custojusto.pt/...",
      "images": ["https://prod-images.custojusto.pt/..."],
      "location": { "district": "Braga", "county": "Esposende" },
      "postedAt": "2026-10-27T13:05:33Z",
      "category": "Telefones e Acessórios",
      "seller": { "name": "filipa07silva", "isCompany": false },
      "attributes": {}
    }
  ]
}
```

`location`, `category`, `seller` and `attributes` vary in detail by source — every other field is
always present, `null` when the source didn't provide it. A source failing (e.g. a transient
anti-bot block) shows up under `sources.<id>.error`/`errors` rather than failing the whole run —
`200`/success with partial results is the normal, by-design outcome.

#### `listingDetail`

```json
{ "operation": "listingDetail", "source": "wallapop", "listingId": "wzvg1qglddzl", "listing": { "...": "a listing object" } }
```

#### `sourceExtra`

```json
{ "operation": "sourceExtra", "source": "leboncoin", "extraName": "categories", "data": [ "...": "source-specific" ] }
```

#### `alertCheck`

```json
{
  "operation": "alertCheck",
  "query": { "...": "echoed params" },
  "sources": ["olx", "custojusto"],
  "errors": [],
  "newMatchCount": 2,
  "newMatches": [ "...": "listing objects" ]
}
```

### Data Storage

- **Dataset**: the full result of the run (as shown above) is pushed once per run.
- **Key-value store**: the same result is also saved under `OUTPUT` for quick viewing in the
  Apify console. For `alertCheck` specifically, "seen listing ids" are kept in a separate,
  persistent named store (`CL-LISTINGS-ALERTS`) keyed by `alertStateKey`, so a later scheduled run
  only reports what's genuinely new — this is *not* the same as the run's own default key-value
  store, which is fresh every run.

### Error Handling

- A single source failing doesn't fail the run — check `errors`/`sources.<id>.error` in the output.
- A hard input error (unknown source, missing required field for the chosen operation) fails the
  run with a descriptive message, visible in the run log and as the run's status message.
- `listingDetail` and `sourceExtra` return a clear error if the listing/extra doesn't exist, rather
  than an empty success.

### Performance Tips

1. Narrow `sources` to what you need — searching all 5 is slower and more likely to hit a
   transient per-source failure than querying one or two.
2. Use `limit` to cap how much each source returns per run.
3. For Wallapop pagination beyond the first page, use the `cursor` a previous run's output gave you
   (`nextCursor`) instead of incrementing `page` — it's a single request instead of walking forward
   one request per page.
4. For `alertCheck`, give each saved search its own `alertStateKey` so they don't share seen-listing
   state.

### Rate Limits and Best Practices

- **Proxies matter more than for most scrapers here.** Leboncoin and Wallapop sit behind active
  anti-bot protection; without a configured proxy (and for Leboncoin specifically, the
  `impitProxyMethodUrl` TLS-fingerprint fallback), requests to those two sources will frequently
  fail. OLX, Custojusto and Facebook work without any proxy configured, but a proxy still helps
  reliability under sustained use.
- Expect occasional transient `500`/failed-source entries from Leboncoin/Wallapop even with good
  proxies configured — these sources are actively protected and not every request gets through.
- Don't run `alertCheck` more often than every few minutes per saved search — these sites don't
  publish new listings fast enough to need it, and it increases the chance of hitting anti-bot
  protection.
- Facebook Marketplace only ever returns its first page (roughly 24-40 listings) — there is no
  anonymous way to page further, by design of this Actor (deeper pagination there requires a
  logged-in session, which this Actor doesn't use).

### Integration Examples

#### JavaScript/Node.js

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

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });

const input = {
  operation: 'search',
  sources: ['olx', 'custojusto'],
  query: 'iphone 13',
  maxPrice: 400,
};

const run = await client.actor('YOUR_ACTOR_ID').call(input);
const { items } = await client.dataset(run.defaultDatasetId).listItems();

items.forEach((run) => {
  run.listings.forEach((listing) => {
    console.log(`${listing.title} — ${listing.price} ${listing.currency} (${listing.source})`);
  });
});
```

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")

run_input = {
    "operation": "search",
    "sources": ["olx", "custojusto"],
    "query": "iphone 13",
    "maxPrice": 400,
}

run = client.actor("YOUR_ACTOR_ID").call(run_input=run_input)
items = client.dataset(run["defaultDatasetId"]).list_items().items

for item in items:
    for listing in item["listings"]:
        print(f"{listing['title']} — {listing['price']} {listing['currency']} ({listing['source']})")
```

### Support

If you have any questions or encounter any issues, please consult the Apify documentation or
reach out to us through one of the following channels:

- **Telegram**: [@pintoflow](https://t.me/pintoflow)
- **Email**: <pintoflowpt@gmail.com>
- **Apify Platform**: You can also contact us directly through this platform.

***

# Actor input Schema

## `operation` (type: `string`):

What this run should do. "search" runs a keyword search across one or more sources (the main use case). "listingDetail" fetches one listing's full detail. "sourceExtra" calls a source's own unique endpoint (categories, etc). "alertCheck" re-runs a saved search and reports only genuinely new listings since the last run with this same Alert State Key — intended to be triggered on a schedule.

## `sources` (type: `array`):

Which classifieds sites to search. Used by "search" and "alertCheck". Leave empty to search all 5.

## `query` (type: `string`):

Keyword search text. Used by "search" and "alertCheck".

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

Source-specific category id/slug (use the "sourceExtra" operation with extraName="categories" to list them for Leboncoin/Custojusto). Optional.

## `location` (type: `string`):

Source-specific location filter: a city name for OLX/Custojusto, a zip/city for Leboncoin, "lat,lon" for Wallapop/Facebook (e.g. "40.4168,-3.7038"), or a raw Facebook location id for Facebook. Optional.

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

Minimum price filter, in the source's own currency. Optional.

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

Maximum price filter, in the source's own currency. Optional.

## `sortBy` (type: `string`):

Result ordering. Used by "search" and "alertCheck".

## `page` (type: `integer`):

Page number, starting at 1. Has no effect on Facebook (first page only). On Wallapop, prefer "cursor" below once you have one — it's far more efficient than paging forward.

## `limit` (type: `integer`):

Max listings to return per source, 1-100.

## `cursor` (type: `string`):

An opaque "nextCursor" value from a previous Wallapop search result — pass it back here to fetch the next page in one request instead of using "page".

## `compareBySource` (type: `boolean`):

When true, a "search" run also includes min/max/avg price stats grouped by source in the output.

## `source` (type: `string`):

A single source. Used by "listingDetail" and "sourceExtra".

## `listingId` (type: `string`):

Used by "listingDetail". For Wallapop/Facebook, the plain listing id (a search result's "sourceId"). For OLX/Custojusto/Leboncoin, the listing's full "url" field from a search result instead — these sites need the slug, not just the numeric id.

## `extraName` (type: `string`):

Used by "sourceExtra". One of: Leboncoin → "categories"; Custojusto → "categories"; OLX → "rawState"; Wallapop → "cars".

## `extraParams` (type: `object`):

Used by "sourceExtra". Parameters for the chosen extra, e.g. {"id": "<olx-listing-url>"} for OLX's rawState, or {"query": "golf", "brand": "Renault"} for Wallapop's cars.

## `webhookUrl` (type: `string`):

If set and the "alertCheck" operation finds new matches, they're POSTed here as { query, sources, matches }.

## `alertStateKey` (type: `string`):

Key-value store key this run's "seen listing ids" are kept under, so a later run (e.g. a scheduled one) only reports genuinely new matches. Use a distinct key per saved search.

## `baseProxyUrl` (type: `string`):

A generic HTTP(S) proxy URL (e.g. http://user:pass@host:port), tried before falling back to the impit service below. Optional but recommended — most sources need this to avoid being blocked.

## `rotatingResidentialProxyUrl` (type: `string`):

A residential proxy that rotates IP per connection, used by the impit fallback for anti-bot-protected sources (Leboncoin especially). Optional.

## `spainResidentialProxyUrl` (type: `string`):

A residential proxy pinned to Spain, used for Wallapop. Optional — falls back to the rotating/base proxy above if not set.

## `portugalResidentialProxyUrl` (type: `string`):

A residential proxy pinned to Portugal, used for OLX and Custojusto. Optional — falls back to the rotating/base proxy above if not set.

## `impitProxyMethodUrl` (type: `string`):

Base URL of a service that fetches a URL with a real browser TLS fingerprint (impit, Firefox impersonation) and returns { status_code, content, headers } — used as the last-resort fallback when a direct/proxied request is blocked. Required for Leboncoin/Wallapop to reliably work. Optional for OLX/Custojusto/Facebook.

## `wallapopAppVersion` (type: `string`):

Advanced: override the x-appversion header Wallapop requires. Leave blank to use the built-in default.

## `wallapopDeviceId` (type: `string`):

Advanced: override the x-deviceid header Wallapop requires. Leave blank to use the built-in default.

## `wallapopDeviceOs` (type: `string`):

Advanced: override the x-deviceos header Wallapop requires. Leave blank to use the built-in default.

## `facebookLocationId` (type: `string`):

Advanced: override the default Facebook-internal location id used when "location" isn't a raw location id. Leave blank to default to the San Francisco Bay Area.

## `requestBudgetMs` (type: `integer`):

Advanced: how long a single outbound request is allowed to spend across its whole retry ladder. Leave blank to use the built-in default (45000).

## Actor input object example

```json
{
  "operation": "search",
  "sources": [],
  "query": "iphone",
  "sortBy": "relevance",
  "page": 1,
  "limit": 30,
  "compareBySource": false,
  "extraParams": {},
  "alertStateKey": "default"
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `overview` (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("pintostudio/classified-listings").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("pintostudio/classified-listings").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 pintostudio/classified-listings --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,pintostudio/classified-listings"
        }
    }
}
```

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/e5FJhfQeTGrAnyeZq/builds/wuy7O3WJKjefLBMSp/openapi.json
