# PistonHeads.com \[$1💰] | Full 208k catalogue | Real pagination (`memo23/pistonheads-scraper`) Actor

💰$1 per 1,000 cars. Scrape the FULL PistonHeads catalogue — rival scrapers loop on page one because the site ignores its own page parameter. Get price, mileage, spec, seller and photos per car. Filter by make, model, price, year, mileage or postcode radius. Optional descriptions + galleries.

- **URL**: https://apify.com/memo23/pistonheads-scraper.md
- **Developed by:** [Muhamed Didovic](https://apify.com/memo23) (community)
- **Categories:** Automation, Lead generation, E-commerce
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $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/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

## PistonHeads Car Scraper

**Turn PistonHeads into a structured UK used-car dataset — the whole catalogue, not just page one.** Scrape car adverts from PistonHeads with price, year, mileage, fuel, gearbox, body style, colour, engine size and power, seller name, type, location and phone, listing date, and photos. Filter by make, model, price, year, mileage, postcode radius and more — or pull every car listed in the last 24 hours. JSON, CSV or Excel out, priced per result.

#### How it works

![How the PistonHeads Scraper works](https://raw.githubusercontent.com/muhamed-didovic/muhamed-didovic.github.io/main/assets/how-it-works-pistonheads.png)

#### ✨ Why use this scraper?

Most PistonHeads scrapers quietly return the same first page over and over. That isn't their bug — it's the site's. PistonHeads ignores `?page=` on its search URL entirely and renders no pagination links at all, because filtering and paging happen client-side after the page loads. Scrape that HTML and you loop on the first ~16 cars forever, however many pages you ask for.

- 🔁 **Pagination that actually reaches the end.** This Actor drives the site's own JSON API and walks `offset` in steps of 100 — verified working past offset 200,000 on a catalogue of roughly 208,000 cars.
- ⚡ **100 complete cars per request, zero per-car fetches.** Price, full specification, seller and images all arrive in the search response, so a 250-car pull takes about 9 seconds. Most scrapers make one request per car.
- 🔤 **Makes and models by name, not internal IDs.** Ask for `Porsche` + `911` and the Actor resolves it against PistonHeads' own lists — including all 19 generation variants (`911 Carrera [991]`, `911 GT3 [992]`, `911 [964]`, …) that you'd otherwise have to enumerate by hand.
- 🕒 **Daily new-listings monitoring.** `listedWithinHours: 24` returns only cars listed today, so a scheduled run costs a few hundred rows instead of re-scraping the catalogue.
- 🎛 **Filters that match the site's own refine panel.** Price, year, mileage, engine size, power, previous owners, fuel, gearbox, body, colour, drivetrain, doors, seats, trade vs private, postcode + radius, and free-text keywords.
- 📝 **Optional advert copy and full photo gallery.** Turn on one flag to add the seller's written description, the features list, the full trim string, and every photo instead of the four the search API returns.
- 🔓 **No login, no captcha, no browser.** The data source is a public endpoint, so runs are fast and cheap.

#### 🎯 Use cases

| Team | What they build |
|------|-----------------|
| **Dealers & traders** | Live pricing benchmarks for stock they hold or want to buy |
| **Car buyers & flippers** | Daily alerts on new listings matching a tight spec, before they're gone |
| **Market analysts** | Depreciation and price-trend datasets by make, model, generation and mileage band |
| **Classic & specialist brokers** | Watch a specific generation (e.g. `911 [993]`) across the whole market |
| **Lead generation** | Dealer directories built from `sellerName`, `sellerType`, `sellerLocation` and `sellerPhone` |
| **Price-comparison sites** | Structured inventory feeds refreshed on a schedule |
| **ML / data teams** | Training data for valuation models — spec, mileage, price and listing date per car |

#### 📥 Supported inputs

You don't need a URL at all. Set the filters and run — that's the primary path. `startUrls` is optional:

| URL pattern | Behaviour |
|---|---|
| `https://www.pistonheads.com/buy/listing/{id}` | **Single advert** — scraped directly, with the same fields as a search result |
| `https://www.pistonheads.com/buy/search?...` | **Search** — recognised query parameters are translated into API filters |

**About pasted search URLs.** This is the easiest workflow: build your search on pistonheads.com, copy the URL out of the address bar, paste it into `startUrls`. The Actor reads the whole query string — makes, models, price, year, mileage, engine size and power, fuel, gearbox, body, colour, drivetrain, doors, seats, seller type, postcode and radius, keywords, and sort order — and applies the identical filters. Because PistonHeads puts the make and model IDs straight into the URL, a pasted search reproduces the site's own result count exactly.

**Paste as many search URLs as you like.** Each one is scraped as its own search, and results are de-duplicated by advert ID across all of them, so overlapping searches never bill you twice for the same car. Note that `maxItems` is a total across every URL, so set it high enough to cover the lot.

**Anything you set on the input overrides what's read from a URL**, so you can paste a search and then, say, tighten the price range on the input.

If the Actor cannot read any filters from a URL you paste, it stops with an error instead of running. That's deliberate: an unreadable URL would otherwise fall through to scraping the entire 208,000-car catalogue rather than your selection.

**Not supported:** seller/dealer profile pages as a starting point, forum threads, and hosts outside `pistonheads.com`.

#### 🔄 How it works

1. **Resolve make and model names** into PistonHeads' internal IDs using the site's own refine-panel facets. An unmatched make stops the run rather than silently widening the search to all 208,000 cars.
2. **Scrape any direct advert URLs** supplied in `startUrls`.
3. **Page the catalogue** via `searchPage(offset: N, limit: 100)`, walking offset until `maxItems` or the end of the result set. Adverts are de-duplicated by ID, because a live catalogue shifts under you as new cars are listed mid-run.
4. **Optionally** fetch each advert's own page for the seller's description, features, trim string and full photo gallery.
5. **Push one row per car** to the dataset.

#### ⚙️ Input parameters

| Field | Type | Description |
|---|---|---|
| `startUrls` | array | Optional advert or search URLs. Leave empty to search by filters alone. |
| `maxItems` | integer | Maximum cars to return. Default `1000`. |
| `makes` / `models` | array | Names, e.g. `["Porsche"]` / `["911"]`. Models require a make. |
| `keywords` | string | Free text across advert headlines. |
| `priceMin` / `priceMax` | integer | Asking price range in GBP. |
| `yearMin` / `yearMax` | integer | Registration year range. |
| `mileageMin` / `mileageMax` | integer | Recorded mileage range. |
| `engineSizeMinCc` / `engineSizeMaxCc` | integer | Engine capacity in cc (2000 = 2.0 litres). |
| `enginePowerMinBhp` / `enginePowerMaxBhp` | integer | Engine power in bhp. |
| `maxPreviousOwners` | integer | Maximum previous keepers. |
| `fuelTypes` | array | Petrol, Diesel, Electric, Hybrid, LPG, Other. |
| `transmissions` | array | Automatic, Manual, Other. |
| `bodyTypes` | array | Convertible, Coupe, Estate, SUV, Hatchback, Saloon, MPV, Pick Up, Other. |
| `colours` | array | 19 colours, from Black to Turquoise. |
| `driveTrains` | array | Front-wheel drive, Rear-wheel drive, 4-wheel drive, Other. |
| `doors` / `seats` | array | Door and seat counts. |
| `sellerType` | string | `Trade`, `Private`, or empty for both. |
| `postcode` + `distanceMiles` | string + integer | Search around a UK postcode. Adds `distanceFromPostcodeMiles` to each row. |
| `listedWithinHours` | integer | Only adverts listed in the last N hours. `24` = today. |
| `includeSoldCars` | boolean | Include adverts marked sold. Default `false`. |
| `sortOption` | string | PistonHeads sort option, passed through. |
| `includeDescription` | boolean | Add the advert's own write-up: description, features, trim and full gallery. Seller name/type/location/phone come free without it. One extra request per car. Default `false`. |
| `maxConcurrency` | integer | Parallel description fetches. Default `10`. |

##### Example input

```json
{
  "makes": ["Porsche"],
  "models": ["911"],
  "priceMin": 40000,
  "priceMax": 90000,
  "transmissions": ["Manual"],
  "maxItems": 500
}
```

Daily new-listings monitor:

```json
{
  "listedWithinHours": 24,
  "maxItems": 2000
}
```

#### 📊 Output overview

One row per car advert. Every field is always present — missing source data comes back as `null` (or `[]`), so your columns never disappear between rows.

#### 📦 Output sample

```json
{
  "listingId": "19910033",
  "listingUrl": "https://www.pistonheads.com/buy/listing/19910033",
  "title": "PORSCHE 911 TURBO CABRIOLET MANUAL 996",
  "make": "Porsche",
  "model": "911 Turbo [996]",
  "year": 2004,
  "listedAt": "2026-02-18T08:00:00Z",
  "price": 40990,
  "currency": "GBP",
  "priceOnApplication": false,
  "priceOnAuction": false,
  "vatOmitted": false,
  "pricingModel": "Visible",
  "mileage": 65000,
  "fuelType": "Petrol",
  "transmission": "Manual",
  "bodyType": "Convertible",
  "colour": "Black",
  "engineSizeCc": 3600,
  "enginePowerBhp": 420,
  "sellerId": "617798",
  "sellerName": "Dorset Sports Cars",
  "sellerType": "Trade",
  "sellerLocation": "Wimborne, United Kingdom",
  "sellerIsOnlineRetailer": false,
  "distanceFromPostcodeMiles": null,
  "sellerPhone": "01483967169",
  "mainImage": "https://img.pistonheads.com/Fullsize/porsche/911-turbo-996/...",
  "images": ["https://img.pistonheads.com/Fullsize/..."],
  "imageCount": 4,
  "isFeatured": false,
  "brandType": "Core",
  "badges": [],
  "description": null,
  "features": null,
  "variant": null,
  "galleryImages": [],
  "galleryImageCount": 0,
  "scrapedAt": "2026-08-17T15:40:40.033Z"
}
```

#### 🗂 Key output fields

| Field | Notes |
|---|---|
| `listingId` / `listingUrl` | Stable advert ID and its public URL. Use the ID to de-duplicate across scheduled runs. |
| `make` / `model` | `model` carries the generation, e.g. `911 Turbo [996]` — useful for valuation work. |
| `variant` | Full trim string, e.g. `4.2 FSI V8 Estate 5dr S Tronic quattro (450 ps)`. Requires `includeDescription`. |
| `listedAt` | When the advert went live. Pairs with `listedWithinHours` for monitoring. |
| `price` | Integer GBP. Check `priceOnApplication` / `priceOnAuction` before treating it as an asking price. |
| `engineSizeCc` / `enginePowerBhp` | Present on roughly 8 in 10 cars — older classics often have neither on PistonHeads. |
| `sellerType` | `Trade` or `Private`. The catalogue skews heavily to trade. |
| `sellerPhone` | Seller's phone number. Dealers publish a full number; PistonHeads masks private-seller numbers (e.g. `07984XXXXXX`), so treat those as partial. |
| `distanceFromPostcodeMiles` | Only populated when you set a `postcode`. |
| `images` / `imageCount` | **Capped at 4 by the search API.** For the full set use `galleryImages`. |
| `galleryImages` | Every photo (commonly 25–45). Requires `includeDescription`. |
| `features` | Structured features list, typically 100+ entries on dealer adverts. **Often `null` on private-seller adverts**, which usually don't include one — that's the source data, not a parse failure. |

#### ❓ FAQ

**Why do other PistonHeads scrapers stop after one page?**
Because they scrape the search HTML and loop `?page=2`, `?page=3`. PistonHeads ignores that parameter — pages 2, 50 and 500 all return the same first adverts. This Actor uses the site's JSON API with real offset pagination instead.

**Can I get all 208,000 cars?**
Yes. Set `maxItems` accordingly and leave the filters empty. Be deliberate about it — you're charged per result.

**Does `models` need the exact PistonHeads name?**
No. `911` matches all 19 generation variants. An exact name matches just that one. The run log always lists what your input resolved to.

**Why is `features` empty on some cars?**
Private-seller adverts usually don't include a features list. Dealer adverts almost always do.

**Do I need a proxy?**
No. The default residential proxy is used and works, but PistonHeads isn't aggressive about blocking.

**How do I run this daily for new listings only?**
Set `listedWithinHours: 24` and schedule the Actor once a day. De-duplicate on `listingId` if you're appending to your own store.

**Are sold cars included?**
Not by default. Set `includeSoldCars: true` if you want them.

#### 💬 Support

- Issues tab on the Actor page
- Email: <muhamed.didovic@gmail.com>

#### 🛠 Additional services

- Custom output shape, additional fields, or one-off datasets: <muhamed.didovic@gmail.com>
- Need a similar scraper for other car marketplaces (AutoTrader UK, AutoScout24, Motors, CarGurus, etc.)? Drop an email.
- For API access (no Apify fee, just a usage fee for the API): <muhamed.didovic@gmail.com>

#### 🔎 Explore more scrapers

If this PistonHeads Scraper was useful, see other scrapers and actors at [memo23's Apify profile](https://apify.com/memo23) — covering car marketplaces, job boards, real estate, social media, and more.

***

### 🤖 For AI Agents & LLM Apps

This Actor is a clean data source for agent pipelines and RAG over the UK used-car market.

- **Deterministic input.** All filters are plain JSON — no URL construction needed. Make and model take human names, so an LLM can pass `"Porsche"` / `"911"` straight through from a user's question.
- **Stable schema.** Every field is present on every row (`null` when absent), so downstream parsing never needs defensive key checks.
- **Bounded cost.** `maxItems` caps the result count exactly; `listedWithinHours` keeps incremental runs small.
- **Grounding-friendly.** `listingUrl` gives every row a citable public source, and `listedAt` + `scrapedAt` let an agent reason about freshness.

Typical agent tasks: "find manual 911s under £90k within 50 miles of Bristol", "track this week's new EV listings under £25k", "build a price-vs-mileage curve for the 997 generation".

***

### ⚠️ Disclaimer

This Actor is an independent tool and is not affiliated with, endorsed by, or sponsored by PistonHeads, CarGurus, or any of their subsidiaries or affiliates. All trademarks mentioned are the property of their respective owners.

The scraper accesses only publicly available car adverts and the public search endpoint on pistonheads.com — no authenticated endpoints, dealer-only features, or content behind a PistonHeads login. Users are responsible for ensuring their use complies with PistonHeads' Terms of Service, applicable data-protection law (GDPR, CCPA, etc.), and any contractual obligations of their own organisation.

***

### SEO Keywords

pistonheads scraper, scrape pistonheads, pistonheads api, pistonheads.com scraper, Apify pistonheads, uk car listings scraper, used car scraper uk, car classifieds scraper, uk car marketplace data, car price data uk, used car price api, car dealer scraper uk, vehicle listings scraper, car market analysis uk, porsche listings scraper, classic car scraper uk, autotrader alternative scraper, motors.co.uk alternative scraper, cargurus alternative scraper, car depreciation data, uk vehicle pricing dataset, car inventory feed uk, second hand car data uk

# Actor input Schema

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

Optional. Paste PistonHeads URLs to scrape. A direct advert URL (https://www.pistonheads.com/buy/listing/20912256) is scraped on its own. A search URL has its recognised filters read off the query string. Leave this empty to search the whole catalogue using the filters below.

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

Maximum number of car adverts to return. PistonHeads lists roughly 208,000 used cars, so set this deliberately — without filters the Actor will keep paginating until it hits this number.

## `makes` (type: `array`):

Car makes by name, e.g. Porsche, Audi. Names are resolved against PistonHeads' own make list, so you don't need internal IDs. A make that doesn't match is reported in the log and the run stops rather than silently scraping everything.

## `models` (type: `array`):

Car models by name, e.g. 911, Cayman. Requires at least one make, because PistonHeads scopes model IDs to their make.

## `keywords` (type: `string`):

Free-text search across advert headlines, e.g. "manual" or "GT3 Touring".

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

Lowest asking price to include.

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

Highest asking price to include.

## `yearMin` (type: `integer`):

Earliest registration year to include.

## `yearMax` (type: `integer`):

Latest registration year to include.

## `mileageMin` (type: `integer`):

Lowest recorded mileage to include.

## `mileageMax` (type: `integer`):

Highest recorded mileage to include — useful for finding low-mileage examples.

## `fuelTypes` (type: `array`):

Keep only cars using these fuel types. Empty = any.

## `transmissions` (type: `array`):

Keep only cars with these gearboxes. Empty = any.

## `bodyTypes` (type: `array`):

Keep only these body styles. Empty = any.

## `colours` (type: `array`):

Keep only cars in these colours. Empty = any.

## `driveTrains` (type: `array`):

Keep only cars with these drivetrains. Empty = any.

## `doors` (type: `array`):

Keep only cars with these door counts. Empty = any.

## `seats` (type: `array`):

Keep only cars with these seat counts. Empty = any.

## `engineSizeMinCc` (type: `integer`):

Smallest engine capacity in cc, e.g. 2000 for 2.0 litres.

## `engineSizeMaxCc` (type: `integer`):

Largest engine capacity in cc.

## `enginePowerMinBhp` (type: `integer`):

Minimum engine power in bhp.

## `enginePowerMaxBhp` (type: `integer`):

Maximum engine power in bhp.

## `maxPreviousOwners` (type: `integer`):

Keep only cars with at most this many previous keepers.

## `sellerType` (type: `string`):

Restrict to dealers or private sellers. Empty = both.

## `postcode` (type: `string`):

UK postcode to search around, e.g. SW1A 1AA. Combine with Search radius. Adds each seller's distance from this postcode to the output.

## `distanceMiles` (type: `integer`):

How far from the postcode to search. Ignored unless a postcode is set.

## `listedWithinHours` (type: `integer`):

Only adverts published in the last N hours — set 24 for cars listed today. This is the cheapest way to run the Actor as a daily new-listings monitor instead of re-scraping the whole catalogue.

## `includeSoldCars` (type: `boolean`):

Also return adverts already marked as sold. Off by default.

## `includeDescription` (type: `boolean`):

Fetches each advert's own page for the ADVERT's write-up — the text the seller wrote about the CAR, not about themselves. REQUIRED for the description, features, variant and galleryImages columns; leave it off and those four are null on every row. Seller name, type, location and phone are always included and do NOT need this. Costs one extra request per car and roughly doubles run time.

## `maxConcurrency` (type: `integer`):

How many description fetches to run in parallel. Only matters when Include seller description is on.

## `proxy` (type: `object`):

Proxy settings. The built-in residential proxy is used by default and works well; override only if you have a reason to.

## Actor input object example

```json
{
  "startUrls": [],
  "maxItems": 100,
  "makes": [],
  "models": [],
  "fuelTypes": [],
  "transmissions": [],
  "bodyTypes": [],
  "colours": [],
  "driveTrains": [],
  "doors": [],
  "seats": [],
  "sellerType": "",
  "includeSoldCars": false,
  "includeDescription": false,
  "maxConcurrency": 10
}
```

# Actor output Schema

## `dataset` (type: `string`):

Every scraped car advert.

# 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": [],
    "maxItems": 100,
    "makes": [],
    "models": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("memo23/pistonheads-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": [],
    "maxItems": 100,
    "makes": [],
    "models": [],
}

# Run the Actor and wait for it to finish
run = client.actor("memo23/pistonheads-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": [],
  "maxItems": 100,
  "makes": [],
  "models": []
}' |
apify call memo23/pistonheads-scraper --silent --output-dataset

```

## MCP server setup

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