Facebook Marketplace Scraper with New-Listing Alerts avatar

Facebook Marketplace Scraper with New-Listing Alerts

Pricing

Pay per event

Go to Apify Store
Facebook Marketplace Scraper with New-Listing Alerts

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.

Pricing

Pay per event

Rating

0.0

(0)

Developer

SR

SR

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

Categories

Share

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

FieldTypeDefaultWhat it does
searchKeywordstringiphoneWhat to search for
keywordsarray–Extra terms, crossed with every location
locationstringamsterdamCity slug or numeric city id from the Marketplace URL
locationsarray–Extra cities
countrystringnlISO code of the market, so the feed comes back localized to it
startUrlsarray–Marketplace URLs from your browser. Overrides every other search field
categorystring–Browse a category instead of searching
sortselectbest_matchBest match, newest, price up, price down, distance
minPrice / maxPriceinteger–Price band in the market's own currency
conditionarray–New, like new, good, fair
daysSinceListedselect–1, 7 or 30 days. Facebook offers no other window
availabilityselectin_stockLive listings, or ones already marked sold
deliveryMethodselectallAny, local pickup only, shipping offered
radiusKminteger–Sent, but Facebook ignores it. See above
exactMatchbooleanfalseTreat the keyword as a phrase. A hint, not a guarantee
maxPagesPerUrlinteger5How deep to follow each search
maxListingsinteger100Hard cap on rows written
listingDetailsbooleanfalseOpen each listing's page for description, category path, coordinates
allPhotosbooleanfalseEvery photo instead of the cover image
maxDetailsinteger–Cap on detail fetches, so a wide search is not a surprise bill
onlyNewListingsbooleanfalseMonitoring mode
cacheKeystring–Names the monitor, so two saved searches keep separate memories
cacheStorageNamestringfacebook-marketplace-seenWhere that memory lives
retriesinteger4Re-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. 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 actorapify/facebook-marketplace-scrapercurious_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 fields25322
Follows the search past page oneyesnot statedyes
Monitoring modeyes–yes
Sold listingsyes–yes
Price drops (wasPrice)yes––
Posted time on every rowyes––
Says which filters Facebook ignoresyes––
Monthly usersnew759382

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.