# Kleinanzeigen Scraper (search, price & new-ad alerts, no login) (`datahamster/kleinanzeigen-listings`) Actor

Search kleinanzeigen.de (Germany classifieds) by keyword, city and category. One row per ad: title, price, VB/free flag, zip and city, image, category, dealer or private, posted date. Monitor mode alerts on new ads and price drops for a saved search. No login.

- **URL**: https://apify.com/datahamster/kleinanzeigen-listings.md
- **Developed by:** [Viktor Dubnytskiy](https://apify.com/datahamster) (community)
- **Categories:** E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 result items

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Kleinanzeigen Scraper (search, price & new-ad alerts, no login)

Search kleinanzeigen.de — Germany's largest classifieds site (ex-eBay Kleinanzeigen) — by keyword, city and category, and get one flat row per ad: title, price, location and posting date, with no login and no cookies.

### What you get

Real rows from the example dataset (query `fahrrad`):

| title | priceLabel | city | categoryId |
|---|---|---|---|
| "FOCUS THRON² 6.8 EQP E-Fully nur 91 km 625 WH Top Zustand 3190€" | `3.190 € VB` | Arnsberg | `217` |
| "Urtopia E-Bike Riemenantrieb OVP" | `990 €` | Konstanz | `217` |
| "Elektrofahrad" | `1.400 €` | Hamburg Heimfeld | `217` |

Full row: `id`, `url`, `platform`, `country`, `title`, `description`, `price`, `currency`, `priceLabel`, `priceNegotiable`, `priceType`, `categoryId`, `categoryPath`, `condition`, `city`, `district`, `region`, `lat`, `lon`, `images`, `sellerId`, `sellerName`, `sellerType`, `createdAt`, `refreshedAt`, `validTo`, `isPromoted`, `deliveryAvailable`, `params`, `query`, `rank`, `source`, `zip`, `location`, `scrapedAt`.

No seller names, phone numbers or e-mail addresses for private individuals are collected: search cards never show a private seller's name, and any phone/e-mail a private seller typed into their own ad text is scrubbed out (`includeDetails` mode, where the full text is read).

### Use cases

- Watch a saved search (keyword + city) and get alerted the moment a new ad appears — the classic "be first to reply" use case for bikes, furniture, electronics.
- Track price drops on a category you're shopping in.
- Build a local price benchmark for an item type across German cities.

### Try it in 10 seconds

Hit **Start**/**Try it** — the input already works: `queries: ["fahrrad"]`, `maxListingsPerQuery: 100`, `maxItems: 200`, nothing required.

To get alerted on new ads and price drops for a saved search: save the task, set **Mode** to `monitor`, and put it on a schedule (Apify → Schedules → cron `0 * * * *` hourly). Each run then returns only ads that are new or whose price changed since the previous check.

### How it works

1. Each query is searched at `kleinanzeigen.de/s-<query>/k0` (optionally scoped by city and/or category) and paginated with `/s-seite:<n>/...` until `maxListingsPerQuery` is reached or the page comes back short.
2. A page past the real last one is not empty — the site redirects it back to page 1 and re-serves almost the same ads; the actor detects that repeat and stops instead of re-charging for the same ads or looping.
3. `includeDetails` opens each ad's own page for the untruncated description and a confirmed private/dealer flag; text from a confirmed or likely-private seller has any phone number or e-mail address stripped before it is returned.

### Input

| Field | Meaning | Default |
|---|---|---|
| `queries` | Search terms, e.g. `fahrrad`, `sofa` | `["fahrrad"]` |
| `location` / `locationId` | City slug + kleinanzeigen's numeric location id to scope every query | empty |
| `categoryId` | Numeric category id to scope every query, e.g. `217` | empty |
| `minPrice` / `maxPrice` | Price bounds in EUR | empty |
| `maxListingsPerQuery` | Stop a query after roughly this many ads | `100` |
| `includeDetails` | Open each ad page for the full description and seller type | `false` |
| `strictQueryMatch` | Stop a query if a whole page looks unrelated (rarely triggers, see Limits) | `true` |
| `maxItems` | Stop after this many rows total | `200` |
| `mode` | `scrape` or `monitor` (only new/changed since last run) | `scrape` |
| `monitorStateId`, `webhookUrl`, `telegramBotToken`, `telegramChatId` | Monitor-mode state key and alert targets | empty |

### Pricing

| Event | Price |
|---|---|
| result | $0.0015 per ad ($1.50 per 1,000) |
| monitor-check | $0.005 per monitor run |
| change | $0.0015 per new/changed ad |

Charged only for ads actually pushed. A run that finds nothing pushes nothing and charges no result events.

### Why this actor

- No login, no cookies, no account to keep alive.
- Price, VB (negotiable) and free-ad flags are parsed out of the card text, not guessed.
- Monitor mode fingerprints on price, so a saved search alerts on both brand-new ads and price changes on ads it already saw.
- Private-seller contact details are actively stripped, not just "not collected on purpose".

### Limits

- kleinanzeigen.de shows ~27 ads per search page; `maxListingsPerQuery` is rounded up to whole pages.
- Seller type on the search card is only ever `commercial` (a shop/dealer badge); a private seller's type is left unset unless `includeDetails` opens the ad page, which prints "Privater Nutzer"/"Gewerblicher Nutzer" explicitly.
- No category-path breadcrumb, no lat/lon, no listing attributes table (only what the search card and, with `includeDetails`, the ad's own description carry).
- `strictQueryMatch` is a defensive no-op today: a nonsense-query fixture confirmed kleinanzeigen.de answers with a genuine "no results" page, not filler.

### FAQ

**Does it need a kleinanzeigen.de login?** No. Every request is made as a logged-out visitor.

**What happens when a search has no ads?** No rows are pushed and no result events are charged. The `RUN_SUMMARY` record in the run's key-value store carries `emptyReason`.

**Does it collect seller names or phone numbers?** No private-seller name is ever returned — the search card shows none, and `sellerName` is only populated for a shop/dealer badge. Any phone number or e-mail a private seller wrote into their own ad text is stripped out.

### Changelog

- 0.1: initial release — keyword/city/category search, price & VB/free parsing, monitor mode, ad-detail enrichment with private-seller contact scrubbing.

***

If this actor saved you time, a short review on its Store page genuinely helps other people find it. Found a bug or need a field that is missing? Open a ticket on the **Issues** tab.

# Actor input Schema

## `maxItems` (type: `integer`):

Stop after this many results (you are charged only for pushed items)

## `mode` (type: `string`):

scrape = full results; monitor = only new/changed items since the previous run of this task

## `monitorStateId` (type: `string`):

Optional state id when not running as a saved task (monitor mode)

## `webhookUrl` (type: `string`):

POST a change summary here in monitor mode

## `telegramBotToken` (type: `string`):

Optional: bot token for monitor-mode change summaries

## `telegramChatId` (type: `string`):

Optional: chat id that receives monitor-mode summaries

## `queries` (type: `array`):

Keywords to search kleinanzeigen.de for, one per line, e.g. "fahrrad", "sofa", "iphone 13". Each query is searched and paginated separately.

## `location` (type: `string`):

Optional city slug to scope every query to, e.g. "berlin". Must be combined with Location id, or it is ignored. Example: "berlin".

## `locationId` (type: `integer`):

kleinanzeigen.de's numeric location id for the city above, e.g. 3331 for Berlin. Find it by searching a city on the site and reading the "...l<number>" suffix from the resulting URL.

## `categoryId` (type: `integer`):

Optional numeric category id to scope every query to, e.g. 217 for Fahrräder (bicycles). Find it from the "k0c<number>" suffix of a category page URL on the site.

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

Drop ads priced below this amount in euro. Ads with no listed price are dropped too when this is set. Example: 50.

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

Drop ads priced above this amount in euro. Ads with no listed price are dropped too when this is set. Example: 500.

## `maxListingsPerQuery` (type: `integer`):

Stop paginating one query after roughly this many ads (pages are ~27 ads each). Example: 100.

## `includeDetails` (type: `boolean`):

true = open each ad's own page for the full, untruncated description and a confirmed private/dealer flag (one extra request per ad; a private seller's phone number or e-mail found in the text is removed). false = return only what the search-result card shows. Example: false.

## `strictQueryMatch` (type: `boolean`):

Safety net: stop a query's pagination if a whole result page looks unrelated to the search term. kleinanzeigen.de answers a nonsense query with a genuine "no results" page, so this rarely changes anything today. Example: true.

## Actor input object example

```json
{
  "maxItems": 200,
  "mode": "scrape",
  "queries": [
    "fahrrad"
  ],
  "maxListingsPerQuery": 100,
  "includeDetails": false,
  "strictQueryMatch": true
}
```

# Actor output Schema

## `results` (type: `string`):

All pushed rows (dataset, JSON)

## `resultsTable` (type: `string`):

Dataset in the Console viewer

## `runSummary` (type: `string`):

RUN\_SUMMARY record

# 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 = {
    "queries": [
        "fahrrad"
    ],
    "maxListingsPerQuery": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("datahamster/kleinanzeigen-listings").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 = {
    "queries": ["fahrrad"],
    "maxListingsPerQuery": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("datahamster/kleinanzeigen-listings").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 '{
  "queries": [
    "fahrrad"
  ],
  "maxListingsPerQuery": 100
}' |
apify call datahamster/kleinanzeigen-listings --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,datahamster/kleinanzeigen-listings"
        }
    }
}
```

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/tO3cy5bDqJ3cED3vb/builds/gVkiPQrh0VbZzaMh7/openapi.json
