# Facebook Marketplace Scraper — Listings by City, Price, Date (`steadyfetch/facebook-marketplace-scraper`) Actor

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.

- **URL**: https://apify.com/steadyfetch/facebook-marketplace-scraper.md
- **Developed by:** [Steadyfetch Team](https://apify.com/steadyfetch) (community)
- **Categories:** E-commerce, Social media, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 listings

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Facebook Marketplace Scraper — Listings by City, Price, Date

**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. Leave `query` empty for the city's own feed.
- **Cap the bill:** set `maxTotalChargeUsd` on 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. Send `maxItems`, `limit` or `maxResults` instead — the names other Marketplace scrapers use — and the smallest one present is read as `maxListings`, 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:

- **`currency` is always `null` on a `listing` row.** 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 and `currency` is a real value on the `listingDetails` row, because the listing's own answer does carry it.
- **`description` is always `null` on a `listing` row**, for the same reason, and is the whole point of the `listingDetails` row.
- **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 `listing` row** (`photoUrl`, the primary one). Every photo the listing has (`photoUrls[]`) is on the `listingDetails` row.
- **No exact street address.** A `listing` row carries the city and state; a `listingDetails` row 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

1. **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.
2. **The limit is exact.** `maxListings` is 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.
3. **Listing details return the description and every photo.** `includeListingDetails` adds one `listingDetails` row 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`** — `maxUnresolvableCities` was 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. The `listing` row 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 own `maxListings`, and your own `maxTotalChargeUsd`.
- **`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. `maxListings` is 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 `query` costs 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`–`maxPrice` is 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.** `radiusKm` is 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`, `state` and `locationName` are on every row, and `locationName` is 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. `isSold` and `isPending` are 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 `skipListingsAlreadyDelivered` off, or give that job its own `memoryKey`. Those rows are delivered and charged again, because they are rows you asked for a second time.
- **Leave `memoryKey` empty 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](https://apify.com/steadyfetch/facebook-posts-scraper)
- A reel, watch, share or fb.watch link: [Facebook reels, with the MP4 and the transcript](https://apify.com/steadyfetch/facebook-reels-scraper)
- The comments under a post, in full: [Facebook comments](https://apify.com/steadyfetch/facebook-comments-scraper)
- An Ad Library link, or the ads an advertiser is running right now: [Meta Ad Library rows](https://apify.com/steadyfetch/facebook-ad-library-scraper)

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.

# Actor input Schema

## `cities` (type: `array`):

Cities to search, one per line. A name like "Austin, Texas" or "Seattle, Washington" is looked up for you; a bare "30.27,-97.74" latitude/longitude pair skips that lookup entirely. A name Facebook cannot place leaves one uncharged row naming the city and the run carries on with the rest of the list — the lookup it cost is on us, not on you. Leave this empty and press Start for the built-in sample: nothing is searched on Facebook and there is no result fee.

## `query` (type: `string`):

What to search for in each city — "bicycle", "ikea desk", "f150". Leave it empty for the city's own Marketplace feed. Facebook fuzzy-matches a keyword, so a misspelt word comes back with loosely-related listings rather than nothing: check the first few titles before scaling a run up.

## `maxListings` (type: `integer`):

Hard ceiling on delivered listing rows for the WHOLE run, across every city — not a limit per city. Exact: ask for 20 and 20 arrive. Asking for more than 5000 is clamped to 5000, and the clamp itself is never charged. Also answers to `maxItems`, `limit` and `maxResults` — the names other Marketplace scrapers use — and the smallest one present binds, with one uncharged note row saying which name was read.

## `radiusKm` (type: `integer`):

How far around each city to search. Default 60. Anything outside 1–500 is clamped into that range and the clamp is never charged; one uncharged note row says what you typed and what the run used.

## `minPrice` (type: `integer`):

Lowest price to return, in whole currency units, passed straight to the search. Leave it empty for no floor. Listings the band drops are never delivered and never charged.

## `maxPrice` (type: `integer`):

Highest price to return, in whole currency units, passed straight to the search. Leave it empty for no ceiling. If it ends up below the minimum, the run reads the two the other way round rather than returning nothing, and says so on one uncharged note row.

## `condition` (type: `string`):

Only listings in this condition. "any" sends no condition filter at all. A value this actor does not offer is refused on one uncharged row naming the values that work, and the run uses "any" rather than quietly searching a different condition.

## `daysSinceListed` (type: `string`):

Only listings posted inside this window. "any" sends no date filter. This is applied by Facebook on the search, so a narrow window buys fewer pages — it is the field a daily schedule sets. A value this actor does not offer is refused on one uncharged row naming the windows that work.

## `deliveryMethod` (type: `string`):

Only listings the seller will hand over this way. "any" sends no delivery filter. A value this actor does not offer is refused on one uncharged row naming the values that work.

## `sortBy` (type: `string`):

The order Facebook returns the search in. A value this actor does not offer is refused on one uncharged row naming the orders that work, and the run uses "best\_match" — a swapped order returns billed rows that look exactly like the ones you asked for, so it is never substituted behind your back.

## `availability` (type: `string`):

"available" returns listings still for sale; "sold" returns sold ones. They are separate requests and the default never mixes them. `isSold` and `isPending` are on every row either way.

## `includeListingDetails` (type: `boolean`):

ON adds one Listing details row per delivered listing: the seller's full description text, every photo, the attributes such as condition, the real currency code, the listing's own approximate latitude and longitude, the availability wording, the share link, and whether shipping, Buy Now and messaging are offered. Charged as one Listing details unit per row delivered, $0.012 on the Apify free plan and from $0.005 on paid plans. A listing taken down before its details are read leaves one uncharged row saying so, and the listing row above it still stands and states its own charge.

## `skipListingsAlreadyDelivered` (type: `boolean`):

ON (default): a listing already delivered to your account is skipped before anything is reserved or bought — no row, no charge — and the next new listing takes its place, so you still get the number you asked for. OFF: those listings are collected and delivered again, and charged again, which is what you want when you are watching a price change. Listings older than 90 days in your account's memory count as new again either way.

## `memoryKey` (type: `string`):

Which memory this run reads and writes. Leave it empty and it is derived from the cities, the keyword and the filters, so a scheduled run keeps its own memory by itself and two different schedules never eat each other's. Set it to share one memory across several different searches, or to give one job a memory of its own.

## `maxUnresolvableCities` (type: `integer`):

How many city names Facebook fails to place before the run stops trying to place any more. Default 3. Set it to 0 to never look a name up at all, which suits a run that sends only "latitude,longitude" pairs. Anything outside 0–20 is clamped into that range and the clamp is never charged. Every name the cap stops leaves its own uncharged row saying so.

## `maxRunSeconds` (type: `integer`):

The run stops cleanly before this many seconds and reports what is left, instead of being killed by a timeout. Anything outside 30–3600 is clamped into that range and the clamp is never charged. Never charged for the listings it did not reach — they ship as uncharged rows naming the clock.

## Actor input object example

```json
{
  "cities": [],
  "query": "",
  "maxListings": 100,
  "radiusKm": 60,
  "condition": "any",
  "daysSinceListed": "any",
  "deliveryMethod": "any",
  "sortBy": "best_match",
  "availability": "available",
  "includeListingDetails": false,
  "skipListingsAlreadyDelivered": true,
  "memoryKey": "",
  "maxUnresolvableCities": 3,
  "maxRunSeconds": 1800
}
```

# Actor output Schema

## `listings` (type: `string`):

One `rowType: listing` row per Marketplace listing delivered: 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, the primary photo, the category ID, mileage on a vehicle listing, the delivery types, and whether the listing is sold, pending or still live. `searchCity` and `searchQuery` echo what you asked for, so a multi-city run sorts itself. `currency` and `description` are always an explicit null on this row: neither is in the search feed a logged-out reader is served, and this actor will not promise a column it cannot fill — both are real on a Listing details row. No seller is returned on any row: a Marketplace seller is a member of the public. Every row carries `charged` and `chargedEvent`, so the invoice reconciles from the dataset itself, and starting a run costs nothing. Listings your account already had are skipped before anything is bought and are never charged again.

## `listingDetails` (type: `string`):

One `rowType: listingDetails` row per listing, and only with "Include listing details" on: the seller's full description text, every photo on the listing, the attributes (condition and whatever else the category carries), the real currency code, the listing's own approximate latitude and longitude, the availability wording, the share link, and whether shipping, Buy Now and messaging are offered. Charged as one Listing details unit per row delivered. A listing that is gone by the time its details are read leaves one uncharged row saying so; the listing row above it still stands and states its own charge.

## `misses` (type: `string`):

One uncharged row for everything that did not arrive, each with a status code: `city_not_found` (the city name could not be placed — the lookup was on us, not on you), `city_cap` (your `maxUnresolvableCities` was reached and the run stopped placing names), `no_listings` (the search returned nothing for that city), `listing_gone` (the listing was no longer available when its details were read), `source_error` (the read did not go through — temporary, and a re-run is the fix), `limit_reached` and `budget_reached` (your own `maxListings` and `maxTotalChargeUsd`), `skipped.all_repeat` (every listing on that page had already been delivered to your account), and `skipped.page_cap` (a bound of THIS ACTOR's own — the feed was still serving and the run had read the 40 pages it buys for one city, which is never a verdict about the city). None of these is charged, and each sentence opens with what you already have before it says anything about a fee.

## `summary` (type: `string`):

Listings delivered, what was asked for, listing detail rows delivered, listings your account already had and what that saved, what stopped the run, and the charged-event totals.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "cities": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadyfetch/facebook-marketplace-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "cities": [] }

# Run the Actor and wait for it to finish
run = client.actor("steadyfetch/facebook-marketplace-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "cities": []
}' |
apify call steadyfetch/facebook-marketplace-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadyfetch/facebook-marketplace-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/xjDNX10b1CBjuIf51/builds/cyVNCGc87bXXzcWAF/openapi.json
