# Craigslist Multi-City Listings Scraper (`devilscrapes/craigslist-listings-scraper`) Actor

Scrape Craigslist listings across many US metro areas and categories in one run. Get titles, prices, locations, images, posted/updated dates, structured attributes, descriptions, and public reply links -- normalized into one clean dataset instead of one run per city.

- **URL**: https://apify.com/devilscrapes/craigslist-listings-scraper.md
- **Developed by:** [DevilScrapes](https://apify.com/devilscrapes) (community)
- **Categories:** Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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

<div align="center">
  <img src=".actor/icon.svg" width="160" alt="Devil Scrapes mark" />

## Craigslist Multi-City Listings Scraper

**💰 $2.01 / 1 000 results**  ·  pay only for results  ·  no credit card to try

*We do the dirty work so your dataset stays clean.* 😈

Scrape Craigslist listings across many US metro areas and categories in one run. Get titles, prices, locations, images, posted/updated dates, structured attributes, descriptions, and public reply links -- normalized into one clean dataset instead of one run per city.

</div>

***

### 🎯 What this scrapes

Craigslist's own search pages (`/search/area/<areaSlug>`) and listing-detail pages (`/view/d/<slug>/<postId>`) -- fanned out across every metro area and category you pick, in one Actor call instead of one run per city. Point it at a single area, or union in a curated top-US-metro preset, pick one or all seven top-level categories, and get back a single normalized dataset: title, price, location, coordinates, images, posted and updated timestamps, structured attributes, description, and the public reply-flow link. Every request runs through our full anti-blocking stack by default -- rotating browser-fingerprint impersonation, session-rotated proxying, retries with backoff -- the same defenses every Devil Scrapes Actor ships, so your dataset holds up even if Craigslist changes its defenses tomorrow.

### 🔥 What we handle for you

- 🛡️ **We rotate browser fingerprints on every request** -- `curl-cffi` impersonates real Chrome, Firefox, and Safari TLS/HTTP handshakes, so nothing about the traffic looks like a bare Python script, today or if Craigslist tightens up tomorrow.
- 🌐 **We rotate Apify Proxy sessions** -- a fresh session ID and exit IP on any request that comes back looking blocked, no manual restart needed.
- 🔁 **We retry with exponential backoff** on `408 / 429 / 5xx`, honouring `Retry-After` -- up to 5 attempts before we give up on a page.
- 🧱 **We pace to the target's rate limits and isolate faults per combo** -- one slow metro or blocked category logs a warning and gets skipped; the rest of the run keeps going, and partial progress is always reported via a clear status message, never a silent empty dataset.
- 🧊 **Clean, typed dataset rows** -- every row is `ResultRow`-validated (Pydantic), ISO-8601 timestamps, stable post IDs, ready to export as JSON, CSV, or Excel.
- 💰 **Pay-per-result pricing** -- a small one-off `actor-start` charge, then you pay only for rows that land in your dataset.

### 💡 Use cases

- Reseller and arbitrage price-scanning across dozens of metros in a single run, instead of one Actor call per city.
- Multi-city lead-generation list building for local-services and real-estate outreach.
- Rental and vehicle market intelligence -- price and inventory tracked across metros side by side.
- Apartment and job aggregation, normalized into one schema instead of stitching together per-city exports.
- Posting-velocity research -- track price drops and reposts via `posted_at`, `updated_at`, and `is_edited_or_price_drop`.

### ⚙️ How to use it

1. Click **Try for free** at the top of the page.
2. Fill in the input form — most fields have sensible defaults.
3. Click **Start**. Output streams into the run's dataset.
4. Export from **Storage → Dataset** as JSON, CSV, or Excel — or fetch via the API.

### 📥 Input

| Field | Type | Required | Default | Notes |
|---|---|:--:|---|---|
| `areas` | `array` | no | \[] | Craigslist area slugs to search (e.g. "newyork", "losangeles") from /about/sites. Combined with All top US metros below… |
| `allTopUsMetros` | `boolean` | no | False | Union in a curated list of up to 25 top-US-metro Craigslist area slugs (not the full ~700-area directory) on top of… |
| `categories` | `array` | no | \['sss'] | Top-level Craigslist category codes to search: sss (for sale), cta (cars & trucks), reo (real estate), apa… |
| `query` | `string` | no | 'iphone' | Single search term, passed verbatim to Craigslist's search. Leave empty to browse a category with no keyword filter. |
| `maxResultsPerCombo` | `integer` | no | 100 | Cap on listings collected per (area, category) combo. 300 is Craigslist's observed single-page result-set ceiling. |
| `fetchDetails` | `boolean` | no | True | When enabled, fetch each listing's detail page for posted/updated dates, full description, structured attributes, and… |
| `postedWithinDays` | `integer` | no | '—' | Drop listings older than this many days. Only enforceable when Fetch listing detail pages is enabled -- posting date is… |
| `maxRunMinutes` | `integer` | no | 10 | Run-wide wall-clock budget. New (area, category) combos stop being started once this is exceeded; already-collected… |
| `proxyConfiguration` | `object` | no | {'useApifyProxy': True} | Apify Proxy configuration. Defaults to the standard group; we layer session rotation and fingerprint impersonation on… |

#### Example input

```json
{
  "areas": [
    "newyork"
  ],
  "allTopUsMetros": false,
  "categories": [
    "sss"
  ],
  "query": "iphone",
  "maxResultsPerCombo": 3,
  "fetchDetails": true,
  "postedWithinDays": null,
  "maxRunMinutes": 5,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

### 📤 Output

Every row is one dataset item.

| Field | Type | Notes |
|---|---|---|
| `area_slug` | `string` | Input area slug for this combo (e.g. 'newyork'). |
| `category_code` | `string` | Input top-level category code for this combo (e.g. 'sss'). |
| `query` | `string` | Input search query, verbatim; null in category-browse mode. |
| `post_id` | `string` | Craigslist posting ID, parsed from the detail-URL path. |
| `title` | `string` | Listing title, from the static search-result list. |
| `price` | `number` | Listing price; null for priceless categories (jobs/services). |
| `currency` | `string` | ISO currency code, from the joined JSON-LD offer when a price is present. |
| `location_text` | `string` | Free-text location, from the static search-result list. |
| `latitude` | `number` | Listing latitude, from the joined JSON-LD geo block. |
| `longitude` | `number` | Listing longitude, from the joined JSON-LD geo block. |
| `images` | `array` | Image URLs, from the joined JSON-LD block; empty array when none matched. |
| `detail_url` | `string` | Canonical https://www.craigslist.org/view/d/<slug>/<postId> listing URL. |
| `posted_at` | `string` | ISO-8601 (with offset) posted timestamp; null when not fetched or unavailable. |
| `updated_at` | `string` | ISO-8601 (with offset) updated timestamp, when present on the detail page. |
| `is_edited_or_price_drop` | `boolean` | Best-effort: true when updated\_at differs from posted\_at; null when undetermined. |
| `description` | `string` | Full listing description text, from the detail page's #postingbody. |
| `attributes` | `object` | Structured item attributes from the detail page's .attrgroup; shape varies per category. |
| `reply_url` | `string` | Public reply-flow URL as it appears on the detail page; never fetched or followed. |
| `repost_fingerprint` | `string` | Best-effort hash of normalized title + area + category; not a guaranteed dedup key. |
| `scraped_at` | `string` | ISO-8601 wall-clock time of row construction. |

#### Example output

```json
{
  "area_slug": "newyork",
  "category_code": "sss",
  "query": "iphone",
  "post_id": "7812345678",
  "title": "iPhone 14 Pro 256GB Unlocked",
  "price": 550.0,
  "currency": "USD",
  "location_text": "Queens",
  "latitude": 40.7282,
  "longitude": -73.7949,
  "images": [
    "https://images.craigslist.org/00A0A_abc_600x450.jpg"
  ],
  "detail_url": "https://www.craigslist.org/view/d/queens/7812345678",
  "posted_at": "2026-08-05T09:12:00-0400",
  "updated_at": "2026-08-09T14:03:00-0400",
  "is_edited_or_price_drop": true,
  "description": "Barely used, comes with box and charger...",
  "attributes": {
    "condition": "like new",
    "make": "apple"
  },
  "reply_url": "https://newyork.craigslist.org/reply/que/sss/7812345678/abc123",
  "repost_fingerprint": "a1b2c3d4",
  "scraped_at": "2026-08-12T12:00:00.000Z"
}
```

### 💰 Pricing

Pay-Per-Event — you pay only when these events fire:

| Event | USD | What it is |
|---|---:|---|
| `actor-start` | $0.01 | One-off warm-up charge per run |
| `result` | $0.002 | Per dataset item |

Example: 1 000 results at the rates above ≈ **$2.01**. No subscription, no minimum, no card to start — Apify gives every new account $5 of free credit.

### 🚧 Limitations

- Each `(area, category)` combo returns up to `maxResultsPerCombo` (max 300) -- Craigslist's observed single-page ceiling. There's no pagination past that in v1.
- Total combos (`areas x categories`, post-resolution) are capped at 150 per run, a FREE-tier compute guardrail -- split very large fan-outs across multiple runs.
- `allTopUsMetros` unions in a curated list of up to 25 metro slugs, not the full ~700-area Craigslist directory.
- One `query` term per run -- no multi-keyword search in a single call.
- `reply_url` is captured exactly as Craigslist renders it on the listing page. We never open it, solve any verification step it triggers, or de-obfuscate a masked email -- that boundary is deliberate, and Craigslist's own `robots.txt` disallows the reply flow.
- `repost_fingerprint` is a best-effort hash (normalized title + area + category), not a guaranteed unique dedup key.
- `postedWithinDays` only filters when `fetchDetails` is enabled -- posting date lives on the detail page only.

### ❓ FAQ

**Can I get a seller's phone number or email from this?**

No. We surface `reply_url` exactly as Craigslist places it on the listing page, but we never open it, solve any verification step it triggers, or de-obfuscate a masked email. That's a deliberate scope boundary, not a technical shortfall -- Craigslist's own `robots.txt` disallows the reply flow, and we respect it.

**Why one run instead of one run per city?**

Every incumbent we benchmarked (memo23, automation-lab, solidcode, ivanvs) makes you run their Actor once per metro, then stitch the exports together yourself. We fan out across up to 20 explicit areas plus an optional curated top-US-metro preset, and every category you pick, in one call, into one normalized dataset.

**What happens if one city or category is unreachable mid-run?**

We isolate failures per `(area, category)` combo -- a dead metro or a blocked category logs a warning and gets skipped, the rest of the run keeps going, and you get a clear status message on what finished. We never fail the whole run over one bad combo, and we never hand back a silent empty dataset.

**Do I need Residential proxy?**

Not by default -- we route every request through the standard Apify Proxy group with session rotation and browser-fingerprint impersonation layered on top. Switch `proxyConfiguration` to Residential anytime you want that extra layer for larger or more aggressive runs.

**How fresh are `posted_at` and `updated_at`?**

Both are read live from the listing's own detail page during your run, when `fetchDetails` is on. Turn `fetchDetails` off to skip the detail-page fetch entirely and get search-list fields only, faster and cheaper.

**Is this legal to run?**

We only fetch what Craigslist serves on its public search and listing pages. We never touch the reply flow, never log in, and never scrape any authenticated seller feature. Match your own use case against Craigslist's terms before using the data commercially.

### 💬 Your feedback

Spotted a bug, hit a weird edge case, or need a new field? Open an
issue on the Actor's **Issues** tab on Apify Console — we ship
fixes weekly and we read every report.

***

<div align="center">

Built by **[Devil Scrapes](https://apify.com/DevilScrapes)** 😈 — a small fleet of
opinionated public-data Actors. Honest pricing, real engineering, zero fine print.

</div>

# Actor input Schema

## `areas` (type: `array`):

Craigslist area slugs to search (e.g. <code>"newyork"</code>, <code>"losangeles"</code>) from <code>/about/sites</code>. Combined with <b>All top US metros</b> below when that's enabled. Up to 20 entries.

## `allTopUsMetros` (type: `boolean`):

Union in a curated list of up to 25 top-US-metro Craigslist area slugs (not the full ~700-area directory) on top of anything listed in <b>Areas</b>.

## `categories` (type: `array`):

Top-level Craigslist category codes to search: <code>sss</code> (for sale), <code>cta</code> (cars & trucks), <code>reo</code> (real estate), <code>apa</code> (apartments/housing), <code>jjj</code> (jobs), <code>bbb</code> (community), <code>ggg</code> (gigs). Use <code>"all"</code> to expand to all 7.

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

Single search term, passed verbatim to Craigslist's search. Leave empty to browse a category with no keyword filter.

## `maxResultsPerCombo` (type: `integer`):

Cap on listings collected per (area, category) combo. 300 is Craigslist's observed single-page result-set ceiling.

## `fetchDetails` (type: `boolean`):

When enabled, fetch each listing's detail page for posted/updated dates, full description, structured attributes, and the public reply-flow URL. When disabled, only search-list fields are returned.

## `postedWithinDays` (type: `integer`):

Drop listings older than this many days. Only enforceable when <b>Fetch listing detail pages</b> is enabled -- posting date is a detail-page-only field.

## `maxRunMinutes` (type: `integer`):

Run-wide wall-clock budget. New (area, category) combos stop being started once this is exceeded; already-collected rows are still flushed.

## `proxyConfiguration` (type: `object`):

Apify Proxy configuration. Craigslist's search and detail pages clear cleanly on the standard (non-residential) group -- no anti-bot behaviour observed.

## Actor input object example

```json
{
  "areas": [
    "newyork"
  ],
  "allTopUsMetros": false,
  "categories": [
    "sss"
  ],
  "query": "iphone",
  "maxResultsPerCombo": 100,
  "fetchDetails": true,
  "maxRunMinutes": 10,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `datasetItems` (type: `string`):

All dataset items as JSON.

## `datasetItemsCsv` (type: `string`):

Same data exported to CSV.

## `datasetView` (type: `string`):

Open the run dataset in the Console.

# 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 = {
    "areas": [
        "newyork"
    ],
    "allTopUsMetros": false,
    "categories": [
        "sss"
    ],
    "query": "iphone",
    "maxResultsPerCombo": 100,
    "fetchDetails": true,
    "maxRunMinutes": 10,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("devilscrapes/craigslist-listings-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 = {
    "areas": ["newyork"],
    "allTopUsMetros": False,
    "categories": ["sss"],
    "query": "iphone",
    "maxResultsPerCombo": 100,
    "fetchDetails": True,
    "maxRunMinutes": 10,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("devilscrapes/craigslist-listings-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 '{
  "areas": [
    "newyork"
  ],
  "allTopUsMetros": false,
  "categories": [
    "sss"
  ],
  "query": "iphone",
  "maxResultsPerCombo": 100,
  "fetchDetails": true,
  "maxRunMinutes": 10,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call devilscrapes/craigslist-listings-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,devilscrapes/craigslist-listings-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/IV5oezZruO4eoNQy6/builds/qiQgdhCgt8F1hh98C/openapi.json
