By the end of this tutorial, you will have a Python script that pulls used-phone ads from Kleinanzeigen.de into structured JSON: title, asking price in EUR, whether the price is negotiable, town and postcode, posting time, shipping and buy-now flags, and the ad URL.
Kleinanzeigen.de is a German classifieds site where people sell second-hand phones, bikes, furniture and cars. Copying ads by hand into a spreadsheet gets old quickly, and each search shows only 25 ads per page.
In this guide, we use Python and the Apify Actor Kleinanzeigen Scraper (reventadata/kleinanzeigen-scraper). Our benchmark run (configured with searchQueries: ["iphone 13"], sellers: "private", shippingOnly: true, maxItems: 60 and maxPages: 4) finished in 9.3 seconds and returned 60 ads.
⏱️ Time estimate: About 30 minutes.
Prerequisites
To follow this tutorial, you will need:
- Python 3.11+ installed locally.
- The Apify API client for Python (
pip install apify-client). - An Apify account and its API token.
Copy your API token from API & Integrations in Apify Console, then install the client:
pip install apify-client
What you'll achieve
You will run the Actor from Python and get back one JSON record per Kleinanzeigen ad. The records come straight from the search result cards, so no ad page is opened unless you ask for it.
Note on data boundaries: the default run reads the public search result cards only. The seller fields (
sellerName,sellerKind,sellerSinceand a few others) staynullunless you turn onfetchDetails.
Example use case: Suppose you want to compare asking prices for a used iPhone 13 that private sellers will ship. One input, searchQueries: ["iphone 13"] with sellers: "private" and shippingOnly: true, returns exactly that list.
Step 1: Access Kleinanzeigen Scraper
Open the Kleinanzeigen Scraper page on Apify Store. The page lists the input fields and a sample of the output. The Try for free button links to Apify Console. In this guide we skip the Console form and start the Actor from Python.
Step 2: Configure search parameters
The main input is searchQueries, a list of search terms. Each term is searched across every category of kleinanzeigen.de, Germany-wide, unless you add a category or location ID. Here is the input we used for the benchmark run:
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run_input = {
"searchQueries": ["iphone 13"],
"sellers": "private",
"shippingOnly": True,
"maxItems": 60,
"maxPages": 4,
}
run = client.actor("reventadata/kleinanzeigen-scraper").call(run_input=run_input)
for item in client.dataset(run.default_dataset_id).iterate_items():
print(item["title"], item["price"], item["currency"], item["location"], item["url"])
This is the exact input behind the numbers in this article. maxPages: 4 caps the walk at four result pages of 25 ads each, and maxItems: 60 stops the run once 60 rows are saved.
Step 3: Configure optional settings and filters
The Actor has more switches than the example uses:
| Parameter | Type | Default | What it does |
|---|---|---|---|
searchQueries |
array | none | One search per term. Optional if you give a categoryId. |
categoryId |
integer | none | Numeric category from the site's own URLs (173 = Smartphones and phones). |
locationId |
integer | none | Numeric place from the site's URLs (3331 = Berlin). Needs a search term or category ID. |
offerType |
string | offers |
offers, wanted or all. Wanted ads are the cards marked Gesuch ("I am looking for"). |
sellers |
string | any |
any, private or proShops. |
minPrice, maxPrice
|
integer | none | Inclusive EUR bounds, applied after fetching. |
shippingOnly |
boolean | false |
Keeps only cards marked Versand möglich. |
excludePromoted |
boolean | false |
Drops the undated cards shown ahead of the dated results. |
requireKeywordMatch |
boolean | true |
Drops ads where a search word is missing from both title and card text. |
fetchDetails |
boolean | false |
Opens each kept ad for description, attributes and seller fields. |
maxItems |
integer | 100 |
Hard cap on rows across all search terms. |
maxPages |
integer | 50 |
Pages per search term, at most 50 (1,250 ads). |
Three details are worth knowing before you rely on a filter:
-
sellers: "private"means "no PRO badge". The Actor judges seller kind from the search card, so a commercial seller without a PRO shop badge is listed as private. Turn onfetchDetailsand readsellerKindfor the site's own label. -
Price bounds drop negotiable-only ads. Ads with no numeric price (just "VB") are dropped whenever you set
minPriceormaxPrice. Free ads count as 0. -
The site mixes in wanted ads. The default
offerType: "offers"already removes them.
Step 4: Run the Actor
Replace the token placeholder, save the script and run it. .call() starts the Actor in the Apify cloud and waits until it finishes. The Actor then fetches the result pages one by one, filters each card against your input, and saves the kept ads to the run's dataset. Keep the script running until it prints; our benchmark run finished in 9.3 seconds.
Each line the script prints looks like the sample record in the next step, cut down to five fields.
Step 5: View and download results
The loop in Step 2 iterates the dataset item by item. In Apify Console the same dataset can be exported as JSON, CSV or Excel from the run's storage. Here is the first record of our benchmark run, verbatim:
{
"id": "3549899772",
"title": "IPhone 13 Pro Max",
"url": "https://www.kleinanzeigen.de/s-anzeige/iphone-13-pro-max/3549899772-173-702",
"price": 290,
"currency": "EUR",
"priceNegotiable": true,
"isFree": false,
"previousPrice": null,
"postcode": "24143",
"location": "Gaarden",
"region": null,
"postedAt": "2026-10-23T20:59:00+02:00",
"postedDate": "2026-10-23",
"isPromoted": false,
"isWanted": false,
"shipping": true,
"buyNow": false,
"sellerWarranty": false,
"isProShop": false,
"shopName": null,
"shopUrl": null,
"imageUrl": "https://img.kleinanzeigen.de/api/v1/prod-ads/images/4b/4b5daab1-628b-45a6-a28b-258f983bb9b4?rule=$_2.AUTO",
"imageCount": 4,
"snippet": "iPhone 13 Pro Max – 128 GB – Guter Zustand\n\nIch verkaufe mein iPhone 13 Pro Max mit 128 GB Speicher...",
"description": null,
"condition": null,
"attributes": null,
"sellerName": null,
"sellerId": null,
"sellerKind": null,
"sellerShopUrl": null,
"sellerSince": null,
"sellerAdCount": null,
"categoryId": null,
"categoryPath": null,
"latitude": null,
"longitude": null,
"detailFetched": false,
"matchedKeyword": "iphone 13",
"scrapedAt": "2026-10-23T19:00:35.214489+00:00"
}
Understanding your results
The fields you will use most:
-
price: asking price in whole euros. It isnullwhen the ad shows only "VB", and0for free ads. -
priceNegotiable:truewhen the card carries VB (Verhandlungsbasis). -
postcodeandlocation: the five-digit postcode and the town or district on the card. -
postedAtandpostedDate: posting time (Europe/Berlin) and date. -
shipping,buyNow,sellerWarranty: badges on the card (Versand möglich, Direkt kaufen, Verkäufergarantie). -
isPromoted,isProShop,isWanted: how the card was placed and who posted it.
What our 60 rows show:
- 59 ads had a numeric price, ranging from 4 EUR to 400 EUR with a median of 230 EUR. One ad was posted as "VB" with no figure.
- 33 ads were marked negotiable and 27 had a fixed price. None were free (33 + 27 + 0 = 60).
- 2 ads carried a
previousPrice, for example an iPhone 13 mini listed at 199 EUR, down from 279 EUR. - All 60 offered shipping and all 60 came from sellers without a PRO badge, as the input asked. 22 had Direkt kaufen switched on.
- 1 ad was a promoted placement; the other 59 were organic.
Pro tips and advanced usage
-
Page size drives
maxPages. A page holds 25 ads, somaxPages: 4can look at about 100 cards. The site serves at most 50 pages, or 1,250 ads, per search. -
Narrow by category and place. Use
categoryIdandlocationIdfrom the numbers in the site's own URLs instead of searching all of Germany. -
Add detail pages when you need seller data.
fetchDetails: trueopens every kept ad and fills description, attributes,sellerKindandsellerSince. It adds one extra request per row, so runs are slower. -
Drop promoted cards. The Actor infers promoted cards from the missing posting date; set
excludePromoted: trueto remove them. - Pricing: For current pricing and details, see the Store page.
-
Automate with webhooks. A webhook's payload
resourceis the response of the Get Actor run API endpoint (docs.apify.com/platform/integrations/webhooks/actions), which includesdefaultDatasetId.
Troubleshooting common issues
-
The run fails with "No rows pushed for any search". The Actor raises this error when nothing was saved. Either every ad was filtered out (a narrow price range plus
shippingOnly, say) or the page layout changed. -
The run fails with "Give at least one search term". You passed neither
searchQueriesnor acategoryId, and the Actor raises that error at startup. AlocationIdalone is not enough. -
Input refused before the run starts.
offerTypeandsellersaccept only the values in the table above; anything else is refused by the platform before the run begins. -
Cases and repair offers in the results. The site matches your words anywhere in the ad text.
requireKeywordMatchis on by default and keeps only ads whose title and card text, read together, contain every search word.
For support, use the Actor's Store page.
Next steps
Save the search as a task and add an Apify schedule to repeat it, then compare price and previousPrice between runs, or feed the rows into a spreadsheet or database. Start with the Apify Python client documentation and the Store page.
What are you planning to look for on Kleinanzeigen? Drop a comment below!
Top comments (0)