Facebook Marketplace Scraper — Listings by City, Price, Date
Pricing
from $1.50 / 1,000 listings
Facebook Marketplace Scraper — Listings by City, Price, Date
Facebook Marketplace scraper by city: name a city and a keyword and get its listings as rows — link, title, price and the crossed-out price, listed date, city and state, photo, category, mileage, delivery, sold/pending/live. Optional details: the description and every photo. No start fee.
Pricing
from $1.50 / 1,000 listings
Rating
0.0
(0)
Developer
Steadyfetch Team
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
21 hours ago
Last modified
Categories
Share
Click Start with nothing set and the run returns the built-in sample rows — a three-listing sample from one US city with listing details off, so you see the exact output shape before you spend anything: nothing is collected from Facebook, and there is no result fee. Then name a city, add a keyword, and get that city's Marketplace listings as rows. One JSON row per listing: the listing ID and its link, the title, the price as a number and as Facebook writes it, any strikethrough price, when it was listed, the city and state, one photo, the category, mileage on a vehicle, the delivery types, and whether it is sold, pending or still live. From $1.50/1,000 listings on the Business plan ($3.00 on the Apify free plan), platform usage included, no start fee.
Using an AI agent? Pin this actor in Apify's MCP server with one link: https://mcp.apify.com?tools=steadyfetch/facebook-marketplace-scraper
- Actor id:
steadyfetch/facebook-marketplace-scraper - Input:
{ "cities": ["Austin, TX"], "query": "bicycle" }— the two fields that make a real run. A city name is resolved for you; a bare"30.27,-97.74"pair skips the resolve. Leavequeryempty for the city's own feed. - Cap the bill: set
maxTotalChargeUsdon the run (a run option, not Actor input), e.g.0.50— the run stops when it reaches it and still writes out everything already in hand. - Your cap is the cap.
maxListings(default 100) is exact and counts across every city in the run, not per city. SendmaxItems,limitormaxResultsinstead — the names other Marketplace scrapers use — and the smallest one present is read asmaxListings, with one uncharged note row saying which name was read.
Unofficial. Not affiliated with, endorsed by, or sponsored by Facebook or Meta Platforms, Inc.
What a row does NOT carry
Read this before the feature list, because it is the part a listing usually leaves for the Issues tab. Marketplace serves a short row in the search feed and a long one on the listing itself, and this actor will not promise a column it cannot fill:
currencyis alwaysnullon alistingrow. The search feed carries the price as a number and as the string Facebook prints it, and it does not carry a currency code. Turn on listing details andcurrencyis a real value on thelistingDetailsrow, because the listing's own answer does carry it.descriptionis alwaysnullon alistingrow, for the same reason, and is the whole point of thelistingDetailsrow.- There is no seller, ever. Not a name, not an ID, not a profile link, not a rating — on either row type. A Marketplace seller is a member of the public, and this actor does not return one. No input asks for one and no column promises one.
- One photo on a
listingrow (photoUrl, the primary one). Every photo the listing has (photoUrls[]) is on thelistingDetailsrow. - No exact street address. A
listingrow carries the city and state; alistingDetailsrow carries the listing's own latitude and longitude exactly as Facebook serves them, which is an approximate pin and not a doorstep.
All of these ship as explicit null or absent-by-design columns rather than as missing keys, so a sheet or a fixed-schema table never breaks and you never have to wonder whether an empty cell was a real empty.
The three things this actor does differently
- You are never charged twice for a listing you already have. Every run remembers the listing IDs it delivered to your account. A later run on the same cities and the same query skips them before anything is reserved or bought — not delivered and then refunded — and the slot goes to the next new listing instead, so you still get the number you asked for. The run summary says how many were skipped. The memory is a key-value store in your own Apify account, so a run started with a scoped API token in restricted-access mode needs key-value store Read, Write and Create permission (or Actor runs set to Full access); without it the run says so on its status line and cannot skip what you already have (Write is what lets the run remember what it delivered; without it every run pays again, and because each search keeps its own memory, Create is needed for every new search rather than only the first). That is what makes a daily watch on the same cities cost only what is new, and it is the reason to schedule this actor rather than re-run it by hand.
- The limit is exact.
maxListingsis a number of delivered rows, not a hint. Ask for 20 and 20 arrive, across however many cities you gave. There is no start fee to pay before the first row and no per-result minimum underneath it, so a small run really is a small bill. - Listing details return the description and every photo.
includeListingDetailsadds onelistingDetailsrow per listing with the seller's full description text, the whole photo set, the listing's own latitude and longitude, its attributes (condition and the rest), the currency, and whether shipping, Buy Now and messaging are offered. It is off by default and charged only on a details row that was actually delivered.
Price
Two charged events, and each one lands only on a row that was actually delivered. A row is charged as either a Listing or a Listing details row, never both.
| Event | Apify free plan | Bronze | Silver | Gold and above |
|---|---|---|---|---|
| Listing — one delivered Marketplace listing | $0.003 | $0.0022 | $0.0018 | $0.0015 |
Listing details — one details row, only with includeListingDetails | $0.012 | $0.008 | $0.006 | $0.005 |
The store header is the cheapest tier of the primary event: from $1.50/1,000 listings. Platform usage is included in both prices — there is no separate compute bill on top. No start fee, no filter fee, and no result fee on a miss. A price band, a radius, a condition or a date window costs nothing to apply, and a run that delivers nothing charges nothing. Set maxTotalChargeUsd on the run and it is a hard ceiling: the run stops cleanly under it, writes out everything already collected, and the last row says what is left.
What is never charged
Every run closes its own books, and each of these ships as a labelled, uncharged row rather than as silence:
city_not_found— the city name you sent could not be placed. The row names the city, and the other spelling we tried for you if there was one, and says the lookup was on us, not on you. Fix the spelling, name a larger city nearby, or send a"lat,lng"pair instead.city_cap—maxUnresolvableCitieswas reached and the run stopped trying to place names. A bound of yours, set to 3 by default, so a list with a typo in it cannot quietly become a list of lookups.no_listings— the search came back with nothing at all for that city.listing_gone— the details call found the listing no longer available. Thelistingrow above it still stands and states its own charge.source_error— the read did not go through. Temporary, says nothing about your city or your query, and a re-run is the fix.limit_reached·budget_reached— your ownmaxListings, and your ownmaxTotalChargeUsd.all_repeat— every listing on that page had already been delivered to your account, so the run stopped paying to look.page_cap— a bound of THIS ACTOR'S own, never a verdict about the city: the feed was still serving and the run had read the 40 pages it buys for a single city. Narrow the query, cut the radius, or split the price band across runs to go deeper.- Listings your own filters dropped, listings past your own limit, and listings your account already had.
- Starting a run. There is no start fee, per city or per run.
The last row of every run is a receipt: delivered, asked for, skipped as already-had, what stopped it, and the charged-event totals. A cost cap ends the collecting, never the delivering — rows already in hand are always written out.
Honest caps
- 40 pages per city, and that bound is ours. The run reads at most 40 feed pages for a single city before it stops. It is this actor's own ceiling and the row says so; it is never dressed up as the city running out of listings.
- The page size is the source's, not ours. Marketplace serves this feed in pages of up to 24 listings, and the page size is not something a caller can set.
maxListingsis still exact: a run that asks for 20 reads a 24-row page and delivers exactly 20. You are charged for the 20 that arrived — the other four are ours to absorb and never reach your invoice. - Public listings only, exactly as a logged-out reader is served them. There is no login here, so nothing member-only, nothing hidden, and no seller inbox.
- A keyword that matches nothing still returns rows. Facebook fuzzy-matches a search, so a nonsense query comes back with loosely-related listings rather than an empty answer. That is a relevance property of the surface, not a billing trick — but it does mean a misspelt
querycosts you delivered rows, so check the first few titles before you scale a run up. - The price band is enforced by this actor, because the source treats it as a hint. Marketplace answers a price range by widening to the nearest listings it has rather than returning fewer — ask for a band nothing sits inside and it still hands back a full page, every row outside it. Your band is sent anyway, because it narrows what comes back, and then it is applied again here: a listing outside
minPrice–maxPriceis dropped before anything is reserved, the next listing takes its slot, and you are never charged for a row your own band excluded. The run summary says how many were dropped. A listing that carries no price at all is not admitted by a band either, and is dropped the same way. - The radius is the source's own, and it widens.
radiusKmis sent on the search, but Marketplace treats it as a hint too: a 2 km ask around a small town comes back with listings from the towns around it, tens of km out. There is no coordinate on a search row to measure against, so this actor does not pretend to enforce it —city,stateandlocationNameare on every row, andlocationNameis the town the listing is actually in. Filter on that if the exact distance matters to you. - Sold listings are a separate request.
availability: "sold"returns sold listings; the default"available"does not mix them in.isSoldandisPendingare on every row either way. - Facebook may change how any of this is served. If it does, the rows that fail are labelled and not charged.
How often the data changes
Marketplace is a continuous feed rather than a daily digest: on the city feeds harvested for this actor's tests, the newest rows were hours old, not days. Two things move on a listing you already have — its price, which a seller can cut at any time, and its state, as isPending and isSold flip while the listing is still live. Nothing else on the row changes once it is posted.
So the pattern worth scheduling is the same cities and the same query daily, with daysSinceListed: "last_24_hours", rather than one deep run a week: a deep run buys the back catalogue over and over, a daily run buys the day. Repeat memory is what makes that cheap — the second run and every run after it pays only for listings that were not there yesterday. (A scheduled run started on a scoped API token needs the key-value store permission above, or it cannot read that memory at all.)
Two things follow from that, and they are worth setting deliberately:
- Re-pricing is the exception. If you want the same listings back to compare their prices, turn
skipListingsAlreadyDeliveredoff, or give that job its ownmemoryKey. Those rows are delivered and charged again, because they are rows you asked for a second time. - Leave
memoryKeyempty on a schedule. It is then derived from the cities, the query and the filter set, so an unchanged scheduled run keeps its own memory by itself and two different schedules never eat each other's.
What you put in
| Input | What it takes |
|---|---|
cities | One or more cities, one per line: a name like Austin, Texas (resolved for you), or a bare "30.27,-97.74" latitude/longitude pair, which skips the lookup. Austin, TX works too — a two-letter US state is spelled out before the lookup, and if that finds nothing your own spelling is tried once. A name that cannot be placed leaves one uncharged row and does not stop the run. |
query | The keyword — bicycle, ikea desk, f150. Leave it empty for the city's own Marketplace feed. |
maxListings | Hard ceiling on delivered listing rows for the WHOLE run, across every city. Default 100, exact. Asking for more than 5000 is clamped to 5000 and the clamp is never charged. |
radiusKm | How far around the city to search. Default 60, clamped to 1–500 in code. |
minPrice · maxPrice | A price band in whole currency units, passed straight to the search. No filter fee. |
condition | any · new · used_like_new · used_good · used_fair. |
daysSinceListed | any · last_24_hours · last_7_days · last_30_days — the field a daily schedule sets. |
deliveryMethod | any · local_pick_up · shipping. |
sortBy | best_match (default) · newest · price_low_to_high · price_high_to_low. |
availability | available (default) or sold. |
includeListingDetails | One extra row per listing: the full description, every photo, the attributes, the currency, the listing's own coordinates, and the shipping, Buy Now and messaging flags. Off by default, charged per details row delivered. |
skipListingsAlreadyDelivered | ON by default — a listing already delivered to your account is skipped before anything is bought, and the next new listing takes its place. Turn it off to collect the same listings again, for example to watch a price. |
memoryKey | Names the memory this run reads and writes. Leave it empty and it is derived from the cities, query and filters, so a schedule keeps its own memory automatically. |
maxUnresolvableCities | How many unplaceable city names the run will try before it stops resolving. Default 3, clamped to 0–20 in code. |
maxRunSeconds | The run stops cleanly before this many seconds and reports what is left, instead of being killed by a timeout. Default 1800, clamped to 30–3600 in code. A time limit ends the collecting, never the delivering. |
What you get
Three kinds of row, told apart by rowType. Every row carries charged and chargedEvent, so your invoice reconciles from the dataset itself.
rowType: "listing" — one per delivered listing, charged as a Listing.
| Column | What it holds |
|---|---|
listingId · url | The listing's own ID and its Marketplace link. |
title | What the seller called it. |
price · priceText · strikethroughPrice | The number, the string Facebook prints, and the crossed-out previous price where there is one. |
currency | Always null here — see the block above. |
listedAt · listedText | When it was listed, ISO-8601 UTC, and Facebook's own wording for it. |
city · state · locationName · cityPageId | Where the listing is, as the feed gives it. |
photoUrl | The primary photo. |
categoryId · mileage · mileageUnit · mileageText | The category, and the odometer reading on a vehicle listing. |
deliveryTypes | Local pick-up, shipping, or both. |
isSold · isPending · isLive | The listing's state at the moment it was read. |
description | Always null here — it is on the details row. |
searchCity · searchQuery | What you asked for, echoed onto the row, so a multi-city run sorts itself. |
charged · chargedEvent | Whether this row was billed, and as which event. |
rowType: "listingDetails" — only with includeListingDetails, charged as a Listing details row.
| Column | What it holds |
|---|---|
listingId · url · title | The listing this row is about. |
description | The seller's full description text — the reason this row exists. |
price · priceText · currency · strikethroughPrice | The price with its real currency code. |
listedAt · listedText · availabilityText | When it was listed and how Facebook states its availability. |
locationText · latitude · longitude | The listing's own approximate pin, not a street address. |
photoUrls | Every photo on the listing. |
attributes | Name, label and value for each — condition and whatever else the category carries. |
categoryId · mileage · mileageUnit | As on the feed row. |
deliveryTypes · isShippingOffered · isBuyNowEnabled · messagingEnabled | How the seller will transact. |
isSold · isPending · isLive · shareUri | State, and the share link Facebook serves for it. |
charged · chargedEvent | Whether this row was billed, and as which event. |
rowType: "note" — never charged, charged: false. One per miss, each with a status code from the list above and a plain sentence that opens with what you already have before it says anything about a fee. rowType: "summary" closes the run with the counts.
Column names are ours and they stay put. The source is read in one place and renamed once, so a Facebook rename does not rename your columns and a fixed-schema table never breaks on a new row.
Where the other Facebook links go
- A page or a public group, for its posts: Facebook page and group posts
- A reel, watch, share or fb.watch link: Facebook reels, with the MP4 and the transcript
- The comments under a post, in full: Facebook comments
- An Ad Library link, or the ads an advertiser is running right now: Meta Ad Library rows
Paste one of those into this actor and it comes back as an uncharged row naming the actor that takes it, rather than as a failed run.
Something not right?
Open the Issues tab on this actor with your run ID and the input you used, and it gets looked at. If it earned its keep, a rating helps other buyers find it, and saving the actor keeps it one click away.