How to Get Facebook Marketplace Listings with Python (Search, Filter, Track Prices)
Meta has no public Marketplace API and BeautifulSoup scripts break. Search Facebook Marketplace by keyword, location and price with one API call, export listings to CSV, and get alerts for new listings and price drops, in Python.
The reliable way to get Facebook Marketplace listings in Python is to call a Marketplace search API instead of parsing Facebook's pages: send a keyword, a location (latitude and longitude) and optional price filters, and get back JSON with each listing's title, price, location, photo, status and link. Meta has no public Marketplace API, and Marketplace pages are rendered with JavaScript behind login prompts, so requests + BeautifulSoup scripts break constantly. Below: the API call, the real response, a script that exports listings to CSV, and one that alerts you when a new matching listing appears.
Why the BeautifulSoup approach doesn't hold up
Most tutorials show requests.get("https://www.facebook.com/marketplace/...") plus CSS selectors. In practice:
- The listings aren't in the HTML: Marketplace loads them with JavaScript after the page opens, so the HTML you download has no listing cards.
- Facebook shows a login wall after a few pages, and changes its class names often, so selectors stop matching.
- Headless browsers (Playwright, Selenium) get further, but need a logged-in account to stay useful, which is fragile and against Facebook's terms for automation.
An API returns the same public listings as structured data and handles the collection side for you.
What you need
- A SocialAPIs API key: the free plan has 200 calls a month, no card (sign up).
- Python 3.9+ and
requests(pip install requests).
The API call
GET /facebook/marketplace/search:
1curl -G "https://api.socialapis.io/facebook/marketplace/search" \
2 -H "x-api-token: $SOCIALAPIS_API_KEY" \
3 --data-urlencode "query=hot wheels" \
4 --data-urlencode "filter_location_latitude=40.7142" \
5 --data-urlencode "filter_location_longitude=-74.0064" \
6 --data-urlencode "filter_radius_km=40" \
7 --data-urlencode "filter_price_upper_bound=100"| Parameter | What it does |
|---|---|
query | keyword, e.g. iphone 15 |
filter_location_latitude, filter_location_longitude | search center; get them for a city name with /facebook/marketplace/city-coordinates |
filter_radius_km | radius around the center, e.g. 40 |
filter_price_lower_bound, filter_price_upper_bound | whole numbers in the local currency |
commerce_search_and_rp_condition | new, used_like_new, used_good, used_fair |
commerce_search_sort_by | BEST_MATCH, CREATION_TIME_DESCEND (newest first), PRICE_ASCEND… |
posted_today | true for today's listings only |
category_url | a category slug from /facebook/marketplace/categories |
end_cursor | next page, from page_info.end_cursor |
Each call costs 1 credit and returns about 24 listings (pricing). For cars and rentals there are dedicated endpoints with mileage, year, bedroom and bathroom filters: /facebook/marketplace/vehicles and /facebook/marketplace/rentals.
The response
Trimmed to one listing (seller name removed):
1{
2 "data": {
3 "page_info": { "end_cursor": "{\"pg\":0,…}", "has_next": true },
4 "items": [
5 {
6 "id": "1647394070433636",
7 "listingUrl": "https://www.facebook.com/marketplace/item/1647394070433636",
8 "marketplace_listing_title": "Toy cars lot (read description) disney pixar",
9 "listing_price": { "formatted_amount": "$20", "amount": "20.00" },
10 "strikethrough_price": { "formatted_amount": "$35", "amount": "35.00" },
11 "location": { "reverse_geocode": { "city": "Paterson", "state": "NJ" } },
12 "is_live": true,
13 "is_pending": false,
14 "is_sold": false,
15 "delivery_types": ["IN_PERSON", "PUBLIC_MEETUP", "DOOR_PICKUP"],
16 "primary_listing_photo": { "photo_image_url": "https://scontent…" },
17 "listing_video": null,
18 "marketplace_listing_seller": { "name": "…", "id": "…" }
19 }
20 ]
21 },
22 "meta": { "creditsCharged": 1, "creditsRemaining": 199 }
23}The fields you'll use most: id, listingUrl, marketplace_listing_title, listing_price.amount (a string, convert it to a number), strikethrough_price (set when the seller lowered the price, null otherwise), location.reverse_geocode, is_sold / is_pending, and delivery_types (IN_PERSON, SHIPPING_ONSITE, PUBLIC_MEETUP…). For a listing's full description and all photos, call /facebook/marketplace/listing with its id.
Script 1: search and export to CSV
1import csv
2import os
3
4import requests
5
6API_KEY = os.environ["SOCIALAPIS_API_KEY"]
7URL = "https://api.socialapis.io/facebook/marketplace/search"
8
9
10def search(query, lat, lng, radius_km=40, max_price=None, pages=2):
11 params = {
12 "query": query,
13 "filter_location_latitude": lat,
14 "filter_location_longitude": lng,
15 "filter_radius_km": radius_km,
16 }
17 if max_price is not None:
18 params["filter_price_upper_bound"] = max_price
19
20 listings = []
21 for _ in range(pages):
22 response = requests.get(URL, headers={"x-api-token": API_KEY}, params=params, timeout=60)
23 response.raise_for_status()
24 data = response.json().get("data", {})
25 listings.extend(data.get("items", []))
26 page_info = data.get("page_info") or {}
27 if not page_info.get("has_next"):
28 break
29 params["end_cursor"] = page_info["end_cursor"]
30 return listings
31
32
33def to_row(item):
34 place = (item.get("location") or {}).get("reverse_geocode") or {}
35 was = item.get("strikethrough_price") or {}
36 return {
37 "id": item["id"],
38 "title": item.get("marketplace_listing_title", ""),
39 "price": float((item.get("listing_price") or {}).get("amount") or 0),
40 "was": float(was["amount"]) if was.get("amount") else "",
41 "city": place.get("city", ""),
42 "state": place.get("state", ""),
43 "sold": item.get("is_sold", False),
44 "shipping": "SHIPPING_ONSITE" in (item.get("delivery_types") or []),
45 "url": item.get("listingUrl", ""),
46 }
47
48
49if __name__ == "__main__":
50 rows = [to_row(i) for i in search("hot wheels", 40.7142, -74.0064, max_price=100)]
51 rows.sort(key=lambda r: r["price"])
52 with open("listings.csv", "w", newline="") as f:
53 writer = csv.DictWriter(f, fieldnames=rows[0].keys() if rows else ["id"])
54 writer.writeheader()
55 writer.writerows(rows)
56 print(f"Saved {len(rows)} listings to listings.csv")Two pages is about 48 listings for 2 credits. The script keeps only what you need for a price comparison; it doesn't store seller names.
Script 2: alert on new listings and price drops
Run it every 30 minutes; it remembers each listing's last price and prints only new listings and price drops (swap print for a Slack webhook, email or Telegram).
1import json
2from pathlib import Path
3
4from marketplace_search import search, to_row
5
6STATE = Path("marketplace_state.json")
7
8
9def main():
10 known = json.loads(STATE.read_text()) if STATE.exists() else {}
11 first_run = not known
12
13 for row in map(to_row, search("hot wheels", 40.7142, -74.0064, max_price=100, pages=1)):
14 if row["sold"]:
15 continue
16 previous = known.get(row["id"])
17 known[row["id"]] = row["price"]
18 if first_run:
19 continue
20 if previous is None:
21 print(f"New: {row['title']} for ${row['price']:.0f} in {row['city']}\n{row['url']}")
22 elif row["price"] < previous:
23 print(f"Price drop: {row['title']} ${previous:.0f} → ${row['price']:.0f}\n{row['url']}")
24
25 STATE.write_text(json.dumps(known))
26
27
28if __name__ == "__main__":
29 main()Save Script 1 as marketplace_search.py next to it. Add commerce_search_sort_by=CREATION_TIME_DESCEND to the search parameters if you want the newest listings first. Every 30 minutes is about 1,440 calls a month for one search.
No code: n8n, Make, Zapier
The same flow works in any automation tool: a schedule, an HTTP request to /facebook/marketplace/search with the x-api-token header, a split on data.items, a "remove duplicates" step on id, and a Slack, email or Google Sheets step. In n8n that's Schedule Trigger → HTTP Request → Split Out (data.items) → Remove Duplicates (previous executions, {{ $json.id }}) → Slack.
With an AI assistant
The SocialAPIs MCP server gives Claude, ChatGPT or Cursor a facebook_marketplace tool, so you can ask "find used road bikes under $400 within 30 km of Austin and sort them by price" and get the listings back.
Is this allowed?
These are listings anyone can see on Marketplace without logging in. Keep it that way: use public listing data for search, price research and alerts; don't collect sellers' names or profiles unless your use case needs them (in the EU and UK they're personal data under GDPR); and don't contact sellers in bulk or republish their photos as your own.
Common questions
How do I search a different city? Call /facebook/marketplace/city-coordinates?city=Austin for its latitude and longitude, or use any coordinates you already have, plus filter_radius_km.
Can I get the listing description? Yes, with /facebook/marketplace/listing?listing_id=<id> (1 credit per listing). The search result only carries the title.
Prices are strings? Yes: listing_price.amount is "20.00". Convert with float(); formatted_amount has the currency symbol for display.
Is there a cars-only search? /facebook/marketplace/vehicles filters by price, year, mileage and individual sellers; /facebook/marketplace/rentals by bedrooms, bathrooms and price.
Get a free API key and run Script 1 on your own search: sign up, 200 calls a month free.