# Mobile.de Scraper — German Car Listings, Prices & Dealer Data (`studio-amba/mobile-de-scraper`) Actor

Scrape car listings from Mobile.de, Germany's #1 car marketplace with 46M+ monthly visits. Search by keyword, make, model, price range, and more. Extract prices, mileage, specs, dealer info, and photos. No login or cookies required.

- **URL**: https://apify.com/studio-amba/mobile-de-scraper.md
- **Developed by:** [Studio Amba](https://apify.com/studio-amba) (community)
- **Categories:** Other
- **Features:** Limited permissions
- **Stats:** 21 total users, 1 monthly users, 96.7% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 result scrapeds

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

## Mobile.de Scraper

Scrape **car listings from [Mobile.de](https://www.mobile.de)**, Germany's largest online car marketplace with over 46 million monthly visits. Extract prices, specifications, mileage, dealer information, and photos from millions of car listings. No login or cookies required.

### What does Mobile.de Scraper do?

This Actor extracts structured car listing data from Mobile.de search results. Search by keyword, filter by price, year, and mileage, and get clean, structured JSON output with all the details you need for market research, price comparison, or inventory monitoring.

Mobile.de is part of the Adevinta group and hosts listings from both private sellers and professional dealers across Germany and Europe. With this scraper, you can programmatically access this data via the Apify platform with built-in proxy rotation, scheduling, and API access.

### Why use Mobile.de Scraper?

- **Market research** — Track car prices across makes, models, and regions in Germany's largest car market
- **Dealer intelligence** — Monitor competitor inventory and pricing strategies
- **Price comparison** — Find the best deals by comparing thousands of listings automatically
- **Inventory monitoring** — Set up scheduled runs to track new listings matching your criteria
- **Data analysis** — Export to CSV, JSON, or Excel for analysis in your tools of choice

### How to scrape Mobile.de data

1. Go to the **Input** tab
2. Enter a search query (e.g., "bmw 3er") or provide direct Mobile.de URLs
3. Optionally set price, year, and mileage filters
4. Set the maximum number of results you want
5. Click **Start** and wait for the run to finish
6. Download your data from the **Output** tab in JSON, CSV, or Excel format

### Input

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `query` | String | No | Make and model (e.g., "bmw", "audi a4", "vw golf", "mercedes c-klasse"). A make or model mobile.de lists is searched with its own make/model filter; other words search the listing description |
| `make` | String | No | Make as mobile.de writes it (e.g. "BMW", "Mercedes-Benz"). Uses mobile.de's make filter |
| `model` | String | No | Model or model group, used with `make` (e.g. "X5", "C-Klasse") |
| `startUrls` | Array | No | Direct Mobile.de search or detail URLs |
| `minPrice` | Integer | No | Minimum price filter in EUR |
| `maxPrice` | Integer | No | Maximum price filter in EUR |
| `minYear` | Integer | No | Minimum first registration year |
| `maxYear` | Integer | No | Maximum first registration year |
| `mileageMin` | Integer | No | Minimum mileage in km |
| `maxMileage` | Integer | No | Maximum mileage in km |
| `sellerType` | String | No | `any` (default), `dealer`, `private` or `company`. Uses mobile.de's own seller filter |
| `maxResults` | Integer | No | Maximum listings to scrape (default: 100) |
| `fetchDetails` | Boolean | No | Off by default. Opens every ad page and adds the ad-page fields below |
| `trackListings` | Boolean | No | Off by default. Remembers listings between runs and adds removals and market days supply (see below) |
| `trackingKey` | String | No | Name of the tracked segment. Empty = derived from the filters |
| `salesWindowDays` | Integer | No | Days counted as potential sales for market days supply (default: 45) |
| `priceBands` | Array | No | Band edges in EUR for the price breakdown in the segment summary |
| `proxyConfiguration` | Object | No | Not used. Kept so older inputs keep working |

### Output

Each car listing contains:

| Field | Type | Example |
|-------|------|---------|
| `make` | String | `"BMW"` |
| `model` | String | `"3er"` |
| `variant` | String | `"320d Touring M Sport"` |
| `title` | String | `"BMW 320d Touring M Sport"` |
| `price` | Number | `32900` |
| `currency` | String | `"EUR"` |
| `year` | Number | `2021` |
| `mileage` | Number | `45000` |
| `fuelType` | String | `"Diesel"` |
| `transmission` | String | `"Automatik"` |
| `power` | String | `"190 PS"` |
| `sellerType` | String | `"Dealer"` |
| `sellerName` | String | `"Autohaus Schmidt GmbH"` |
| `location` | String | `"Munich, Bavaria"` |
| `imageUrl` | String | Main listing image URL |
| `imageUrls` | Array | All listing image URLs |
| `url` | String | Direct link to the listing |
| `listingId` | String | `"447812345"` (the id in the ad URL) |
| `scrapedAt` | String | `"2026-04-06T12:00:00.000Z"` |

#### Ad page fields (fetchDetails)

With `fetchDetails` on, every row also has: `firstRegistration`, `sellerId`, `dealerName`, `dealerRating`, `dealerRatingCount`, `sellerAddress`, `vin`, `hsn`, `tsn`, `fuelTypeDetail`, `previousOwners`, `exteriorColor`, `damageCondition`, `category`, `priceNet`, `vatRate`, `equipment`, `description`, `imageCount`, `adCreatedAt`, `adModifiedAt`, `adRenewedAt` and `detailsError`. `transmission`, `sellerType` (Dealer, Private or Company) and the full photo list come from the ad page too. Without the flag these fields are not in the output.

- `vin` is only filled when the seller wrote the VIN in the description. Mobile.de has no VIN field.
- `detailsError` says why an ad page could not be read (for example the ad was removed during the run). The row still has the search card data.
- It opens one extra page per listing, so a run takes longer.

### Example output

```json
{
    "make": "BMW",
    "model": "3er",
    "variant": "320d Touring M Sport",
    "title": "BMW 320d Touring M Sport",
    "price": 32900,
    "currency": "EUR",
    "year": 2021,
    "mileage": 45000,
    "fuelType": "Diesel",
    "transmission": "Automatik",
    "power": "190 PS",
    "sellerType": "Dealer",
    "sellerName": "Autohaus Schmidt GmbH",
    "location": "Munich, Bavaria",
    "imageUrl": "https://img.classistatic.de/api/v1/mo-prod/images/...",
    "imageUrls": ["https://img.classistatic.de/api/v1/mo-prod/images/..."],
    "url": "https://suchen.mobile.de/fahrzeuge/details.html?id=123456789",
    "listingId": "123456789",
    "scrapedAt": "2026-04-06T12:00:00.000Z"
}
```

You can download the dataset in various formats such as JSON, HTML, CSV, or Excel.

### Track listings and market days supply

Turn on `trackListings` and run the same search every day. The actor reads every result page of the search and remembers each listing in a key-value store called `mobile-de-tracking` in your own Apify account, under one key per search (`trackingKey`).

Every run then outputs:

- One row per active listing with `status` (`new` or `active`), `firstSeenAt`, `lastSeenAt`, `previousPrice`, `priceChanged`, `listedAt` and `daysListed`.
- One row with `status: "removed"` for each listing that disappeared since the last run. `removalClass` is `reposted` when a new ad for the same car showed up in the same run (same VIN, or same seller, make, model, year, fuel, gearbox and seller type with mileage within 2% or 1500 km), otherwise `potential_sold`.
- One row with `status: "active_outside_filter"` for a listing that is still online but no longer matches your filters, for example after a price change. It is not counted as a removal.
- One row with `recordType: "__SEGMENT_SUMMARY__"` with the segment numbers: active listings, new listings, price changes, potential sales and reposts in the sales window, average daily sales, `mdsDays`, median, 25th and 75th percentile (`p25Price`, `p75Price`), average, min and max price, median days listed, and a count per price band.

Market days supply is calculated as:

```
mdsDays = activeListings / (potentialSalesInWindow / effectiveWindowDays)
```

`effectiveWindowDays` is `salesWindowDays` (45 by default), or the number of days tracked so far when that is shorter. The summary says so in `notes` until the window is full. `mdsDays` is null when there were no potential sales in the window.

`listedAt` is the ad's creation time. With `fetchDetails` on it comes from the ad page. Without it, it is read from the ad id for the 14-digit ids mobile.de gives new ads; older 9-digit ids carry no date, so `listedAt` is null for those.

#### Limitations

- **Mobile.de does not say why an ad disappeared.** A removed ad and an id that never existed return the same "page not found". So a removal is a potential sale, never a confirmed one. Every removal row carries `removalConfidence: "site does not say why an ad disappeared"`.
- **A listing is only marked removed when the run read every result page.** The actor compares what it read with the count mobile.de shows. If `maxResults` stopped the run early, or a page could not be read, nothing is marked removed and the summary explains why in `incompleteReason`. Set `maxResults` above the size of your search.
- **Mobile.de shows at most 2,000 listings per search** (100 pages). With `trackListings` on, a bigger search is split into price ranges automatically and every range is read in full. Listings without a price are not in any price range. Set `maxResults` above the number of listings in the search.
- **Missing listings are checked one by one.** Before a listing counts as removed, the actor opens its ad page twice. Listings move between result pages while a run reads them, so this stops a listing that was only skipped from counting as a sale.
- **The first run is the baseline.** Removals and sales are counted from the second run on.
- **Reposts are matched within one run.** A car that comes back days later under a new id counts as a new listing.
- **Run one schedule per search.** Two runs on the same `trackingKey` at the same time overwrite each other's state.

#### How to schedule it daily on Apify

1. Fill in your search and turn on `trackListings`. Set `maxResults` above the number of listings mobile.de shows for that search.
2. Save it as a task in Apify Console.
3. Open **Schedules**, create a schedule with a daily time (for example 06:00), and add the task.
4. Read the `__SEGMENT_SUMMARY__` row of each run, or the full dataset for listing-level history.

To track several segments (for example price bands or model years), make one task per segment. Each gets its own `trackingKey`.

### How much does it cost to scrape Mobile.de?

This actor loads Mobile.de through Bright Data (Web Unlocker, with the Scraping Browser as a second route) to handle Mobile.de's Akamai bot protection. Each search page gives 20 listings. It charges $0.02 per run (start fee, based on its 4 GB memory allocation) plus $0.002 per result scraped. 100 results costs about $0.22; 1,000 results about $2.02. With `trackListings` on, every active, removed and summary row is a result, so a daily run on a search of 500 listings costs about $1.02. A run's usage cost only settles after it reports SUCCEEDED.

### Tips for best results

- **Start small** — Test with `maxResults: 10` before running large scrapes
- **Use filters** — Price, year, and mileage filters reduce the number of pages to scrape
- **Schedule runs** — Set up daily or weekly runs to track price changes over time
- **Direct URLs** — If you know the exact search URL from Mobile.de, paste it in `startUrls` for precise control

### Limitations

- Mobile.de uses Akamai bot protection. The actor handles it through Bright Data, so no proxy setup is needed, but a page can take 10-60 seconds to load
- A normal search reads at most 50 result pages (1,000 listings)
- The full description, equipment and dealer details are on the ad pages and need `fetchDetails`
- Data is scraped from the public website and may change without notice

### FAQ and support

**Is it legal to scrape Mobile.de?** This Actor extracts publicly available data from Mobile.de. Always respect the website's terms of service and applicable laws in your jurisdiction.

**The scraper returns no results.** When Mobile.de has no cars for your search, the run ends SUCCEEDED with 0 results and a status message saying so. When Mobile.de could not be loaded before the run timeout, the run fails with a message saying that; retry the run.

**Can I scrape specific car makes or models?** Yes. Type the make and model in `query` ("audi a4" returns only Audi A4 listings; "mercedes c-klasse" every C-Klasse model). The run log names the make/model filter it used. Words that are not a make or model mobile.de lists are searched in the listing description. You can also paste a filtered Mobile.de search URL in `startUrls`.

For bug reports or feature requests, please use the **Issues** tab. For custom scraping solutions, feel free to reach out.

# Actor input Schema

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

Search term (e.g. 'bmw', 'audi a4', 'mercedes c-klasse'). Leave empty to browse all listings.

## `make` (type: `string`):

Optional. Car make as mobile.de writes it (e.g. BMW, Audi, Mercedes-Benz, VW). Uses mobile.de's own make filter. A make mobile.de does not list ends the run with a message and no results. When empty, the make is read from the start of the search query.

## `model` (type: `string`):

Optional, used together with Make. Model or model group as mobile.de writes it (e.g. X5, A4, C-Klasse, 3er).

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

Direct Mobile.de search or detail URLs. Overrides the query field if provided.

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

Minimum price filter in euros.

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

Maximum price filter in euros.

## `minYear` (type: `integer`):

Minimum first registration year (e.g. 2018).

## `maxYear` (type: `integer`):

Maximum first registration year (e.g. 2024).

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

Minimum mileage filter in kilometers.

## `maxMileage` (type: `integer`):

Maximum mileage filter in kilometers.

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

Uses mobile.de's own seller filter (Anbieter).

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

Maximum number of car listings to scrape. Each search page has 20 results. A normal search reads at most 50 pages (1,000 listings); with trackListings on, the whole search is read, and a search over mobile.de's 2,000-listing limit is split into price ranges automatically.

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

Off by default. When on, the actor opens every ad page and adds firstRegistration, sellerId, dealerName, dealerRating, dealerRatingCount, sellerAddress, vin, hsn, tsn, fuelTypeDetail, previousOwners, exteriorColor, damageCondition, category, priceNet, vatRate, equipment, description, imageCount, adCreatedAt, adModifiedAt, adRenewedAt and detailsError. Without it those fields are not in the output at all. One extra page per listing, so runs take longer. VIN is only there when the seller wrote it in the description.

## `trackListings` (type: `boolean`):

Off by default. When on, the actor reads every result page of this search, remembers each listing in a key-value store in your account ("mobile-de-tracking") and adds firstSeenAt, lastSeenAt, status (new, active), price changes, one row per listing that disappeared since the last run (status removed, with removalClass potential_sold or reposted), and one **SEGMENT_SUMMARY** row with market days supply. Mobile.de does not say why an ad disappeared, so a removal is a potential sale, never a confirmed one. Set maxResults above the number of listings in the search: listings are only marked removed when the run read every result page. A search over mobile.de's 2,000-listing limit is split into price ranges automatically. Schedule the same input daily.

## `trackingKey` (type: `string`):

Name of the tracked segment in the store. Leave empty to derive it from the search filters, so the same filters always continue the same history. Set your own for a readable name (letters, digits, - \_ . only). A new key starts a new history.

## `salesWindowDays` (type: `integer`):

Number of past days counted as potential sales for the market days supply: MDS = active listings / (potential sales in the window / window days). Only used with trackListings.

## `priceBands` (type: `array`):

Band edges for the price breakdown in the segment summary, in EUR. Default \[10000, 20000, 30000, 40000, 50000, 75000, 100000] gives 0-9999, 10000-19999, ... and 100000+. Only used with trackListings.

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

Kept for backwards compatibility with older input payloads. Mobile.de uses Akamai bot protection that requires Bright Data Web Unlocker — this scraper fetches pages through Bright Data instead of Apify proxies, so this field has no effect.

## `brightDataApiKey` (type: `string`):

Optional: your own Bright Data API key for the Web Unlocker zone. Leave empty to use the built-in unlocking service.

## Actor input object example

```json
{
  "query": "bmw",
  "sellerType": "any",
  "maxResults": 5,
  "fetchDetails": false,
  "trackListings": false,
  "salesWindowDays": 45,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `results` (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 = {
    "query": "bmw",
    "maxResults": 5,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("studio-amba/mobile-de-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 = {
    "query": "bmw",
    "maxResults": 5,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("studio-amba/mobile-de-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 '{
  "query": "bmw",
  "maxResults": 5,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call studio-amba/mobile-de-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,studio-amba/mobile-de-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/d76qzD883KJKC4I7r/builds/zgXJVfHyM4mzJrDCi/openapi.json
