Kleinanzeigen.de Scraper - Ads, Prices & Seller Details avatar

Kleinanzeigen.de Scraper - Ads, Prices & Seller Details

Pricing

from $1.00 / 1,000 results

Go to Apify Store
Kleinanzeigen.de Scraper - Ads, Prices & Seller Details

Kleinanzeigen.de Scraper - Ads, Prices & Seller Details

From $1/1K. Scrape Kleinanzeigen.de ads by keyword, category, location, price, seller type, or search URL. Extract descriptions, photos, attributes, seller details, posted dates, category paths, and imprint data. Returns flat JSON records ready for spreadsheets or databases.

Pricing

from $1.00 / 1,000 results

Rating

0.0

(0)

Developer

Abot API

Abot API

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

15 days ago

Last modified

Share

Kleinanzeigen.de Scraper: Classifieds Listings, Prices, Sellers & Photos

Kleinanzeigen.de Scraper turns Germany's largest classifieds marketplace (formerly eBay Kleinanzeigen) into structured data. Search by keyword, category, location, radius, price, offer type and seller type across any category, vehicles, electronics, furniture, property, jobs, fashion, hobby and more, or paste your own Kleinanzeigen.de search or category URLs. Each ad comes back as one flat record with price, full description, every photo, the attribute list, the seller profile and, for registered traders, the legal imprint. Export to JSON, CSV or Excel, or pull results straight into your app through the API.

Why This Scraper?

  • Two ways in. A filter builder (keyword, category id, location with radius, min/max price, offered vs wanted, private vs commercial, sort) or a paste-URL mode that accepts any Kleinanzeigen.de search or category URL.
  • Every category, not just one niche. The same filters and fields work whether you're tracking bicycles, cars, flats, electronics, jobs or collectibles.
  • Automatic forward pagination. 27 ads per page, walked until Kleinanzeigen.de's own real end of results or your page limit, whichever comes first.
  • About 40 fields per ad. Full description, all photo URLs, attribute key/value pairs, category path, seller name/type/registration date, and the commercial-seller legal imprint (name, address, email) when the seller is a registered trader.
  • A lean, cheaper mode. Turn off detail fetching for a lighter run that still returns about 14 fields straight from the search results.
  • Deduplicates promoted listings that repeat across result pages, so you don't pay for the same ad twice.
  • Built for schedules. Incremental mode tags every ad NEW, UPDATED, UNCHANGED, REAPPEARED or EXPIRED on recurring runs, and a resumable full-catalogue walk survives platform interruptions.

Use Cases

  • Resellers and classifieds arbitrage: watch a category and price band for underpriced items worth flipping.
  • Market and pricing research: build price distributions for a product type or city over time from real, live asking prices.
  • Property and rental scouting: monitor a city or postal code for new listings the moment they post.
  • Local lead generation: find people offering or seeking specific goods or services in a chosen city or radius.
  • Marketplace monitoring: schedule a saved search and get only new and changed ads each run with incremental mode.

Data You Get

Sample shape, values are illustrative placeholders, not from a live ad.

FieldExample
adId"3400000001"
url"https://www.kleinanzeigen.de/s-anzeige/sample-listing-title/3400000001-217-0001"
title"Sample Listing Title"
descriptionShort"First sentence of the ad as shown in the result list."
descriptionFull"Full ad description text appears here when fetchDetails is true."
price"1.500 € VB"
priceAmount / priceType1500 / "NEGOTIABLE"
currency"EUR"
locationText"10115 Berlin - Mitte"
postalCode / cityName"10115" / "Mitte"
latitude / longitudenull / null (rarely present in the page HTML)
categoryId / categoryName"217" / "Fahrräder & Zubehör"
categoryPath"Auto, Rad & Boot > Fahrräder & Zubehör"
postedDate / postedDateText"2026-01-01" / "Heute, 09:00"
imageCount / imageUrls5 / ["https://img.kleinanzeigen.de/api/v1/prod-ads/images/00/00000000-0000-0000-0000-000000000000?rule=$_59.AUTO"]
attributes / attributesRaw{ "Art": "Herren", "Typ": "Elektrofahrräder" } / [{ "name": "Art", "value": "Herren" }]
isPro / isTopAd / isSoldfalse / false / false
sellerId / sellerName / sellerType"100000000" / "Sample Seller" / "PRIVATE"
sellerActiveSince / sellerOtherAdsCount"2026-01-01" / 4
imprint{ "raw": "Sample Trader\nEinzelunternehmer\n10115 Berlin, Sample Str. 1\ntrader@example.com", "lines": ["Sample Trader", "Einzelunternehmer"], "email": "trader@example.com", "phone": null, "vatId": null, "registerNumber": null, "registerCourt": null, "managingDirector": null } (commercial sellers only, null for private sellers)
searchUrl"https://www.kleinanzeigen.de/s-fahrrad/k0"
scrapedAt"2026-01-01T00:00:00.000Z"

Also included: tags (site badges such as shipping available) and adType (OFFERED or WANTED). Fields that only come from the detail page (descriptionFull, categoryName, categoryPath, extra photos beyond the first, attributes, seller fields, imprint, postedDate) are null or empty when fetchDetails is false. In Incremental mode every record also carries changeType, changedFields, firstSeenAt and lastSeenAt.

How to Use

  1. Pick a mode: search (build from the filters below) or url (paste ready-made Kleinanzeigen.de search or category URLs).
  2. For search mode, fill in keyword, category, location, radius, price range, offer type, seller type and sort. For url mode, paste one or more search URLs.
  3. Set Max pages per search, Max listings total and Fetch full ad details to shape run size, depth and cost, then click Start.
  4. Download the dataset as JSON, CSV or Excel, or read it through the API.

Search by keyword:

{
"mode": "search",
"keyword": "fahrrad",
"maxPages": 3,
"maxListings": 50,
"fetchDetails": true
}

Search a category in a city with a radius and a price range, newest first:

{
"mode": "search",
"keyword": "",
"categoryId": "217",
"location": "Berlin",
"radiusKm": 20,
"minPrice": 100,
"maxPrice": 800,
"sortBy": "date",
"maxPages": 5,
"maxListings": 100,
"fetchDetails": true
}

Only private sellers, wanted ads:

{
"mode": "search",
"keyword": "sofa",
"offerType": "gesuche",
"sellerType": "privat",
"maxPages": 2,
"fetchDetails": false
}

Paste several search URLs (filter fields are ignored in this mode):

{
"mode": "url",
"urls": [
"https://www.kleinanzeigen.de/s-fahrrad/k0",
"https://www.kleinanzeigen.de/s-berlin/sofa/k0l3331"
],
"maxPages": 3,
"maxListings": 80,
"fetchDetails": true
}

URL mode is built for search and category result pages. A link to a single ad (/s-anzeige/...) is not rejected, but it is read as if it were a result page, so you get the handful of "similar ads" suggestions shown on that ad's page instead of the ad itself. To get one specific ad, search for it and use the search URL.

Pasted searches share one list of collected ads. If a later result page of one search is made up entirely of ads that another pasted search already collected in the same run, that search treats the page as its end and stops there, skipping all of its remaining pages. With Incremental mode and emitExpired on, the ads on those skipped pages can then be reported as EXPIRED (and billed) even though they are still live. If your pasted searches overlap heavily, run them one at a time (or give each its own schedule) instead of together.

Run it from your code

Python:

from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/kleinanzeigen-de-scraper").call(run_input={"mode": "search", "keyword": "fahrrad", "maxListings": 50})
for ad in client.dataset(run["defaultDatasetId"]).iterate_items():
print(ad["title"], ad["price"])

JavaScript:

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/kleinanzeigen-de-scraper').call({ mode: 'search', keyword: 'fahrrad', maxListings: 50 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the Integrations tab.

Resuming a full-catalogue walk

For an unbounded pull (maxPages: 0, maxListings: 0), two things protect a long run:

  • resumeFromRunId lets you continue in a brand-new run: paste the id of a previous run (or its dataset), and this run skips every adId already saved there, appending only new ads. One limit: once the resumed walk has found at least one new ad, the first later page made up entirely of already-saved ads is treated as the end of the search. On a busy search where new ads have been posted at the top since the interrupted run, the resumed run can therefore return just those new ads and stop before it reaches the unfinished part of the walk. Resume soon after the interruption, or check the run log for a "stopping (end of catalogue reached)" line on an early page.
  • A checkpoint is saved automatically as the run progresses. If the run is interrupted by an Apify platform migration or you click Resurrect on a failed run, it picks back up from where it left off in the same run, without re-saving (or re-charging for) ads it already collected. No input is needed for this; it's automatic.

If Incremental mode is also on, that checkpoint saves the collected-ad list and the run's counters, but not this run's in-progress change-tracking. Rows already written to the dataset stay there, and ads are not falsely marked EXPIRED, but the ads that were tagged NEW, UPDATED or REAPPEARED before the interruption are not remembered as such. The next scheduled run reports those same ads with the same tag once more (and bills those rows again); tracking for them is accurate again from the run after that. The interrupted run may also skip its EXPIRED check; any ads that really disappeared are then reported on the next complete run instead.

Tracking a recurring search (incremental mode)

Turn on incrementalMode to monitor the same search on a schedule instead of re-scraping everything each time. The actor remembers what it saw last time (keyed by stateKey, or an auto-derived hash of your search/filter settings when left empty) and tags every ad in the dataset with a changeType:

  • NEW: first time this ad has been seen for this search.
  • UPDATED: the ad changed since last time; changedFields lists exactly which fields.
  • UNCHANGED: nothing changed. Suppressed by default (not pushed, not billed); turn on emitUnchanged to include these rows anyway.
  • REAPPEARED: an ad that had gone EXPIRED is back.
  • EXPIRED: an ad tracked previously is no longer found. Only emitted when emitExpired is on AND the run reached the real end of every tracked search (no resumeFromRunId, no maxListings/maxPages cap cutting it short); a run cut short by a cap never marks anything expired, since it can't tell "gone" apart from "not reached yet". The one exception is several overlapping pasted URLs, described under How to Use above, where a search can stop early without the run noticing.

Every row also carries changedFields, firstSeenAt, and lastSeenAt. incrementalMode is independent of resumeFromRunId: the former tracks the same search across many separate scheduled runs, the latter continues one specific interrupted run.

A few behaviours worth knowing before you rely on this for billing-sensitive schedules:

  • EXPIRED rows are not capped by maxListings. That cap counts the ads checked during the walk; once a run reaches a complete scan and emitExpired fires, every ad that disappeared since last time is pushed in one batch regardless of maxListings. If your tracked baseline is large and many ads vanish at once, a single run's item count (and bill) can exceed maxListings.
  • Combining resumeFromRunId with incrementalMode on the very first run for a search seeds every ad from the resumed dataset into the baseline without a real stored snapshot. The next ordinary scheduled run then classifies each of them it finds as UPDATED, with every field listed in changedFields (and bills those rows), even though nothing on the ad actually changed, simply because there was nothing genuine to compare against yet. This happens once per ad and then self-corrects.
  • A manually-set stateKey is taken exactly as given, unlike the auto-derived key, which always changes when your filters change. Reusing the same manual stateKey for two differently configured recurring searches (different keyword, category, or a different fetchDetails setting) merges them into one shared baseline. Real, unchanged ads can then show up as UPDATED purely because the two setups return a different field shape, and with emitExpired on, ads that only one of the searches covers can be reported as EXPIRED by the other even though they are still live. Give each independent recurring search its own stateKey, or leave it blank.

Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step after the scrape. The Apify dataset is never changed.

What gets written to the connector: a condensed, human-readable summary of each record, not the full JSON. Each item becomes one entry with a title and its key fields flattened to plain text. The complete record always stays in the Apify dataset.

  1. Authorize a connector once under Apify → Settings → Integrations (Notion, Linear, Airtable, or Apify).
  2. Select it in the "Pipe results into your apps" input field. (If the picker is empty, you haven't authorized a connector yet.)
  3. For Notion, also set notionParentPageUrl to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

Input Parameters

ParameterTypeDefaultDescription
modestringsearchsearch to build URLs from the filters below, url to paste your own search URLs.
keywordstring(empty; prefilled fahrrad)Free-text query. Leave empty to browse a whole category.
categoryIdstring(empty)Numeric Kleinanzeigen.de category id, e.g. 217. Find it in any category URL: .../c217 becomes 217.
locationstring(empty)City name or German postal code. Resolved automatically to the matching location id.
radiusKminteger0Search radius in km around the location. Ignored when no location is set.
minPriceinteger(empty)Lowest price to include.
maxPriceinteger(empty)Highest price to include.
offerTypestringanyangebote for offered ads, gesuche for wanted ads, any for both.
sellerTypestringanyprivat for private sellers, gewerblich for commercial sellers/shops, any for both.
sortBystringrelevancerelevance, date (newest), price-asc, price-desc, distance. Promoted ads can appear first regardless of sort.
urlsarray of strings(empty; prefilled ["https://www.kleinanzeigen.de/s-fahrrad/k0"])Search/category URLs to scrape (used only when mode is url). Single-ad links are not supported.
maxPagesinteger0Result pages to walk per search/URL (27 ads per page). 0 means unlimited: the walk keeps going until Kleinanzeigen.de has no next page for that search (Kleinanzeigen.de may itself cap a search around 50 pages, which is fine, the walk simply stops there).
maxListingsinteger20Stop after this many ads across all searches. With several URLs the cap is split evenly between them. In Incremental mode, unchanged ads that are not returned still count toward it. 0 means unlimited (still bounded by maxPages).
fetchDetailsbooleantrueOpen each ad's detail page for the full description, all photos, attributes, seller info and imprint. Turn off for a faster, cheaper run.
resumeFromRunIdstring(empty)A previous run id or dataset id to continue ONE interrupted full-catalogue walk across separate runs. Ads already collected there (matched by adId) are skipped, so this run only saves the new ones.
incrementalModebooleanfalseTrack a recurring search over time: remembers the ads from this actor's own previous run of the SAME search and tags every ad with a changeType (NEW/UPDATED/UNCHANGED/REAPPEARED/EXPIRED). Pair with the Apify Scheduler. Distinct from resumeFromRunId above.
stateKeystring(empty)Incremental mode only. A name for this recurring search's saved state. Leave empty to auto-derive one from your search/filter settings; two different filter setups never share a baseline this way.
emitUnchangedbooleanfalseIncremental mode only. Also push ads with no detected change (changeType=UNCHANGED). Bills one extra row per unchanged ad every run.
emitExpiredbooleanfalseIncremental mode only. Also push ads tracked previously but no longer found, once, tagged changeType=EXPIRED. Only fires on a run that reaches the real end of every tracked search (no resumeFromRunId, no maxListings/maxPages cap hit). Bills one extra row per expired ad, and this batch is not itself limited by maxListings.
proxyobjectResidentialApify Proxy. Residential is the default and rotates across EU countries automatically unless you pin one; Datacenter can be picked instead in the same field.
mcpConnectorsarray(none)Optional: send a summary of each record to apps you authorized under Integrations.
notionParentPageUrlstring(none)Notion connector only: page under which records are created.
maxNotifyListingsinteger50Cap on items written to each connector per run.

Output Example

Sample shape, values are illustrative placeholders, not from a live ad.

{
"adId": "3400000001",
"url": "https://www.kleinanzeigen.de/s-anzeige/sample-listing-title/3400000001-217-0001",
"title": "Sample Listing Title",
"descriptionShort": "First sentence of the ad as shown in the result list.",
"descriptionFull": "Full ad description text appears here when fetchDetails is true.",
"price": "1.500 € VB",
"priceAmount": 1500,
"priceType": "NEGOTIABLE",
"currency": "EUR",
"locationText": "10115 Berlin - Mitte",
"postalCode": "10115",
"regionName": "Berlin",
"cityName": "Mitte",
"latitude": null,
"longitude": null,
"categoryId": "217",
"categoryName": "Fahrräder & Zubehör",
"categoryPath": "Auto, Rad & Boot > Fahrräder & Zubehör",
"postedDate": "2026-01-01",
"postedDateText": "Heute, 09:00",
"viewCount": null,
"imageCount": 1,
"thumbnailUrl": "https://img.kleinanzeigen.de/api/v1/prod-ads/images/00/00000000-0000-0000-0000-000000000000?rule=$_59.AUTO",
"imageUrls": ["https://img.kleinanzeigen.de/api/v1/prod-ads/images/00/00000000-0000-0000-0000-000000000000?rule=$_59.AUTO"],
"attributes": { "Art": "Herren", "Typ": "Elektrofahrräder", "Zustand": "Sehr Gut" },
"attributesRaw": [{ "name": "Art", "value": "Herren" }, { "name": "Typ", "value": "Elektrofahrräder" }, { "name": "Zustand", "value": "Sehr Gut" }],
"isPro": false,
"isShop": false,
"isTopAd": false,
"isSold": false,
"adType": "OFFERED",
"tags": ["Versand möglich"],
"sellerId": "100000000",
"sellerName": "Sample Seller",
"sellerType": "PRIVATE",
"sellerUrl": "https://www.kleinanzeigen.de/s-bestandsliste.html?userId=100000000",
"sellerActiveSince": "2026-01-01",
"sellerOtherAdsCount": 4,
"imprint": null,
"searchUrl": "https://www.kleinanzeigen.de/s-fahrrad/k0",
"scrapedAt": "2026-01-01T00:00:00.000Z"
}

Plan Requirement

Apify Residential proxy is the default and recommended connection for Kleinanzeigen.de; it rotates automatically across EU countries unless you pin one. Datacenter can be picked instead in the Proxy configuration field, though Kleinanzeigen.de blocks a meaningful share of Datacenter IPs, so expect more retries and occasionally fewer results. Detail enrichment (fetchDetails) adds one request per ad; use Max listings total, Max pages per search, or turn detail enrichment off to control a run's size and cost.

FAQ

How much does it cost?

You pay per ad returned, with detail enrichment, unchanged rows and expired rows billed as separate, optional events only when you switch them on. The Pricing tab shows the current rates. Use Max listings total and Max pages per search to cap the cost of any run.

This actor collects only publicly available classifieds data. You are responsible for how you use it: follow Kleinanzeigen.de's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Personal data such as a seller's name or a commercial imprint is publicly posted by the seller themselves; handle it in line with applicable data protection law.

Can I get only new or changed ads on a schedule?

Yes. Schedule the actor from the Schedules tab with the same search or URLs and turn on Incremental mode. Each run then tags every ad NEW, UPDATED, UNCHANGED, REAPPEARED or EXPIRED, and unchanged ads are not billed unless you turn emitUnchanged on.

What's the difference between "Resume from a previous run" and "Incremental mode"?

resumeFromRunId continues ONE specific interrupted run: paste its run or dataset id and this run only appends ads it hasn't already saved there. incrementalMode is for a recurring search you run again and again on a schedule; it remembers its own state between runs and classifies every ad as NEW, UPDATED, UNCHANGED, REAPPEARED or EXPIRED. They solve different problems and can be combined for the first run of a new search, though the scheduled run right after that combined run reports the ads carried over from the resumed dataset as UPDATED once before tracking settles down.

Why did my incremental run return more rows, and cost more, than Max listings?

maxListings bounds how many ads a run checks while walking result pages (unchanged ads that are skipped still count toward it). It does not bound the EXPIRED batch: once a run completes a full scan with emitExpired on, every previously tracked ad that has disappeared is pushed in that same run, on top of whatever the walk itself collected. If your tracked baseline is large, turn emitExpired off, or expect that batch to add to the run's total.

Why did my run return 0 listings instead of failing?

An empty search is a legitimate outcome, not an error, so the run finishes successfully with a status message explaining the likely cause (usually a Datacenter proxy being blocked, or filters that are too narrow). The run does fail outright only in specific cases: an unreadable resumeFromRunId, or turning on incrementalMode together with resumeFromRunId when that search already has saved incremental state.

Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.

🔗 Want more classifieds data?

Pair this actor with these related scrapers from the same team:

🏷️ Finn.no
From $0.8/1K. Scrape active Finn.no ads via search builder or URL. Returns 30+ fields per...
🏷️ OLX Global
From $1/1K. Fast OLX scraper across 9 countries, including Poland, Romania, Portugal...
🏷️ Craigslist Scraper
Scrape Craigslist postings in any of 700+ cities worldwide: price, title, posting text...
🏷️ 2dehands & 2ememain
Scrape classifieds from 2dehands.be and 2ememain.be by keyword, filters, or URLs. Returns...
🏷️ Gumtree UK
Scrape gumtree.com classifieds across motors, property, jobs, services, pets and goods...
🏷️ Tutti.ch
Scrape listings from tutti.ch across all 23 categories, including vehicles, property...

👉 Browse all abotapi scrapers

💬 Support & custom scrapers

  • 🐞 Found a bug or a missing field? Open a ticket on the Issues tab. We usually reply within hours.
  • 🛠️ Need another site, extra fields or a private build? Email abotapi@proton.me or message Telegram @abotapi.
  • ⭐ Enjoying it? A quick review on the actor page helps other users find it.