← All posts

How to Scrape Facebook Marketplace Listings and Prices

Someone lists a barely-used road bike for 200 dollars in your city, and by the time you see it in the app three hours later it is gone. If you resell, flip, or just hunt deals seriously, you have felt this. The obvious fix is to watch listings programmatically so you see the price the moment it posts. Then you try to scrape Facebook Marketplace and hit a wall that most tutorials quietly skip over.

Let me be straight about what works, what does not, and where the real line sits.

The Login Wall Is the Whole Problem

Facebook Marketplace is not a normal public web page. Most of what you want to see sits behind an authentication gate, and Facebook is one of the most aggressive anti-automation operators on the internet. That combination is what makes this hard, not the HTML parsing.

Here is what you run into, in the order you hit it:

  • Auth-gated content. Scroll a category or open many listings without logging in and you get bounced to a login prompt. The full feed, seller profiles, and messaging all require an account.
  • Automation detection. Facebook fingerprints browsers heavily: canvas, WebGL, timing, mouse movement, TLS handshake. A vanilla headless browser is spotted fast.
  • Account risk. The moment you automate a real logged-in account, you are risking that account. Checkpoints, rate blocks, and bans are common, and they escalate quickly against scripted behavior.

That last point is the one people learn the expensive way. Do not point automation at your personal account and expect it to survive. If you go the logged-in route at all, it is a burner account you are prepared to lose, and even then you are on borrowed time.

What Is Actually Public

The good news: a meaningful slice of Marketplace is reachable without logging in. Individual listing pages and some city or category views render for logged-out visitors, especially when you arrive with a direct URL rather than trying to browse the feed. Those pages carry the fields that matter for a resale workflow: title, price, location, description, and the listing images.

That is the honest scope of this post. We are pulling public listing data from pages Facebook serves to anyone, not simulating a logged-in human clicking through a private feed. If you need the authenticated feed, that is a different and much riskier project, and one I would not build on a scraper at all.

Two hard constraints shape any approach:

  1. Facebook's terms prohibit automated collection. Scraping public data is legally murkier than people claim, and Facebook actively litigates. Read Is web scraping legal before you build anything at scale, and keep your volume low and your purpose defensible.
  2. The pages are heavily JavaScript-rendered and bot-hardened. curl gets you an empty shell or a login redirect, every time.

Why curl and requests Fail Here

The request everyone writes first does nothing useful:

import requests
r = requests.get("https://www.facebook.com/marketplace/item/1234567890/")
print(r.status_code)  # often 200, but the body is a login shell, not the listing

You get a status that looks fine and a body that contains none of the data you wanted. The price and description are hydrated by JavaScript after the page loads, and the request never got far enough to run any of it. Spoofing a User-Agent header does not help, because your TLS fingerprint already told Facebook you are a Python client. This is the same layer-below-HTTP problem that shows up on Cloudflare sites, and the mechanics are worth understanding: see curl vs headless vs stealth browser.

To get a rendered listing page from a logged-out state, you need a real browser fingerprint, JavaScript execution, and often a residential egress IP so the request does not look like it came from a datacenter. Building and maintaining that stack against a target that changes its defenses constantly is the actual work.

Fetching a Public Listing With link.sc

Rather than run a browser fleet, you can hand the URL to a fetch API that already climbs the anti-bot ladder for you. link.sc renders JavaScript, presents a real browser fingerprint, and can route through residential IPs, then hands back clean structured data. You give it a schema describing the fields you care about and it returns exactly those.

import requests

schema = {
    "type": "object",
    "properties": {
        "title": {"type": "string"},
        "price_usd": {"type": "number"},
        "location": {"type": "string"},
        "description": {"type": "string"},
        "image_urls": {"type": "array", "items": {"type": "string"}},
    },
}

resp = requests.post(
    "https://api.link.sc/v1/fetch",
    headers={"x-api-key": "lsc_YOUR_KEY"},
    json={
        "url": "https://www.facebook.com/marketplace/item/1234567890/",
        "format": "json",
        "schema": schema,
    },
)
listing = resp.json()["data"]
print(listing["price_usd"], listing["title"])

You get back something like {"title": "Trek road bike 56cm", "price_usd": 200, "location": "Austin, TX", ...} instead of a 500KB blob of React state you have to reverse-engineer. If a listing has gone behind a hard block, you find out from the response rather than from a silently empty field.

Turning It Into a Resale Watch

The reason to automate this is speed, so the shape of the system is a poller plus a diff. This is where a Marketplace scraper diverges from a normal ecommerce price monitor: you are not watching one product's price drift over months, you are watching for new inventory appearing under a price threshold, then reacting in minutes.

A practical loop looks like this:

  1. Keep a small list of listing URLs or search-result pages you care about, scoped to your city and categories.
  2. Fetch each on a schedule (every 15 to 30 minutes is plenty; going faster mostly raises your block rate and your risk).
  3. Normalize each result to the schema above and store it keyed by listing ID.
  4. Alert on two events: a listing ID you have never seen, and a price on a known ID that dropped.
import json, pathlib

seen = json.loads(pathlib.Path("seen.json").read_text() or "{}")
listing_id = "1234567890"

prev = seen.get(listing_id)
if prev is None:
    notify(f"NEW: {listing['title']} at ${listing['price_usd']} in {listing['location']}")
elif listing["price_usd"] < prev["price_usd"]:
    notify(f"DROP: {listing['title']} ${prev['price_usd']} -> ${listing['price_usd']}")

seen[listing_id] = listing
pathlib.Path("seen.json").write_text(json.dumps(seen, indent=2))

That "new listing" alert is the whole edge for arbitrage. Being ninety minutes early on an underpriced item is the difference between messaging the seller first and reading "is this still available" into a void. If you want the general pattern for value-based diffing without false-positive noise, the competitor pricing monitor post covers it in more depth.

Where to Draw the Line

Keep three rules and you stay on the sane side of this. Only touch public pages, never a private authenticated feed through automation. Keep volume genuinely low, because polite and undetected are the same behavior here. And never automate an account you cannot afford to lose. Facebook is not a target that rewards greed, and the difference between a small, slow, well-behaved watcher and a banned account is mostly restraint.

Scraping Facebook Marketplace is possible for public listings, unreliable for anything behind the login, and legally sensitive throughout. Go in knowing that and you can build something genuinely useful for spotting deals before everyone else does.


Point a URL at link.sc and get clean structured listing data back, with the anti-bot ladder handled for you and 500 free credits every month.