Facebook Marketplace Scraper — Search Results Export
Pricing
Pay per event
Facebook Marketplace Scraper — Search Results Export
Export public Facebook Marketplace search results — title, price, seller, city/state, and delivery type — for one or more search queries and a buy-location. No login, no browser automation. Deduplicated listings; pay only for results that land.
Pricing
Pay per event
Rating
0.0
(0)
Developer
DevilScrapes
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
10 hours ago
Last modified
Categories
Share
Give this Actor a search term (bike, sofa, whatever your buyers are hunting for) and a buy-location, and it walks Marketplace's own search results the same way a logged-out browser would — talking to the internal GraphQL endpoint the search page itself loads, handling the persisted-query id Meta rotates on every deploy, and handing you back structured JSON instead of a page you'd have to scroll and screenshot yourself. Pricing is per listing landed — a narrow query at a quiet location still costs you only the flat start fee.
🎯 What this scrapes
Public Facebook Marketplace search results — the same listings you'd see running that search logged out. For each queries entry we:
- Search Marketplace from your chosen
latitude/longitude/radius_km. - Walk the search results page by page, up to
max_pages_per_query. - Emit one row per listing: title, price, seller, location, and delivery options.
Listing detail pages, seller profiles, messaging, and category browsing without a search term are out of scope — see Limitations below.
🔥 Features
- 🛡️ We rotate browser fingerprints via
curl-cffiimpersonation so Facebook's servers see a real browser's TLS handshake, not a bare Python client. - 🔁 We retry with exponential backoff on
408/429/503and network errors — up to 5 attempts per request, honouringRetry-Afterwhen Facebook sends it. - 🔧 We self-heal the persisted-query id. Meta rotates the internal
doc_idthis endpoint depends on with every deploy; when a search comes back with a hard error, we re-derive the current id straight from the search page's own JS bundles, cache it, and retry — no manual patch, no waiting on us to ship a fix. - 🧱 We isolate failures per query. A search term that fails doesn't take down the rest of your batch — it's logged and reported on its own, and the run keeps going.
- 🧊 We keep the dataset clean and deduplicated — Pydantic-validated rows, ISO-8601 timestamps, and the same listing never billed twice even if it turns up under two different search terms in one run.
- 💰 You pay only for results that land. A quiet search that genuinely finds nothing costs only the small
actor-startwarm-up fee — never billed per listing for listings that weren't there.
🧭 Use cases
- Resale/arbitrage sourcing — track new listings for a product category across one or more buy-locations to spot underpriced inventory fast.
- Local market research — see what's actually selling (and for how much) in a category near a given city, without manually paging through Marketplace.
- Price monitoring — watch a search term's price spread over time to inform your own listing price.
- Lead generation for local services — find people selling items that signal a need (moving boxes, old furniture, used equipment) in your service area.
⚙️ How to use it
- Click Try for free (or run via API/CLI) and open the input form.
- Enter one or more
queries— the search terms you'd type into Marketplace yourself. - Set
latitude/longitude/radius_kmto the buy-location you want results centered on. Defaults to New York City, 500 km radius. - Set
max_pages_per_queryif you want fewer or more results than the default3pages per search. - Leave
proxy_configurationon its default (Residential, US). Facebook serves a login wall to datacenter IPs, so this one is required rather than optional — we measured it. - Run it. Each query is processed in turn; results land in the dataset as they're scraped, so you can start exporting before the run finishes.
📥 Input
| Field | Type | Default | Notes |
|---|---|---|---|
queries | array of string | — | Required. One or more Marketplace search terms. |
latitude | number | 40.7128 (NYC) | Buy-location latitude, -90..90. |
longitude | number | -74.0060 (NYC) | Buy-location longitude, -180..180. |
radius_km | integer | 500 | Search radius around the buy-location, 1..500. |
max_pages_per_query | integer | 3 | Stop paginating a query's results after this many pages, 1..50. |
proxy_configuration | object | Residential, US | Apify Proxy configuration. Measured in the cloud: the plain runner IP and the shared datacenter pool both get Facebook's login wall; Residential/US returns real listings. |
{"queries": ["bike", "sofa"],"latitude": 40.7128,"longitude": -74.0060,"radius_km": 500,"max_pages_per_query": 2,"proxy_configuration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "US" }}
📤 Output
| Field | Type | Notes |
|---|---|---|
query | string | Echo of the input search query this listing was found under. |
listing_id | string | Facebook Marketplace listing id. |
title | string | Listing title. |
listing_url | string | Constructed link to the listing on Marketplace. |
price_formatted / price_amount | string | null / number | null | Human-readable and numeric listing price. |
strikethrough_price_formatted | string | null | Original (pre-discount) price, when the seller shows one. |
creation_time | string (ISO-8601) | Listing creation time. |
city / state | string | null | Reverse-geocoded listing location. |
seller_name / seller_id | string | null | Seller info — sometimes null; Marketplace occasionally omits the seller block entirely. |
delivery_types | array of string | e.g. ["IN_PERSON", "PUBLIC_MEETUP"]. |
{"query": "bike","listing_id": "1378988563664222","title": "Lightweight & Comfortable City Bike - $160","listing_url": "https://www.facebook.com/marketplace/item/1378988563664222/","price_formatted": "PLN160","price_amount": 160.0,"strikethrough_price_formatted": "PLN190","creation_time": "2026-08-19T00:00:00+00:00","city": "Palo Alto","state": "CA","seller_name": null,"seller_id": null,"delivery_types": ["IN_PERSON", "PUBLIC_MEETUP"]}
💰 Pricing
Pay-per-event. No subscription, no seat fee — you're billed for a run's warm-up and for every listing that actually lands in your dataset.
| Event | Price | Trigger |
|---|---|---|
actor-start | $0.20 | Once per run, the first time any query's search results are successfully read (whether that yields listings or a genuine zero-result search). |
listing-scraped | $0.005 | Once per unique listing written to the dataset. |
At the default settings that works out to $5.20 per 1,000 listings ($0.20 start + 1,000 × $0.005). A search that genuinely finds nothing still costs the $0.20 start fee and nothing more — you're never billed per listing for listings that weren't there. The same listing turning up under two different search terms in one run is billed once, not twice.
🚧 Limitations
- Public Marketplace search results only. No login, no cookies beyond the anonymous bootstrap request, and no account of ours is used to reach anything.
- No listing detail pages. You get the primary thumbnail's data path, not the full photo gallery, seller profile, or full description text.
- No messaging or making offers. Purely a read-only export of what's already publicly visible.
- No category browsing without a search term, and no saved searches. Every run needs at least one
queriesentry. - No historical/date-range seeking. The Actor walks forward from the current search cursor only; it doesn't jump to an arbitrary past date.
- Result density varies. A narrow query/location combination can legitimately return one listing, or zero — that's a real, successful answer, not a failure, and it's priced accordingly (you only pay for what lands).
- If Facebook's servers can't be reached at all, or the search page comes back as a login wall/challenge, that query is reported as failed rather than as an empty (but successful) result — the two are never conflated.
❓ FAQ
Does this need a Facebook account or access token? No. It reads the same public search results anyone can see logged out.
Can it scrape listing detail pages, seller profiles, or messages? No — search results only. See Limitations.
Why did one of my queries come back with zero rows?
Either that search genuinely has no matching listings right now near your buy-location (a real, successful zero), or the search page didn't resolve at all (login wall/challenge) — the run's status message tells you which, per query.
What proxy should I use? Residential, pinned to the US — and that is the default, so you do not need to do anything. We measured it in the cloud: Facebook hands the plain Apify runner IP and the shared datacenter pool a login wall and zero rows, while a US residential exit returns real listings. The country is pinned on purpose, because a geo-random exit would quietly return a different region's Marketplace rather than an error.
Will I get charged twice if the same listing shows up under two search terms? No. Listings are deduplicated across the whole run — a repeat is never pushed to the dataset or billed twice.
🙋 Your feedback
Found a query this doesn't handle right, or want a field we don't capture yet? Message DevilScrapes on Apify or open an issue from the Actor's Store page — we read every one and ship fixes fast.