DEV Community

Devil Scrapes
Devil Scrapes

Posted on

How to Scrape Cars.bg Car Listings (Bulgaria) to JSON/CSV

Quick answer: Cars.bg publishes no public API, so structured used-car data requires scraping the public listings. The Cars.bg Bulgaria Car Scraper on Apify handles browser fingerprint rotation, residential proxy rotation, and retries for you — returning clean typed rows with dual EUR and BGN pricing at $2.05 per 1,000 results ($0.05 actor-start + $0.002 per result).


Bulgaria's used-car market has a quirk that sets it apart from most of Western Europe: prices appear in both euros and Bulgarian lev (BGN) on every single listing. Bulgaria is an EU member but retains the lev — pegged at a fixed rate of 1.9558 BGN per EUR — and the marketplace convention is to show both. If you are building a pricing model or running arbitrage across EU markets, you need both currency fields in your dataset.

Cars.bg is Bulgaria's largest automotive marketplace with no public API and no structured data export. Scraping it is not trivial: the site uses a modern JavaScript frontend with an internal API, and its detail pages are guarded more aggressively than the listing cards — requiring residential proxy rotation and careful session management to retrieve reliably.

This guide covers how to pull Cars.bg data programmatically using the Cars.bg Bulgaria Car Scraper, a managed Apify Actor that handles the infrastructure.


Does Cars.bg Have an API? 🔎

No. Cars.bg offers no developer API, no data export, and no documented structured feed. The site's listing data is rendered via an internal API that the frontend calls with session-scoped parameters — not something you can call stably from external code.

One important distinction from other scrapers in this fleet: Cars.bg's detail pages are guarded more aggressively than its listing cards. With the default enrichment-off setting, you can pull make, model, year, price (EUR + BGN), mileage, fuel type, location, seller type, and photos from listing cards alone — which is the fastest and most reliable path. Detail-page enrichment (gearbox, body type, engine spec, colour, full description) is available but requires a larger proxy pool to avoid rate-limiting.


What Data You Can Extract

The Actor returns one row per listing. Fields from the listing card are always present (enrichment off is the default). Fields marked "enrichment-only" require enrichDetails: true.

Field Description
listing_id Cars.bg listing reference ID
listing_url Absolute URL to the listing detail page
title Make + model from listing title
make Manufacturer (e.g. Mazda)
model Model (e.g. 6)
year Manufacture year
price Integer price in EUR
currency Always EUR
price_bgn Price in Bulgarian lev (BGN) as shown on the listing
mileage_km Odometer in kilometres
fuel_type Дизел / Бензин / Електрически (Bulgarian Cyrillic)
transmission Автоматични / Ръчни скорости — enrichment-only
engine_power_hp Horsepower (к.с.) — enrichment-only
engine_size_cc Displacement in cc — enrichment-only
body_type Комби / Седан / Джип, etc. — enrichment-only
color Exterior colour — enrichment-only
first_registration First-registration month/year, e.g. "Март 2009" — enrichment-only
location Seller city
seller_type dealer or private
seller_name Seller display name — enrichment-only
photo_urls List of photo URLs
description Free-text seller description — enrichment-only
posted_date Relative posting time from listing card (e.g. "днес, 01:44")
scraped_at ISO-8601 UTC timestamp of scrape

A note on language: Cars.bg is a Bulgarian-language site. Fuel type, gearbox, and body type values are in Bulgarian Cyrillic. The Actor maps all field names to English column names — your schema is consistent — but the values are preserved as-is. Дизел stays Дизел. This is correct behaviour: silent translation introduces errors for uncommon values.


A Realistic Output Row

Here is a sample result using the exact field names from the Actor's Pydantic ResultRow model, with enrichment enabled:

{
  "listing_id": "68eb6bba3c8c0e169c027093",
  "listing_url": "https://www.cars.bg/offer/68eb6bba3c8c0e169c027093",
  "title": "Mazda 6 2,2MZR-CD",
  "make": "Mazda",
  "model": "6 2,2MZR-CD",
  "year": 2009,
  "price": 2850,
  "currency": "EUR",
  "price_bgn": 5574,
  "mileage_km": 308000,
  "fuel_type": "Дизел",
  "transmission": "Ръчни скорости",
  "engine_power_hp": 185,
  "engine_size_cc": 2200,
  "body_type": "Комби",
  "color": "Светло сив металик",
  "first_registration": "Март 2009",
  "location": "Монтана",
  "region": null,
  "seller_type": "private",
  "seller_name": "Aleksandr",
  "photo_urls": ["https://g1-bg.cars.bg/2025-10-12_1/68eb68ec14f9749b22054f64b.jpg"],
  "description": "Продавам Mazda 6 комби (GH), 2009 г. ...",
  "posted_date": "днес, 01:44",
  "scraped_at": "2026-06-02T10:00:00+00:00"
}
Enter fullscreen mode Exit fullscreen mode

Notice both price (2,850 EUR) and price_bgn (5,574 BGN) — both come directly from the listing card, so they are available even without detail enrichment.


How We Handle the Blocks

Cars.bg is a modern JavaScript marketplace. Its listing cards and detail pages behave differently under automated traffic — the site's defences are more aggressive on detail pages than on search results. Here is what the Actor does:

  • Browser fingerprint rotation. We use curl-cffi to impersonate real Chrome, Firefox, and Safari TLS handshakes. The server sees a browser, not Python.
  • Residential proxy rotation via Apify Proxy. We rotate residential exit IPs and fresh session IDs on every block signal. Datacenter IPs are a reliable path to a 403 on Cars.bg detail pages.
  • Exponential backoff with Retry-After. We retry on 408, 429, and 5xx — up to five attempts per page, honouring the server's own Retry-After header.
  • Rate-limit-aware pacing. When the target slows us, we match its pace rather than pushing through.
  • Loud failure, never silent. A hard block produces a non-zero exit with a descriptive status message. Empty datasets with green statuses are not acceptable.

The default configuration has enrichDetails: false — not because the detail pages are off-limits, but because detail enrichment requires a larger proxy pool to run reliably, and we want the default run to succeed predictably.


Running It with Python 🐍

from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")

run = client.actor("DevilScrapes/carsbg-bulgaria-cars").call(
    run_input={
        "searchUrl": "https://www.cars.bg/carslist.php?subtype=1&brand=16",
        "maxResults": 200,
        "enrichDetails": False,
        "proxyConfiguration": {
            "useApifyProxy": True,
            "apifyProxyGroups": ["RESIDENTIAL"]
        }
    }
)

items = list(client.dataset(run["defaultDatasetId"]).iterate_items())
print(f"Scraped {len(items)} listings")
for item in items[:3]:
    print(item["make"], item["model"], item["year"],
          item["price"], "EUR /", item["price_bgn"], "BGN")
Enter fullscreen mode Exit fullscreen mode

The searchUrl is optional. Leave it empty to scrape the default used-cars feed, or visit cars.bg, apply your filters (brand, body type, price band, year range), and paste the resulting URL. The Actor follows pagination from there.


Use Cases

EUR and BGN price analytics. Track asking prices in both currencies across the Bulgarian market. Since the lev is pegged to the euro, the BGN field is a sanity check for data quality rather than a currency risk surface — but having both is useful when joining against Bulgarian financial data sources that use one or the other.

Cross-border EU arbitrage. Bulgaria is one of the more affordable used-car markets in the EU. Compare Cars.bg EUR prices against German, French, or Romanian equivalents to identify import opportunities. The mileage_km field is already normalised to kilometres across the fleet.

Dealer intelligence. Filter by seller_type = dealer and location to track inventory in specific Bulgarian cities. Sofia, Plovdiv, and Varna account for most of the dealer volume.

Market research. Aggregate fuel_type across the full dataset to measure diesel vs. petrol vs. electric adoption in the Bulgarian market — one of the later EU adopters of EV technology.

Lead generation. Build a directory of Bulgarian dealers from seller_name and location. Diff successive runs to score dealers by listing volume and price-cut frequency.


FAQ ❓

How much does it cost?

Pay-Per-Event pricing: $0.05 for actor-start (one-off per run) plus $0.002 per result row. 1,000 results costs $2.05 total. Every new Apify account gets $5 of free credit — no credit card required to try.

Should I enable detail enrichment?

With enrichDetails: false (the default), you get make, model, year, price (EUR + BGN), mileage, fuel type, location, seller type, and photos from the listing card alone. That is enough for most pricing and market research use cases. Enable enrichDetails: true to add gearbox, engine displacement, engine power, body type, colour, first-registration month, full description, and seller name — but note that Cars.bg guards detail pages more aggressively, so enrichment performs best on larger proxy tiers.

Is scraping Cars.bg legal?

Cars.bg's terms restrict automated access. You are responsible for ensuring your use complies with applicable law and the site's terms. The Actor scrapes only public listing data visible to any unauthenticated browser. We do not bypass authentication or access private user data.

What currency does Cars.bg use?

Cars.bg shows each price in both EUR and BGN. The price field is the integer EUR amount and currency is always EUR. The price_bgn field carries the BGN amount shown alongside it on the listing card.


Get Started

The Actor is live on the Apify Store. Every new Apify account starts with $5 of free credit — enough for roughly 2,400 enriched listings or around 2,400 listing-card-only rows per dollar.

Cars.bg Bulgaria Car Scraper on Apify Store

Questions or edge cases? Use the Actor's Issues tab on Apify Console. We ship fixes weekly and read every report.


Built by Devil Scrapes — a fleet of opinionated public-data Actors. Honest pricing, real engineering, zero fine print.

Top comments (0)