# Craigslist Scraper (`mlg14/craigslist-scraper`) Actor

Search public Craigslist listings by city, category, keyword, or URL and export prices, descriptions, attributes, images, and approximate locations.

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

## Pricing

$1.00 / 1,000 results

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/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Craigslist Scraper

Scrape public Craigslist classifieds by city, category, keyword, or a search URL and export Craigslist data as CSV or JSON. This Craigslist API alternative collects one row per posting, including prices, the full description, category attributes, images, and approximate location when detail enrichment is enabled.

The actor accepts both guided searches and URLs copied from the site. A search URL keeps its existing filters, which is useful for a carefully tuned search that includes a radius, a neighborhood, seller type, bedrooms, or other category-specific settings. Search results and posting details come from the public data used by Craigslist's own pages. No account is required.

### What data can you extract from Craigslist?

Each output row is a listing. Fields are flat and stable; category-specific facts appear in the `attributes` object. The example values below describe the fields, while the complete real result in the output section shows how they fit together.

| Field | Description | Example |
|---|---|---|
| `url` | Direct public posting link | `https://www.craigslist.org/view/d/...` |
| `postId` | Numeric posting identifier, stored as text | `7932622659` |
| `title` | Listing headline | `Electric bike front suspension 20”x 4.00` |
| `priceUsd` | Legacy numeric price field; always read `currency` | `80` |
| `price` | Numeric price in the listing's local currency | `80` |
| `currency` | Currency code for the city | `USD` |
| `region` | City hostname | `newyork` |
| `categorySlug` | Category used for the search | `sss` |
| `categoryLabel` | Broad category on the posting | `for sale` |
| `subcategorySlug` | Posting's specific category code | `bop` |
| `subcategoryLabel` | Specific category name | `bicycle parts - by owner` |
| `postedAt` | Original posting time, expressed in UTC | `2026-09-26T12:37:44Z` |
| `updatedAt` | Update time supplied by the posting data | `2026-09-26T12:32:44Z` |
| `location` | Seller's displayed area description | `brooklyn` |
| `latitude` | Approximate map latitude | `40.7012` |
| `longitude` | Approximate map longitude | `-73.9436` |
| `attributes` | Extra fields provided for a category | `{"condition":"new"}` |
| `description` | Full public posting body | `New fork with lock system never installed` |
| `imageUrls` | Listing gallery image links | An array of image URLs |
| `hasImages` | Whether at least one image is available | `true` |
| `phoneNumbers` | Numbers explicitly written in the public body | `[]` |
| `emails` | Addresses explicitly written in the public body | `[]` |
| `contactObfuscated` | Whether the public body lacks direct contact details | `true` |
| `replyToken` | Area, category, and ID for the public reply route | `nyc/bop/7932622659` |
| `isDeleted` | Detail request reported a removed posting | `false` |
| `scrapedAt` | Time this row was collected, in UTC | `2026-09-26T14:15:32Z` |

`priceUsd` is retained for compatibility with existing pipelines, but its name is misleading outside US cities. Use `price` together with `currency` for calculations. `attributes` can contain keys such as `condition`, `sale_manufacturer`, `bedrooms`, or `sqft`; the available keys depend on the category and on what the poster supplied. Missing optional details are represented by `null`, an empty object, or an empty array rather than a fabricated value.

### How to scrape Craigslist

1. Choose a city and category. Enter an optional keyword such as `bike`, `honda civic`, or `studio apartment`. You can instead paste one or more public search or posting URLs into `startUrls`.
2. Set filters that make the results useful: a local-currency price range, condition, image requirement, posted-today flag, or sort order. These guided filters apply when `startUrls` is empty.
3. Choose a result limit and decide whether to include full details. With details enabled, each selected search result receives a posting request for its full body, specific category, attributes, and complete image list.
4. Run the actor and inspect the dataset. Download the rows as JSON or CSV, or use the dataset endpoint in an existing workflow.

For example, a New York for-sale search for `bike` with a 40-row limit returned 40 rows in a verified run. Every row had a posting ID, URL, title, price, posting time, description, and region. The result order and individual postings will change as people add, edit, and remove listings.

### Input

Guided parameters are used when `startUrls` is empty. A pasted URL wins over the city, category, keyword, and guided filters so that the search retains the exact settings already encoded in the URL. Multiple URLs are processed in their supplied order, with duplicate post IDs removed across the run.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `startUrls` | URL list | Empty | Search, category, or individual public posting URLs. |
| `region` | Text | `newyork` | City subdomain for guided searches; any valid city hostname can be entered. |
| `category` | Select | `sss` | Broad or specific category code for guided searches. |
| `searchTerm` | Text | Empty | Optional terms matched by the site's search. |
| `priceMin` | Integer | Empty | Minimum price in the city's local currency. |
| `priceMax` | Integer | Empty | Maximum price in the city's local currency. |
| `condition` | Select | `any` | Seller-selected condition for applicable for-sale posts. |
| `hasImage` | Boolean | `false` | Require a listing image. |
| `postedToday` | Boolean | `false` | Request listings marked as posted today. |
| `sort` | Select | `date` | Newest, relevance, ascending price, or descending price. |
| `includeDetails` | Boolean | `true` | Enrich each result with the full public posting data. |
| `maxResults` | Integer | `200` | Hard row cap across sources; `0` selects the internal 100,000-row cap. |
| `proxyConfiguration` | Object | Enabled | Connection settings for public page and data requests. |

A small guided search:

```json
{
  "region": "newyork",
  "category": "sss",
  "searchTerm": "bike",
  "sort": "date",
  "includeDetails": true,
  "maxResults": 40
}
```

For a search already configured in a browser, pass its URL instead:

```json
{
  "startUrls": [
    "https://newyork.craigslist.org/search/sss?query=bike&min_price=50&hasPic=1"
  ],
  "includeDetails": true,
  "maxResults": 100
}
```

A URL can also point to an individual posting. This is useful for checking a known listing alongside a discovery search. The actor resolves the posting from its public page data and collects the same output structure. A posting that has been removed or become unavailable may not yield a row.

### Output example

This is a real row from the 40-result New York bike search. The gallery is shortened to one URL for display; the dataset row contains all four image links.

```json
{
  "url": "https://www.craigslist.org/view/d/brooklyn-electric-bike-front-suspension/bVVrjbGiFSfJeR7wmVNNDq",
  "postId": "7932622659",
  "title": "Electric bike front suspension 20”x 4.00",
  "priceUsd": 80,
  "price": 80,
  "currency": "USD",
  "region": "newyork",
  "categorySlug": "sss",
  "categoryLabel": "for sale",
  "subcategorySlug": "bop",
  "subcategoryLabel": "bicycle parts - by owner",
  "postedAt": "2026-09-26T12:37:44Z",
  "updatedAt": "2026-09-26T12:32:44Z",
  "location": "brooklyn",
  "latitude": 40.7012,
  "longitude": -73.9436,
  "attributes": {},
  "description": "New fork with lock system never installed\n\nHead tube diameter 28.6mm  8-3/4 long\n\nBreak Caliper spacing 74mm\n\nWheel spacing 135mm",
  "imageUrls": [
    "https://images.craigslist.org/00E0E_fLeUAIOretO_0t20CI_600x450.jpg"
  ],
  "hasImages": true,
  "phoneNumbers": [],
  "emails": [],
  "contactObfuscated": true,
  "replyToken": "nyc/bop/7932622659",
  "isDeleted": false,
  "scrapedAt": "2026-09-26T14:15:32.572584Z"
}
```

The `description` is plain text. Line breaks are retained so that dimensions, lists, and paragraphs remain readable. Image URLs refer to the public gallery; image files themselves are not copied into the dataset. Public contact details are extracted only if a poster wrote them in the listing body. The reply relay is not opened or used.

### Use cases

- **Local inventory tracking.** A reseller can schedule a focused search by city, category, and price range, then compare post IDs over time. The dataset makes it straightforward to separate newly observed listings from repeat observations. A missing listing in a later search does not, by itself, prove deletion; it may have fallen outside the current result window.
- **Housing market observation.** A researcher can collect apartments or rooms for selected cities and compare asking prices, neighborhood labels, and available category attributes. Geographic coordinates are approximate, so analysis should be aggregated to areas rather than used as exact addresses.
- **Vehicle comparison.** A buyer can search cars and trucks by keyword and price, then use the complete description and category attributes to build a shortlist for manual review. Filters in a pasted URL can retain city-specific radius, seller type, and other options.
- **Job and gig discovery.** A community organization can monitor a public jobs or gigs category for topics relevant to its members. Job listings often lack a numeric price, so the `price` field should be treated as optional rather than as a salary.
- **Catalog cleanup.** A publisher with a collection of saved posting URLs can collect current public details into one structured dataset. IDs and URLs support deduplication and later checks, while `scrapedAt` records when the snapshot was taken.

These are snapshots of public listings, not a live inventory or a guarantee that an offer remains available. Review the original posting before making a decision or contacting a poster.

### How much does it cost to scrape Craigslist?

The configured result price is **$1.00 per 1,000 delivered rows**. One output row is one result, whether details are enabled or disabled. Platform usage is included in the result price. A 40-row dataset corresponds to about $0.04 at this rate; billing rules and any account credits can affect the amount shown on a particular run.

| Delivered rows | Result-price calculation | Estimated charge |
|---:|---:|---:|
| 100 | `100 × $0.001` | `$0.10` |
| 1,000 | `1,000 × $0.001` | `$1.00` |
| 5,000 | `5,000 × $0.001` | `$5.00` |

Set `maxResults` before a broad search so that the expected number of rows is clear. The actor stops pushing rows at the cap or when the run's spending limit is reached. Detail enrichment adds requests and time, but does not add a second result row for the same posting.

### Tips for best results

- **Start narrow.** A city, specific category, and meaningful keyword usually produce a more relevant set than a city-wide all-for-sale query. Check the result count after a small run before increasing `maxResults`.
- **Use site URLs for advanced filters.** If the guided fields do not cover a filter you use on the site, set the filter in a browser and paste the resulting search URL. The encoded query parameters are kept for that source.
- **Choose the detail setting deliberately.** Turn `includeDetails` on when you need full descriptions, attributes, and specific categories. Turn it off for a faster sweep of titles, prices, timestamps, approximate locations, and available images.
- **Watch currency and optional prices.** Canadian city results use `CAD`; US cities use `USD`. Jobs, community posts, and other categories can have no price. Do not sum `priceUsd` across regions without checking `currency`.
- **Deduplicate with `postId`.** One posting may appear in more than one search or be encountered again in a later run. The actor removes duplicate IDs within a run; use the ID in your downstream system when combining runs.
- **Split broad searches.** Large result sets can exceed the site's visible search window. Use category, city, subarea, keyword, price range, or date filters to create smaller searches, then combine and deduplicate their rows.

### Limits

Craigslist changes its public search and posting data over time. A result count is a current snapshot, not a promise that every matching listing will remain available through the run. Individual postings can disappear between the search request and the detail request. In that case, `isDeleted` can be true and detail fields can be empty. A posting URL supplied directly may return no row if it is already unavailable.

Broad queries may be limited by the site's search window. The actor asks for the initial compact results and, when needed, retrieves the full visible result list in batches. It does not automatically split one query into every possible neighborhood, price, or date slice. For comprehensive collection, provide narrower URLs and use `postId` to merge the results. `maxResults: 0` raises the actor's internal cap; it cannot override the site's own visibility limits.

Some fields depend on the category or poster. A seller may omit attributes, images, a direct phone number, or a direct email. Coordinates are approximate public map points and may be intentionally displaced. The exact street address is not inferred. `contactObfuscated` only describes whether a direct number or address appears in the public body; it does not guarantee that the poster can or cannot be contacted through the site. Timestamp values are taken from the site's data, even when an update time appears earlier than a displayed posting time.

### Example requests for an automated workflow

- “Collect up to 100 public for-sale listings matching `bike` in New York, newest first, and return each posting's description, price, images, and posting ID.”
- “Use this filtered search URL for apartments, keep the URL's existing constraints, and return 50 rows with approximate neighborhood and any bedroom or floor-area attributes.”

These requests map directly to the input fields above. For repeat collection, retain the exact input and compare new dataset rows by `postId` and `scrapedAt`.

### FAQ

#### Is scraping public Craigslist listings legal?

Rules depend on the jurisdiction and intended use. Collect only public information, review the site's terms, and follow applicable privacy and data-protection law. Do not use phone numbers or email addresses for unsolicited messages, harassment, or other misuse. This actor does not access accounts or private replies.

#### Do I need to configure a proxy?

The default connection settings use the available proxy. The fetch layer starts with the cheaper connection route, rotates a session if blocked, and can escalate when required. Most users can leave `proxyConfiguration` at its default. A site block or network interruption can still reduce coverage for a particular run.

#### How long will a run take?

Time depends on result count and whether full details are requested. The verified 40-row run with details completed in about 30 seconds. Larger runs make more posting requests and will take longer. Response time can change with site load and connection conditions.

#### Can I schedule a search and monitor new listings?

Yes. Run the same saved input on a schedule, then compare `postId` values with previous datasets. `scrapedAt` marks the time each row was recorded. Keep the query narrow enough that fresh listings remain within the visible search window between scheduled runs.

#### Can I export to a spreadsheet?

Yes. Download the dataset as CSV for a spreadsheet or as JSON for a programmatic workflow. Nested `attributes` and `imageUrls` may appear as serialized values in a flat CSV export, while JSON preserves their object and array structure.

#### Why are some fields empty?

A poster may not provide a price, images, location, or category-specific attributes. Details can also be unavailable if a posting is removed after search. With `includeDetails` disabled, description, specific category labels, and reply information are intentionally left empty. Empty arrays mean no public phone numbers or addresses were found in the body.

#### Does `priceUsd` always mean US dollars?

No. It is a compatibility field. The numeric value uses the city's local currency, so read `currency` and prefer `price` for new integrations. For example, a Toronto posting can have `currency: "CAD"` while `priceUsd` still contains its local numeric price.

### Integrations

The default dataset can be retrieved through the platform API, downloaded as CSV or JSON, sent through a webhook after a run, or processed by an existing automation service. A common monitoring workflow schedules a narrow query, reads each new dataset, compares `postId` with stored IDs, and sends an alert only for listings that match a rule. Preserve the original `url` in any alert so a reviewer can inspect the current posting before acting.

### Support

Open an issue on the Issues tab; we reply within 24h and add fields on request. Include the input that produced the problem, a sample public posting URL if relevant, and the run identifier so the behavior can be reproduced.

# Actor input Schema

## `startUrls` (type: `array`):

Search, category, or individual posting URLs. When provided, these replace the guided city and category search and preserve filters in each URL.

## `region` (type: `string`):

City subdomain for the guided search; for example newyork, sfbay, toronto, or raleigh.

## `category` (type: `string`):

Category code for the guided search. Use a search URL for another category or more advanced filters.

## `searchTerm` (type: `string`):

Optional word or phrase to search in listing titles and descriptions.

## `priceMin` (type: `integer`):

Only listings at or above this local-currency price. Applies to priced categories.

## `priceMax` (type: `integer`):

Only listings at or below this local-currency price. Applies to priced categories.

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

Filter for-sale listings by seller-selected condition.

## `hasImage` (type: `boolean`):

Require at least one listing image.

## `postedToday` (type: `boolean`):

Return listings marked as posted today by the site.

## `sort` (type: `string`):

Order guided search results. URL sources use the sort already present in their URL.

## `includeDetails` (type: `boolean`):

Fetch each posting for its full description, category, attributes, and image gallery. Disable for faster search-card results.

## `maxResults` (type: `integer`):

Maximum output rows across all sources. Set 0 for an internal cap of 100,000.

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

Proxy settings. The default uses the available proxy and escalates only after blocked requests.

## Actor input object example

```json
{
  "startUrls": [
    "https://newyork.craigslist.org/search/sss?query=bike"
  ],
  "region": "newyork",
  "category": "sss",
  "searchTerm": "",
  "condition": "any",
  "hasImage": false,
  "postedToday": false,
  "sort": "date",
  "includeDetails": true,
  "maxResults": 200,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

All extracted listings in the default dataset.

# 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 = {
    "startUrls": [
        "https://newyork.craigslist.org/search/sss?query=bike"
    ],
    "region": "newyork"
};

// Run the Actor and wait for it to finish
const run = await client.actor("mlg14/craigslist-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 = {
    "startUrls": ["https://newyork.craigslist.org/search/sss?query=bike"],
    "region": "newyork",
}

# Run the Actor and wait for it to finish
run = client.actor("mlg14/craigslist-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 '{
  "startUrls": [
    "https://newyork.craigslist.org/search/sss?query=bike"
  ],
  "region": "newyork"
}' |
apify call mlg14/craigslist-scraper --silent --output-dataset

```

## MCP server setup

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