# Classic.com Vehicles Scraper (`fmchisti/classic-com-scraper`) Actor

Scrape Classic.com vehicle search results and listing pages from start URLs.

- **URL**: https://apify.com/fmchisti/classic-com-scraper.md
- **Developed by:** [Fahim Mahmud Chisti](https://apify.com/fmchisti) (community)
- **Categories:** Developer tools, Integrations, Automation
- **Features:** Limited permissions
- **Stats:** 7 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.50 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

### What does Classic.com Vehicles Scraper do?

**Classic.com Vehicles Scraper** extracts classic and collector vehicle listings from [Classic.com](https://www.classic.com) search and detail pages. Paste a filtered search URL (odometer, query, sort, page) or a direct `/veh/...` listing URL, and the Actor returns structured fields that match the other vehicle scrapers in this monorepo—title, price, mileage, VIN, seller, location, images, and more.

Run it on the [Apify](https://apify.com) platform for scheduling, API access, proxy rotation, monitoring, and dataset exports (JSON, CSV, Excel, HTML).

### Why use Classic.com Vehicles Scraper?

- Track private-seller, dealer and auction inventory with your own Classic.com filters preserved in the start URL
- Monitor lot dates and prices for comps research
- Feed marketplaces, CRM tools, or valuation pipelines with consistent vehicle fields
- Paginate search results and optionally open each listing for full specs

### How to use Classic.com Vehicles Scraper

1. Open the Actor in Apify Console and go to the **Input** tab.
2. Paste one or more Classic.com **Start URLs** (search or vehicle detail).
3. Set **Max items** and **Max pages per search** (defaults are low for quick QA runs).
4. Optionally enable **Scrape item details** for VIN, engine, colors, and description.
5. Keep US residential proxies enabled; Classic.com fronts every page with a Cloudflare challenge.
6. Click **Start** and download results from the **Output** / dataset tab.

### Input

| Field                     | Description                                                                    |
| ------------------------- | ------------------------------------------------------------------------------ |
| `startUrls`               | Required. Classic.com `/search/` or `/veh/` URLs. Filters in the URL are kept. |
| `maxItems`                | Cap on saved listings (`0` = unlimited). Default `5`.                          |
| `maxPagesPerSearch`       | Pages to crawl per search URL. Default `1`.                                    |
| `maxListingAgeDays`       | Optional age filter using the listing lot/listed date.                         |
| `scrapeItemDetails`       | Open each vehicle page for richer fields. Default `true`.                      |
| `duplicateCheck`          | Skip detail scraping for listing URLs already known (default `false`).         |
| `duplicateCheckApiUrl`    | Optional POST exists API; leave empty for Apify storage only.                  |
| `duplicateCheckStoreName` | Named KV store (default `vehicle-listing-urls`).                               |
| `proxyConfiguration`      | Prefer Apify residential US proxies.                                           |

Example input:

```json
{
    "startUrls": [
        {
            "url": "https://www.classic.com/search/?filters%5Bodometer%5D%5Bend%5D=75000&filters%5Bodometer%5D%5Bstart%5D=0&page=1&q=Private+seller&sort=lotdate"
        }
    ],
    "maxItems": 5,
    "maxPagesPerSearch": 1,
    "scrapeItemDetails": false
}
```

### Skip existing listings (duplicate check)

Classic.com detail scraping opens each vehicle in a browser with residential proxies. On scheduled re-runs, most results are often listings you already stored. Duplicate check skips those URLs so you do not pay again for detail pages you already have.

Enable it with `duplicateCheck: true` (default `false`).

If N consecutive listing URLs in a search are known duplicates (N = `duplicateCheckLeadingStop`, default **20**), that search stops (no further pages) to avoid paying for known inventory.

**Modes**

- When **`duplicateCheckApiUrl`** is set, the Actor POSTs batches of up to 500 listing URLs to your endpoint, then skips URLs returned in `existing`. It also reads and updates the named Apify Key-Value store. A URL is skipped if **either** your API or the store marks it as known.
- When **`duplicateCheckApiUrl`** is empty, the Actor uses the **Apify Key-Value store only** (no external API).

**API contract**

```json
// Request
{ "listingUrls": ["https://example.com/listing/1", "https://example.com/listing/2"] }

// Response
{
  "existing": ["https://example.com/listing/1"],
  "missing": ["https://example.com/listing/2"]
}
```

`listingUrls` may also be a single string. URLs are normalized (query string and trailing slash ignored). No auth header is required for public endpoints. If the API call fails or exceeds a **20s timeout**, the Actor fail-opens and scrapes the batch.

After a listing is saved successfully, its URL is written to the named store under the `KNOWN_LISTING_URLS` record so future runs skip it even without an API. Cached URLs expire after **30 days**, and the store is capped at **50,000** entries (oldest first). Legacy boolean `true` entries are migrated to timestamps on load.

Example:

```json
{
    "startUrls": [{ "url": "https://www.classic.com/search/?q=Private+seller&sort=lotdate" }],
    "maxItems": 100,
    "duplicateCheck": true,
    "duplicateCheckApiUrl": "https://your-api.example.com/listings/exists",
    "duplicateCheckStoreName": "vehicle-listing-urls"
}
```

### Output

Each dataset item uses the shared vehicle listing shape. You can download the dataset as JSON, HTML, CSV, or Excel.

```json
{
    "itemId": "1996-jaguar-xj12-executive-long-sajmx6346tc764003-pg2ozLW",
    "title": "1996 Jaguar XJ12 Executive Long",
    "listedAt": "2026-10-31T12:00:00.000Z",
    "price": "24900",
    "currency": "USD",
    "location": "Lake Havasu City, Arizona, USA",
    "seller": "Private Seller",
    "forSaleBy": "owner",
    "year": "1996",
    "make": "Jaguar",
    "model": "XJ",
    "trim": "Executive Long",
    "mileage": "72000",
    "transmission": "Automatic",
    "engine": "6.0L V12",
    "bodyType": "Sedan",
    "exteriorColor": "Black",
    "interiorColor": "Beige/Tan",
    "vin": "SAJMX6346TC764003",
    "specifics": {
        "vehicle id": "1748655",
        "listing status": "For Sale",
        "price type": "Fixed-price",
        "seller type": "Private Seller",
        "seller url": "https://www.classic.com/s/private-seller-e45z7zn",
        "country": "US",
        "views (30 days)": "26",
        "saves": "1",
        "photo count": "15",
        "model generation": "XJ300",
        "doors": "4 Doors"
    },
    "url": "https://www.classic.com/veh/1996-jaguar-xj12-executive-long-sajmx6346tc764003-pg2ozLW/",
    "imageUrl": "https://images.classic.com/vehicles/95b8a01e-4200-4dca-884e-c63c0979d99e.jpg",
    "images": ["https://images.classic.com/vehicles/95b8a01e-4200-4dca-884e-c63c0979d99e.jpg"],
    "page": 1,
    "scrapedAt": "2026-10-31T17:00:00.000Z"
}
```

When the Actor is started from an Apify Task, each dataset item also includes `taskId` and `taskName` so you can track which Task produced it. Direct Actor runs set both to `null`.

With `scrapeItemDetails: true`, expect additional fields such as `vin`, `engine`, `bodyType`, `driveType`, exterior/interior colors, `description`, the full photo set, and the complete spec sheet in `specifics`.

### Data fields

| Field                        | Notes                                                                                                                                                                                                                                                                                       |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `itemId`                     | Classic.com vehicle slug from `/veh/{slug}/`                                                                                                                                                                                                                                                |
| `title`                      | Listing title                                                                                                                                                                                                                                                                               |
| `listedAt`                   | Lot/posted date (`Date this listing was posted` / card lot date), noon UTC                                                                                                                                                                                                                  |
| `price` / `currency`         | Asking or sold price in USD when Classic.com shows it. Gated figures ("Upgrade to view price") stay `null` and set `specifics["price visibility"]`.                                                                                                                                         |
| `mileage`                    | Normalized miles (e.g. `53k` → `53000`)                                                                                                                                                                                                                                                     |
| `year` / `make` / `model`    | Parsed from title and detail specs; `trim` and the model variant/generation come from the spec sheet                                                                                                                                                                                        |
| `seller` / `forSaleBy`       | `Private Seller` / `owner`, the dealer's name / `dealer`, or the auction house's name / `auction`                                                                                                                                                                                           |
| `location`                   | City, region, country as printed next to the flag icon; `specifics.country` holds the flag's ISO code                                                                                                                                                                                       |
| `vin`                        | From the spec sheet when detail scraping is on, otherwise from the URL slug                                                                                                                                                                                                                 |
| `images` / `imageUrl`        | This vehicle's own Classic.com CDN photos (hero rail and Media section); comps and history thumbnails are excluded                                                                                                                                                                          |
| `specifics`                  | Spec sheet plus `vehicle id`, `listing status` (For Sale / Sold / Off Market …), `price type`, `driver side`, `seller type`, `seller url`, `source url` (external auction listing), `views (30 days)` and `saves` (best effort: these counters render a beat after the page), `photo count` |
| `url` / `sourceUrl` / `page` | Provenance and pagination                                                                                                                                                                                                                                                                   |

### Tips

- Build the search on Classic.com first (filters, `q`, `sort=lotdate`), then copy the URL into `startUrls`.
- Keep `maxItems` and `maxPagesPerSearch` small while testing; raise them for production runs.
- Enable `scrapeItemDetails` when you need VIN or full specs—each listing costs an extra request.
- Enable `duplicateCheck` on scheduled re-runs to skip listings you already stored.
- Set `scrapeItemDetails: false` when search-card fields are enough.
- Prefer skipping known listings over raising memory.
- Keep browser concurrency low.
- Classic.com fronts its pages with a Cloudflare challenge. The Actor runs a headed Camoufox browser per proxy session with the fingerprint matched to that session's IP, waits up to 60 seconds for the challenge to clear (usually 10-20 seconds), and retries on a fresh IP otherwise. A session that cleared the challenge is kept for the rest of the run, so only the first page of a run takes a little longer than the rest.
- If a detail page keeps failing, the vehicle is still saved from its search-card data (fewer fields, same identity). If every page stays blocked and nothing is scraped, the run ends as FAILED with the last error in its status message instead of succeeding with an empty dataset.
- If results are empty, check the run key-value store for `DEBUG_PAGE_*` HTML snapshots.

### FAQ and support

This Actor is intended for legitimate research and inventory monitoring. Respect Classic.com terms of service and applicable laws. Prices Classic.com hides behind a paid plan are not extracted. For bugs or feature requests, use the Actor **Issues** tab on Apify, or ask for a custom solution if you need marketplace-specific enrichments.

# Actor input Schema

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

Classic.com search URLs or direct vehicle detail URLs. Filters, query, and sort in the URL are preserved. Pagination continues from the page in the URL.

## `maxItems` (type: `integer`):

Maximum vehicle listings to save across all start URLs. Set to 0 for no item limit.

## `maxPagesPerSearch` (type: `integer`):

Maximum Classic.com result pages to process for each search start URL.

## `maxListingAgeDays` (type: `integer`):

Optional. Save only listings with a known lot/listed date within this many days. Listings without a date are excluded.

## `scrapeItemDetails` (type: `boolean`):

Open each vehicle page for VIN, exact mileage, engine, colors, description, and full specs. Recommended — search cards alone leave many fields empty.

## `duplicateCheck` (type: `boolean`):

When enabled, skip detail scraping for listing URLs already known from your duplicate-check API and/or a named Apify Key-Value store. Saves proxy and compute cost on re-runs.

## `duplicateCheckApiUrl` (type: `string`):

Optional. POST endpoint that accepts { "listingUrls": string|string\[] } and returns { "existing": string\[], "missing": string\[] }. Leave empty to use Apify Key-Value store only.

## `duplicateCheckStoreName` (type: `string`):

Named Apify Key-Value store that remembers listing URLs across runs. Used whenever Skip existing listings is enabled.

## `duplicateCheckLeadingStop` (type: `integer`):

When Skip existing listings is enabled, stop that search after this many consecutive listing URLs are known duplicates (in a row). Default 20. Lower to stop sooner; raise to keep scanning longer.

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

Proxy settings. Residential US proxies are recommended when Classic.com blocks datacenter traffic.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.classic.com/search/?filters%5Bodometer%5D%5Bend%5D=75000&filters%5Bodometer%5D%5Bstart%5D=0&page=1&q=Private+seller&sort=lotdate"
    }
  ],
  "maxItems": 5,
  "maxPagesPerSearch": 1,
  "scrapeItemDetails": true,
  "duplicateCheck": false,
  "duplicateCheckApiUrl": "https://ccscraperapi.up.railway.app/api/listings/exists",
  "duplicateCheckStoreName": "vehicle-listing-urls",
  "duplicateCheckLeadingStop": 20,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `scrapeState` (type: `string`):

No description

## `debugItems` (type: `string`):

No description

## `debugPages` (type: `string`):

No description

# 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": [
        {
            "url": "https://www.classic.com/search/?filters%5Bodometer%5D%5Bend%5D=75000&filters%5Bodometer%5D%5Bstart%5D=0&page=1&q=Private+seller&sort=lotdate"
        }
    ],
    "maxItems": 5,
    "maxPagesPerSearch": 1,
    "scrapeItemDetails": true,
    "duplicateCheckApiUrl": "https://ccscraperapi.up.railway.app/api/listings/exists",
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("fmchisti/classic-com-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": [{ "url": "https://www.classic.com/search/?filters%5Bodometer%5D%5Bend%5D=75000&filters%5Bodometer%5D%5Bstart%5D=0&page=1&q=Private+seller&sort=lotdate" }],
    "maxItems": 5,
    "maxPagesPerSearch": 1,
    "scrapeItemDetails": True,
    "duplicateCheckApiUrl": "https://ccscraperapi.up.railway.app/api/listings/exists",
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("fmchisti/classic-com-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": [
    {
      "url": "https://www.classic.com/search/?filters%5Bodometer%5D%5Bend%5D=75000&filters%5Bodometer%5D%5Bstart%5D=0&page=1&q=Private+seller&sort=lotdate"
    }
  ],
  "maxItems": 5,
  "maxPagesPerSearch": 1,
  "scrapeItemDetails": true,
  "duplicateCheckApiUrl": "https://ccscraperapi.up.railway.app/api/listings/exists",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}' |
apify call fmchisti/classic-com-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,fmchisti/classic-com-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/6FMOzjV6Tw7M2MTXP/builds/8Th4MKs7KLbisXsff/openapi.json
