> ## Content Index
> Fetch the complete content index at: https://roundproxies.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# 4 easy ways to scrape AutoScout24 in 2026
- URL: https://roundproxies.com/blog/scrape-autoscout24/
- Published: 2026-06-07T21:14:05.000Z
- Updated: 2026-09-23T13:04:25.000Z
- Description: Scrape AutoScout24 with Python: 4 easy methods from JSON to stealth browsers. Working code, no paid API.
- Author: Marius Bernard
- Tags: Web Scraping, #dated-6951633926f439f88a95a9da

AutoScout24 is Europe's largest car marketplace, with more than 2 million live listings across 18 countries. That's a lot of pricing data sitting behind a search box.

Building an AutoScout24 scraper takes more than pointing `requests` at a results page. The listings are rendered client side, the site runs active bot detection, and a consent dialog sits between a fresh session and any search results. Fetch a search URL with a plain HTTP client and you can end up with markup that contains no cars at all.

This guide shows four ways to scrape AutoScout24 with Python, ordered from easiest to hardest. You build and run all of them yourself, with no paid scraping API involved. Each one gets you the fields people actually want off a listing: price, mileage, registration year, fuel type, transmission, power, and the seller. The same techniques carry over to the other country domains, since only the hostname changes between markets.

**Quick guidance.** Start with the lightest method that returns the data you need, and move up the list only when the site forces you to. Every step up costs more setup and more machine time per listing.

## What is AutoScout24 scraping?

Scraping AutoScout24 means pulling vehicle listing data straight from the site with code instead of copying it by hand. You get prices, mileage, year, fuel type, transmission, and seller details as structured records. The reliable way is to read the embedded JSON the page already ships, then handle Akamai's bot checks with a real browser only when you have to.

Most tutorials skip the first part. They parse HTML classes that break every release. We'll start with the durable approach instead.

## Why scrape AutoScout24?

People scrape AutoScout24 for a handful of concrete reasons, and they all come back to pricing.

Dealers watch competitor prices across markets. A BMW 3 Series can sit €2,000 cheaper in Italy than in Germany, and that gap is only visible if you're pulling both markets.

Analysts model depreciation. Feed a few thousand listings into a notebook and you can see exactly how mileage and age bend resale value for a given model.

Buyers build alerts. Rather than making you refresh a search page, a scraper pings you the moment a car matching your filters shows up under budget.

Whatever the reason, the shape of the job is the same: pull structured listings at some cadence and store them. Let's set up the tools.

## What you'll need

Before any code, get these in place:

- Python 3.9 or newer
- `requests`, `beautifulsoup4`, and `lxml` for the lightweight methods
- `playwright` and `nodriver` for the browser methods
- Basic comfort reading JSON in your browser's dev tools

Install the core packages in one line:

```bash
pip install requests beautifulsoup4 lxml

```

We'll install the browser tools in their own sections so you only grab what you use.

## How AutoScout24 is built (read this first)

AutoScout24 runs on [Next.js](https://nextjs.org/docs). That single fact decides your whole strategy.

Every page ships a `<script id="__NEXT_DATA__">` tag containing the full page payload as JSON. The prices and specs you see on screen are already in that blob before any JavaScript runs.

So you rarely need to parse HTML at all. You grab the script tag, load the JSON, and read clean fields.

There's a catch, and it's the reason plain scrapers fail. AutoScout24 sits behind **Akamai Bot Manager**, which fingerprints your TLS handshake, sets an `_abck` cookie, and throws 403s at anything that looks automated.

Next.js also hashes its CSS class names. A selector like `Price_price__APlgs` looks stable until the next deploy renames it to something else. Build a scraper on those classes and it rots within a release cycle.

The JSON payload doesn't have that problem. Its shape stays consistent far longer than any class name. That's why we lead with it.

Want to see it yourself before writing code? Open any AutoScout24 search page, hit F12, and go to the Console tab.

Paste this and press enter:

```javascript
JSON.parse(document.getElementById("__NEXT_DATA__").textContent).props.pageProps

```

You'll get an expandable tree of every listing on the page. Spend two minutes here noting the field names for your market. It'll save you an hour of guessing later.

## The 4 methods at a glance

| Method            | How you fetch    | How you parse      | Detection risk | Best for                            |
| ----------------- | ---------------- | ------------------ | -------------- | ----------------------------------- |
| 1\. Embedded JSON | requests         | \_\_NEXT\_DATA\_\_ | Medium         | Clean data, small to medium jobs    |
| 2\. HTML parsing  | requests         | CSS + data-testid  | Medium         | Quick one-offs, missing JSON fields |
| 3\. Playwright    | Headless browser | \_\_NEXT\_DATA\_\_ | Lower          | When plain requests get 403s        |
| 4\. Nodriver      | Stealth browser  | \_\_NEXT\_DATA\_\_ | Lowest         | When Akamai catches Playwright      |

Start at the top. Only move down when the site forces you to.

## Method 1: Grab the embedded JSON with requests

This is the method almost every tutorial hides behind a paid API. It's the easiest and the most durable, so it goes first.

The idea: fetch the page with a normal request, find the `__NEXT_DATA__` script, and read the listings out of JSON.

### Set up a believable session

Akamai reads your headers before anything else. A bare `requests` call with no headers gets flagged instantly.

```python
import requests

def make_session():
    session = requests.Session()
    session.headers.update({
        "User-Agent": (
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/122.0.0.0 Safari/537.36"
        ),
        "Accept-Language": "de-DE,de;q=0.9,en;q=0.8",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        # Sec-Fetch headers make the request look like a real navigation
        "Sec-Fetch-Dest": "document",
        "Sec-Fetch-Mode": "navigate",
        "Sec-Fetch-Site": "none",
    })
    return session

```

The `de-DE` language header matters. You're hitting a German-first site, so speaking German lowers suspicion.

### Pull and load the JSON

Now find the script tag and parse it. Note that we don't hardcode a deep key path, because that path shifts between releases.

```python
import json
from bs4 import BeautifulSoup

def get_next_data(html):
    soup = BeautifulSoup(html, "lxml")
    tag = soup.find("script", id="__NEXT_DATA__")
    if not tag:
        return None
    return json.loads(tag.string)

```

If `get_next_data` returns `None`, you were either blocked or the page shape changed. That's your signal to jump to Method 3.

### Find the listings inside the blob

The page data lives under `props.pageProps`. Instead of guessing the exact key, walk the structure and grab the first list of listing-shaped objects.

```python
def find_listings(node):
    """Recursively search the JSON for the listings array."""
    if isinstance(node, list):
        # A listing usually has both a price and a vehicle id
        if node and isinstance(node[0], dict) and "price" in node[0]:
            return node
        for item in node:
            found = find_listings(item)
            if found:
                return found
    elif isinstance(node, dict):
        for value in node.values():
            found = find_listings(value)
            if found:
                return found
    return None

```

This survives key renames because it looks for the *shape* of a listing, not a fixed address. Open `__NEXT_DATA__` in your dev tools console once to confirm the fields on your target market.

### Tie it together

Here's the full Method 1 run against BMW listings in Germany.

```python
def scrape_json(url):
    session = make_session()
    resp = session.get(url, timeout=20)
    if resp.status_code != 200:
        print(f"Blocked or error: HTTP {resp.status_code}")
        return []
    data = get_next_data(resp.text)
    listings = find_listings(data) if data else []
    return listings

if __name__ == "__main__":
    url = "https://www.autoscout24.com/lst/bmw?atype=C&cy=D&sort=standard"
    cars = scrape_json(url)
    print(f"Found {len(cars)} listings")

```

When this works, you get richer data than the visible page shows, including fields AutoScout24 never renders. When it 403s, you've learned Akamai is challenging your IP, and it's time for a browser.

### Clean up the records and save them

Raw listing objects carry more nesting than you want. Flatten each one to the fields you'll actually use.

```python
def clean_listing(raw):
    """Pull the useful fields out of one raw listing dict."""
    return {
        "make": raw.get("make"),
        "model": raw.get("model"),
        "price": raw.get("price"),
        "mileage": raw.get("mileage"),
        "year": raw.get("firstRegistration"),
        "fuel": raw.get("fuelType"),
        "url": raw.get("url"),
    }

```

Field names vary by market, so confirm them against your own `__NEXT_DATA__` dump. The `.get()` calls mean a renamed key returns `None` instead of crashing the run.

Then write the cleaned records to CSV:

```python
import csv

def save_csv(listings, filename="autoscout24.csv"):
    if not listings:
        return
    rows = [clean_listing(item) for item in listings]
    with open(filename, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=rows[0].keys())
        writer.writeheader()
        writer.writerows(rows)
    print(f"Saved {len(rows)} rows to {filename}")

```

For anything you re-scrape on a schedule, use the listing `url` as a unique key so repeat runs don't pile up duplicates.

## Method 2: Parse the HTML with BeautifulSoup

Sometimes you want a field that isn't in the JSON, or the JSON shape changed and you need a quick fix today. That's when you parse the rendered HTML directly.

Be honest with yourself about the tradeoff. This is the brittle path, so lean on `data-testid` attributes, which AutoScout24 keeps stable for its own tests.

```python
def parse_html_listings(html):
    soup = BeautifulSoup(html, "lxml")
    cars = []
    for article in soup.select("article[class*='cldt-summary-full-item']"):
        # data-testid survives longer than hashed class names
        price = article.select_one("[data-testid='regular-price']")
        mileage = article.select_one("[data-testid='VehicleDetails-mileage_road']")
        year = article.select_one("[data-testid='VehicleDetails-calendar']")
        cars.append({
            "price": price.get_text(strip=True) if price else None,
            "mileage": mileage.get_text(strip=True) if mileage else None,
            "year": year.get_text(strip=True) if year else None,
        })
    return cars

```

Notice the `class*=` selector. It matches a partial class name, so a hash change on the suffix won't break it outright.

Still, treat this as a stopgap. If your `data-testid` selectors start returning `None` across the board, the markup moved, and Method 1's JSON is the better long-term home.

## Method 3: Render with Playwright

When plain requests hit a wall of 403s, run a real browser. [Playwright](https://playwright.dev/python/) drives Chromium, passes Akamai's JavaScript challenge, and lets the `_abck` cookie get set the normal way.

Install it and its browser binary:

```bash
pip install playwright
playwright install chromium

```

The second command downloads the actual Chromium build Playwright controls.

### Launch a less-detectable browser

A default headless launch leaks the `navigator.webdriver` flag. Strip it, and set a realistic viewport and timezone.

```python
from playwright.sync_api import sync_playwright

def make_context(pw):
    browser = pw.chromium.launch(
        headless=True,
        args=["--disable-blink-features=AutomationControlled"],
    )
    context = browser.new_context(
        viewport={"width": 1920, "height": 1080},
        locale="de-DE",
        timezone_id="Europe/Berlin",
    )
    # Hide the automation flag before any page script reads it
    context.add_init_script(
        "Object.defineProperty(navigator, 'webdriver', {get: () => undefined});"
    )
    return browser, context

```

The `add_init_script` runs before the page's own JavaScript, so the flag is already gone when Akamai checks for it.

### Reuse the JSON extractor

Here's the payoff for leading with Method 1: once the browser renders the page, you pull the same `__NEXT_DATA__` JSON. One parser, two fetch strategies.

```python
def scrape_playwright(url):
    with sync_playwright() as pw:
        browser, context = make_context(pw)
        page = context.new_page()
        page.goto(url, wait_until="networkidle", timeout=30000)
        html = page.content()
        browser.close()
    data = get_next_data(html)
    return find_listings(data) if data else []

```

`wait_until="networkidle"` gives Akamai's challenge time to resolve before you read the HTML. If you still get an empty result, the IP itself is flagged, which proxies fix.

## Method 4: Go stealth with Nodriver

Akamai in 2026 can spot Playwright by its Chrome DevTools Protocol traffic. When that happens, [Nodriver](https://github.com/ultrafunkamsterdam/nodriver) is the next step. It drives Chrome without the CDP signals that give automation away.

```bash
pip install nodriver

```

Nodriver manages its own browser, so there's no separate install step.

```python
import asyncio
import nodriver as uc

async def scrape_nodriver(url):
    browser = await uc.start()
    page = await browser.get(url)
    await page.sleep(3)  # let the challenge clear
    html = await page.get_content()
    await browser.stop()
    data = get_next_data(html)
    return find_listings(data) if data else []

if __name__ == "__main__":
    url = "https://www.autoscout24.com/lst/audi/a4?atype=C&cy=D"
    cars = asyncio.run(scrape_nodriver(url))
    print(f"Found {len(cars)} listings")

```

The `sleep(3)` is deliberate. Akamai's challenge can take a couple of seconds to run, and reading the HTML too early gives you an interstitial page instead of listings.

Nodriver is slower than plain requests, but it's the most reliable option when detection is aggressive. Keep it in reserve rather than reaching for it first.

## Handling pagination

One page gives you 20 listings. AutoScout24 caps a single search at 20 pages, so 400 results per query.

Add a `page` parameter and loop, with a pause between requests.

```python
import random, time

def scrape_all_pages(base_url, max_pages=20):
    all_cars = []
    for page in range(1, max_pages + 1):
        sep = "&" if "?" in base_url else "?"
        url = f"{base_url}{sep}page={page}"
        cars = scrape_json(url)
        if not cars:
            break  # blocked or out of results
        all_cars.extend(cars)
        time.sleep(random.uniform(3, 7))  # look human
    return all_cars

```

To pull more than 400 records, split the search with tighter filters like price bands or registration year. Each narrowed search gives you a fresh set of 400.

## Going deeper: individual car pages

Search results give you the summary. The full picture lives on each car's own page: service history, number of owners, options list, and every technical spec.

The good news is that detail pages are also Next.js. The same `__NEXT_DATA__` extractor you already wrote works without changes.

```python
def scrape_detail(url):
    session = make_session()
    resp = session.get(url, timeout=20)
    if resp.status_code != 200:
        return None
    data = get_next_data(resp.text)
    # detail data sits under pageProps, not in a list
    return data["props"]["pageProps"] if data else None

```

Feed it a listing `url` from your search results and you get the complete record for that car.

Pace yourself here. A detail page per listing means 400 extra requests per search, so keep your delays generous and rotate IPs if you're doing this at volume.

## When you need proxies

Akamai blocks datacenter IPs fast. If you're scraping past a few hundred pages, one IP won't cut it, and cheap datacenter ranges get flagged on sight.

Residential IPs from the country you're targeting work best, since a German listing search from a German home IP looks exactly like a real buyer. [Roundproxies](https://roundproxies.com/residential-proxies-pool) rotating residential pools handle this for AutoScout24's `.de`, `.at`, and `.ch` domains.

Wiring a proxy into requests is two lines:

```python
def make_session_with_proxy(proxy_url):
    session = make_session()
    session.proxies = {"http": proxy_url, "https": proxy_url}
    return session

```

For Playwright, pass the proxy at launch instead:

```python
browser = pw.chromium.launch(
    headless=True,
    proxy={"server": "http://your-proxy:port",
           "username": "user", "password": "pass"},
)

```

Match the proxy country to the domain you're scraping. Mismatches are a quiet way to get flagged.

## Common errors and fixes

**403 Forbidden on every request.** Akamai flagged your fingerprint or IP. Move from Method 1 to Playwright or Nodriver, and add residential proxies. Our [403 Forbidden guide](https://roundproxies.com/blog/403-forbidden-error/) breaks down the causes.

**`__NEXT_DATA__` is missing.** You got an interstitial challenge page, not the listing page. Switch to a browser method and wait for the network to settle.

**All selectors return `None`.** The HTML changed and your Method 2 classes are stale. Move that logic to the JSON in Method 1.

**Empty list after a few hundred requests.** You hit a soft IP block or the 400-result cap. Rotate proxies and split the search with filters.

**HTTP 429.** Too many requests, too fast. Add longer random delays and back off exponentially before retrying.

Here's a retry wrapper that doubles the wait after each failure, so a brief block doesn't kill your whole run.

```python
import time

def fetch_with_backoff(url, session, max_retries=3):
    delay = 5
    for attempt in range(max_retries):
        resp = session.get(url, timeout=20)
        if resp.status_code == 200:
            return resp
        if resp.status_code in (429, 403):
            time.sleep(delay)
            delay *= 2  # 5s, then 10s, then 20s
        else:
            return None
    return None

```

Three retries with doubling delays clear most soft rate limits. If you're still blocked after that, the problem is the IP, not the pace, and you need proxies.

## Which method should you use?

Don't overthink it. Follow the ladder.

Use **Method 1** for anything small to medium. It's fast, the data is clean, and it survives site updates. This should be your default.

Reach for **Method 2** only when you need a field the JSON doesn't carry, or you need a fix in the next ten minutes.

Move to **Method 3** the moment plain requests start returning 403s. A real browser clears most of Akamai's checks.

Save **Method 4** for the hardest cases, where even Playwright gets caught. It's the slowest but the stealthiest.

For scaling any of them, add rotating residential proxies. Speed and staying unblocked are the same problem at volume.

## A note on responsible scraping

Scrape the public data, not the site into the ground. Check `robots.txt`, keep your request rate reasonable, and add real delays.

Prices and specs on public listings are fair game for research and monitoring. Personal seller contact details are not.

Reusing data commercially can also cross AutoScout24's terms. When in doubt about commercial use, ask a lawyer, not a blog.

## FAQ

**Is scraping AutoScout24 legal?** Collecting public listing data for personal research is generally fine. Republishing it or using it commercially may breach their terms, so check your specific case.

**Why do my requests get blocked instantly?** Datacenter IPs and bare headers trip Akamai on the first request. Use realistic headers, and residential proxies when you scale. Our [bot detection guide](https://roundproxies.com/blog/bypass-bot-detection/) covers the signals it reads.

**Do I really not need a browser?** For most jobs, no. Method 1's `__NEXT_DATA__` approach pulls everything from a single request. You only need a browser once Akamai starts challenging your IP.

**How many listings can I pull per day?** Each search caps at 400 results. With proxy rotation and sensible delays, thousands of listings a day is realistic. Push too fast and you'll get blocked.

**Which country domains work the same way?** All of them. `.de`, `.at`, `.ch`, `.it`, and the rest all run the same Next.js setup, so the JSON method carries over. Just match your proxy country to the domain.

**Can I scrape historical prices?** No. AutoScout24 only shows current listings. To build history, scrape on a schedule and store the results yourself over time.

## Wrapping up

You now have four ways to scrape AutoScout24, from a single clean request to a full stealth browser. The order is the point.

Start with the embedded JSON, and only add weight when the site pushes back.

Method 1 handles most of what people actually need. Keep Playwright and Nodriver in your pocket for the days Akamai gets serious, and add residential proxies once you scale past a few hundred pages.

If you're scraping other European marketplaces next, the same Next.js JSON trick works on plenty of them. Here's [the same approach applied to Immobilienscout24](https://roundproxies.com/blog/scrape-immobilienscout24/).