Facebook Marketplace Scraper with New-Listing Alerts
Pricing
Pay per event
Facebook Marketplace Scraper with New-Listing Alerts
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.
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.
priceis725.0,priceTextis€725. Zero is a real value: sellers post at zero, and those rows stay in as0.0rather than being silently dropped into your average as a miss - Price drops.
wasPricecarries 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
{"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.
maxPriceat 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
deliveryTypescontaining 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.