# Facebook Marketplace Scraper with New-Listing Alerts (`s-r/facebook-marketplace-scraper`) Actor

Search Facebook Marketplace by keyword, city, price band, condition and age, and get listings back as rows with price, town, coordinates, photos and the seller's description. Monitoring mode returns only what is new since the last run.

- **URL**: https://apify.com/s-r/facebook-marketplace-scraper.md
- **Developed by:** [SR](https://apify.com/s-r) (community)
- **Categories:** Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Facebook Marketplace Scraper with New-Listing Alerts

Search Facebook Marketplace by keyword, city, price band, condition and age, and get listings back as rows: asking price as a number, the seller's town and coordinates, when it was posted, whether it is sold, every photo and the seller's own description.

Turn on monitoring mode and it becomes an alert feed rather than a scraper. It remembers which listings it has already handed you, so every scheduled run returns only what has appeared since the last one.

No login, no cookies, no account of your own at risk.

### What you get

- **Deep results, not one page.** The search page itself carries 24 listings. This Actor follows Facebook's own scroll from there, and you choose how far. One Amsterdam phone search walked 26 pages to **573 distinct listings** and Facebook was still offering more
- **Price as a number and as printed.** `price` is `725.0`, `priceText` is `€725`. Zero is a real value: sellers post at zero, and those rows stay in as `0.0` rather than being silently dropped into your average as a miss
- **Price drops.** `wasPrice` carries the struck-through original when a seller has lowered the price
- **The seller's town on every row**, plus region code and approximate coordinates on detailed rows. A search from one city routinely reaches 50-plus towns, so location decides whether a listing is collectable at all
- **Posted time**, as an epoch and an ISO timestamp, which separates this morning's listing from one that has sat five months
- **Sold, pending and live flags**, so a price history can tell an asking price from a price somebody paid
- **Description, full category chain and every photo**, when listing details are on
- **A summary that says what happened**: searches planned and fetched, pages walked, whether Facebook was still offering more, and which market it was read from

### The limit worth stating up front

**Nobody can promise every listing in a city, and this page will not.**

Facebook publishes no result count and keeps saying there is another page for as long as you keep asking. Depth is a budget decision: each page is one request and brings back about 24 more listings.

- a page budget of 5 gives roughly 120 listings per search
- a budget of 20 gives roughly 480
- there is no setting that means "everything", because the site exposes no "everything"

The honest way to cover a market is to slice it: several cities, a few keywords, price bands. Every search is walked independently and rows are deduplicated by listing id across all of them.

**And one filter does not work.** `radiusKm` is sent because you asked for it, and Facebook ignores it. A 2 km ring and a 250 km ring around the same point both returned towns 100 km out, because the query the site actually runs carries its own fixed radius. Price bounds, sort order, condition and the listed-within window all demonstrably do apply, and the test suite pins each one to a measurement. If distance matters, filter on `city` in the output.

### Deep pagination, and what keeps it working

Listings past the first 24 come from Facebook's own infinite scroll, which uses
a persisted GraphQL query. That query has an id, and the id changes whenever
Facebook ships a new front-end bundle.

The Actor pins the current id and uses it until a call comes back without a
feed. Only then does it read the page's own bundles to find the id in force,
and retry once. A normal run pays nothing for this. The run that happens to
meet a rotation pays the lookup and keeps working, and the run summary reports
`docIdRediscovered` so the change is visible rather than silent.

If the lookup also fails, the run returns the 24 listings the search page
carried and reports `paginationAvailable: false`. That is a smaller answer,
never a wrong one.

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `searchKeyword` | string | `iphone` | What to search for |
| `keywords` | array | – | Extra terms, crossed with every location |
| `location` | string | `amsterdam` | City slug or numeric city id from the Marketplace URL |
| `locations` | array | – | Extra cities |
| `country` | string | `nl` | ISO code of the market, so the feed comes back localized to it |
| `startUrls` | array | – | Marketplace URLs from your browser. Overrides every other search field |
| `category` | string | – | Browse a category instead of searching |
| `sort` | select | `best_match` | Best match, newest, price up, price down, distance |
| `minPrice` / `maxPrice` | integer | – | Price band in the market's own currency |
| `condition` | array | – | New, like new, good, fair |
| `daysSinceListed` | select | – | 1, 7 or 30 days. Facebook offers no other window |
| `availability` | select | `in_stock` | Live listings, or ones already marked sold |
| `deliveryMethod` | select | `all` | Any, local pickup only, shipping offered |
| `radiusKm` | integer | – | Sent, but Facebook ignores it. See above |
| `exactMatch` | boolean | `false` | Treat the keyword as a phrase. A hint, not a guarantee |
| `maxPagesPerUrl` | integer | `5` | How deep to follow each search |
| `maxListings` | integer | `100` | Hard cap on rows written |
| `listingDetails` | boolean | `false` | Open each listing's page for description, category path, coordinates |
| `allPhotos` | boolean | `false` | Every photo instead of the cover image |
| `maxDetails` | integer | – | Cap on detail fetches, so a wide search is not a surprise bill |
| `onlyNewListings` | boolean | `false` | Monitoring mode |
| `cacheKey` | string | – | Names the monitor, so two saved searches keep separate memories |
| `cacheStorageName` | string | `facebook-marketplace-seen` | Where that memory lives |
| `retries` | integer | `4` | Re-requests for a page served without listings on it |

### Output

```json
{
  "id": "933737125922655",
  "url": "https://www.facebook.com/marketplace/item/933737125922655/",
  "title": "iPhone 16 Pro Max 512 gb",
  "price": 350.0,
  "priceText": "€350",
  "currency": "EUR",
  "wasPrice": null,
  "city": "Utrecht",
  "locationText": "Utrecht, UT",
  "latitude": 52.0907,
  "longitude": 5.1214,
  "isSold": false,
  "listedAt": "2026-09-15T08:45:32Z",
  "categoryPath": ["Electronics", "Cell Phone Accessories", "Cell Phone Cases"],
  "deliveryTypes": ["IN_PERSON", "SHIPPING_OFFSITE"],
  "description": "Zo goed als nieuw, met doos.",
  "photos": ["https://scontent.xx.fbcdn.net/..."],
  "searchQuery": "iphone",
  "detailsFetched": true
}
```

Photo URLs carry an expiring signature. Download them during the run rather than storing the URL for next week.

### Monitoring mode, the reason to schedule this

Set `onlyNewListings`, give the monitor a name with `cacheKey`, and schedule it. The first run returns everything and remembers the ids. Every run after that returns only listings it has not handed you before.

- **Deal alerts.** `maxPrice` at what you would actually pay, sorted newest, every fifteen minutes. The dataset is your shortlist and only ever holds things you have not seen
- **Supply tracking.** Watch a category in three cities and count arrivals per day. A rising arrival rate means supply is building, which is the leading indicator that prices are about to soften
- **Resale sourcing.** New listings under your buy price, filtered to `deliveryTypes` containing shipping, is a feed you can act on without opening the site

The memory is a set of listing ids in a named storage that survives between runs. The summary reports which storage was used: `named` means the monitor is working, `run-default` means it fell back to the run's own storage and will not remember this run next time. Check that field after the first scheduled run.

Two things so results never surprise you. A seller who edits a price is **not** re-emitted, because the id has not changed, so take a full non-monitoring run periodically if price edits matter. And a monitor keeps 50,000 ids, oldest dropped first, so a very long monitor on a busy category will eventually re-emit something ancient once.

### Use cases

**What a used iPhone is actually worth here.** Page budget of 15, take the median of `price`, and you have the asking market rather than a guide price. `listedAt` lets you weight the last week higher, which matters where prices move monthly.

**Regional arbitrage.** One search returns 50-plus towns. Group by `city`, compare medians, and the spread is the margin second-hand traders live on. It is invisible from the site itself, which shows one ranked feed and no aggregate.

**Sold-listing price history.** Set `availability` to sold and you get what left the market rather than what is sitting on it.

### How it compares

| | this actor | `apify/facebook-marketplace-scraper` | `curious_coder/facebook-marketplace` |
|---|---|---|---|
| Per 1,000 listings | **$0.40** | $6.20 | $0.50 |
| Per 1,000 listing details | **$0.80** | $6.20 | $1.00 |
| Actor-start fee | **$0.00005** | – | $0.00005 |
| Input fields | **25** | 3 | 22 |
| Follows the search past page one | **yes** | not stated | yes |
| Monitoring mode | **yes** | – | yes |
| Sold listings | **yes** | – | yes |
| Price drops (`wasPrice`) | **yes** | – | – |
| Posted time on every row | **yes** | – | – |
| Says which filters Facebook ignores | **yes** | – | – |
| Monthly users | new | 759 | 382 |

Honest about the other side: they have 759 monthly users and this Actor has none, which is the only column favouring them and it is a real signal. What it does not explain is the price. Fifteen times our per-listing rate, three input fields against twenty-five, and that same rate charged again for the detail fetch. On 500 listings with details, $6.20 against $0.60.

### Cost and reliability

A run costs the start fee plus one event per row written, plus one more per row whose listing page was opened. A monitoring run that finds nothing bills the start fee alone, which is what makes a fifteen-minute schedule affordable. On a wide sweep set `maxDetails` below `maxListings`: details are the expensive half, and for price monitoring the search row already carries price, town, posted time and the sold flag.

A signed-out visitor is sometimes handed a page with no listings instead of the feed. Over twelve identical requests inside one minute it happened twice. That page is an HTTP 200 of respectable size and it parses perfectly well into "this search has no results", which is exactly the failure that makes a scraper confidently wrong. This Actor refuses to report an unserved page as an empty result. It asks again, up to `retries` times, and only a search that never came back with a feed is a failure, reported with its URL in the `errors` record.

# Actor input Schema

## `searchKeyword` (type: `string`):

What to search Marketplace for, exactly as you would type it into the site's own search box. Leave empty if you are using a category or pasting your own Marketplace URLs instead.

## `keywords` (type: `array`):

Additional search terms. Every keyword is crossed with every location, so three keywords across two cities is six searches. Use this instead of running the Actor six times.

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

The Marketplace city to search, as the slug Facebook puts in the URL (amsterdam, london, nyc) or the numeric city id from that same URL. Marketplace is city-scoped: there is no country-wide or worldwide search.

## `locations` (type: `array`):

Additional city slugs or city ids. Every location is crossed with every keyword, so two cities and three keywords is six searches, each of which is paged independently.

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

ISO country code of the market the cities belong to, lowercase. Marketplace localizes what it serves to where the request comes from, so setting this is what makes an Amsterdam search return the Dutch feed with Dutch towns and euro prices.

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

Paste Marketplace search or category URLs straight from your browser after clicking the filters you want. When this is set every other search field is ignored, because your URL already says what you want better than the builder can.

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

A Marketplace category slug to browse instead of searching, taken from the URL of the category page (propertyrentals, vehicles, apparel). Ignored when Marketplace URLs are supplied.

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

How Facebook should order the results. Newest first is what you want for monitoring; price ascending and descending walk the same market from opposite ends and reach different listings.

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

Lower price bound in the market's own currency. Verified to apply: a search with a 400 floor came back with 400 as its cheapest row.

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

Upper price bound in the market's own currency. Combine with the minimum to slice a busy category into bands.

## `condition` (type: `array`):

Restrict to the condition bands sellers pick when they post. Choosing more than one widens the search rather than narrowing it.

## `daysSinceListed` (type: `integer`):

Only listings posted within this many days. Facebook supports exactly three windows, 1, 7 and 30, and rejects anything else.

## `availability` (type: `string`):

Whether to read the live listings or the ones already marked sold. Sold listings are what you want for a price history; live ones are what you want for what is on the market now.

## `deliveryMethod` (type: `string`):

Restrict to listings the seller will ship, or to ones that are collection only. Shipping is rare in some markets, so this filter can legitimately return nothing.

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

Passed to Facebook as the distance ring around the chosen city. Facebook did not honour it in testing: a 2 km ring and a 250 km ring returned the same towns, some of them 100 km away, because the query Facebook actually runs carries its own fixed radius. It is sent because you asked for it. Filter on the city field in the output if distance really matters.

## `exactMatch` (type: `boolean`):

Ask Facebook to treat the keyword as an exact phrase rather than loosely. It reordered our results without obviously narrowing them, so treat it as a hint rather than a guarantee.

## `listingDetails` (type: `boolean`):

Open each listing's own page to add the seller's description, the full category path, map coordinates and whether shipping is offered. It costs one extra request per listing and is billed separately, so leave it off when the search row is enough.

## `allPhotos` (type: `boolean`):

Collect every photo on the listing instead of just the cover image. Requires listing details to be on, because the extra photos only exist on the listing's own page.

## `maxPagesPerUrl` (type: `integer`):

How deep to follow each search. Page one is the 24 listings the search page itself carries; every page after that adds about 24 more. Twenty-six requests against one Amsterdam search returned 573 distinct listings and Facebook was still offering more, so depth is a budget decision rather than a hard ceiling. Each page is one request, so this is the main thing that decides how long a run takes.

## `maxListings` (type: `integer`):

Hard cap on rows written to the dataset, applied after every search has been paged. Leave room above the page size: one search page carries 24 listings and each extra page adds about 24 more.

## `maxDetails` (type: `integer`):

Cap on how many listings get their own page opened, so a wide search cannot turn into an unexpectedly large bill. Ignored when listing details are off.

## `onlyNewListings` (type: `boolean`):

Return only the listings that have appeared since the last run of this same monitor. Schedule the Actor and every run gives you the new arrivals and nothing else. A run that finds nothing new succeeds with an empty dataset.

## `cacheKey` (type: `string`):

Names the memory that monitoring mode reads and writes, so two different saved searches do not share one list of already-seen listings. Pick any short label and keep using it for that search.

## `cacheStorageName` (type: `string`):

The named key-value store that holds monitoring memory across runs. Leave it alone unless you want several monitors kept in separate stores; the run summary reports which store was actually used.

## `retries` (type: `integer`):

How many times to re-request a page that came back without listings on it. Facebook serves a signed-out visitor an empty shell for roughly one request in six, and that shell is indistinguishable from a search with no results unless you ask again.

## Actor input object example

```json
{
  "searchKeyword": "iphone",
  "keywords": [
    "iphone 15",
    "iphone 14 pro"
  ],
  "location": "amsterdam",
  "locations": [
    "rotterdam",
    "utrecht"
  ],
  "country": "nl",
  "startUrls": [
    "https://www.facebook.com/marketplace/amsterdam/search?query=iphone&minPrice=300"
  ],
  "category": "vehicles",
  "sort": "best_match",
  "minPrice": 300,
  "maxPrice": 800,
  "condition": [
    "used_good"
  ],
  "daysSinceListed": 7,
  "availability": "in_stock",
  "deliveryMethod": "all",
  "radiusKm": 40,
  "exactMatch": false,
  "listingDetails": false,
  "allPhotos": false,
  "maxPagesPerUrl": 5,
  "maxListings": 100,
  "maxDetails": 50,
  "onlyNewListings": false,
  "cacheKey": "iphone-amsterdam",
  "cacheStorageName": "facebook-marketplace-seen",
  "retries": 4
}
```

# Actor output Schema

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

One row per Marketplace listing.

## `summary` (type: `string`):

Search pages planned and fetched, how many came back without a feed, listings returned, whether monitoring mode ran and which store held its memory, and which country the market was read from.

## `errors` (type: `string`):

Per-page and per-listing failures with a code, the URL and a redacted message.

# 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 = {
    "searchKeyword": "iphone",
    "location": "amsterdam",
    "country": "nl",
    "sort": "best_match",
    "availability": "in_stock",
    "deliveryMethod": "all",
    "listingDetails": false,
    "maxPagesPerUrl": 5,
    "maxListings": 100,
    "onlyNewListings": false,
    "retries": 4
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/facebook-marketplace-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 = {
    "searchKeyword": "iphone",
    "location": "amsterdam",
    "country": "nl",
    "sort": "best_match",
    "availability": "in_stock",
    "deliveryMethod": "all",
    "listingDetails": False,
    "maxPagesPerUrl": 5,
    "maxListings": 100,
    "onlyNewListings": False,
    "retries": 4,
}

# Run the Actor and wait for it to finish
run = client.actor("s-r/facebook-marketplace-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 '{
  "searchKeyword": "iphone",
  "location": "amsterdam",
  "country": "nl",
  "sort": "best_match",
  "availability": "in_stock",
  "deliveryMethod": "all",
  "listingDetails": false,
  "maxPagesPerUrl": 5,
  "maxListings": 100,
  "onlyNewListings": false,
  "retries": 4
}' |
apify call s-r/facebook-marketplace-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,s-r/facebook-marketplace-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/mcGBeoRKeXBCDamaj/builds/KM9pWeX6ocjjQNoHD/openapi.json
