Feedback and high-quality pull requests are highly welcome!
For details on the new smoke test strategy and contributor guidance, see TESTING.md.
kleinanzeigen-bot is a command-line application to publish, update, delete, and republish listings on kleinanzeigen.de.
- Automated Publishing: Publish new listings from YAML/JSON configuration files
- Smart Republishing: Automatically republish listings at configurable intervals to keep them at the top of search results
- Bulk Management: Update or delete multiple listings at once
- Download Listings: Download existing listings from your profile to local configuration files
- Browser Automation: Uses Chromium-based browsers (Chrome, Edge, Chromium) for reliable automation
- Flexible Configuration: Configure defaults once, override per listing as needed
The use of this program could violate the terms of service of kleinanzeigen.de applicable at the time of use. It is your responsibility to ensure the legal compliance of its use. The developers assume no liability for any damages or legal consequences. Use is at your own risk. Any unlawful use is strictly prohibited.
Die Verwendung dieses Programms kann unter Umständen gegen die zum jeweiligen Zeitpunkt bei kleinanzeigen.de geltenden Nutzungsbedingungen verstoßen. Es liegt in Ihrer Verantwortung, die rechtliche Zulässigkeit der Nutzung dieses Programms zu prüfen. Die Entwickler übernehmen keinerlei Haftung für mögliche Schäden oder rechtliche Konsequenzen. Die Nutzung erfolgt auf eigenes Risiko. Jede rechtswidrige Verwendung ist untersagt.
-
The following components need to be installed:
- Chromium, Google Chrome, or Chromium based Microsoft Edge browser
-
Open a command/terminal window
-
Download and run the app by entering the following commands:
-
On Windows:
curl -L https://github.com/Second-Hand-Friends/kleinanzeigen-bot/releases/download/latest/kleinanzeigen-bot-windows-amd64.exe -o kleinanzeigen-bot.exe kleinanzeigen-bot --help
-
On Linux:
curl -L https://github.com/Second-Hand-Friends/kleinanzeigen-bot/releases/download/latest/kleinanzeigen-bot-linux-amd64 -o kleinanzeigen-bot chmod 755 kleinanzeigen-bot ./kleinanzeigen-bot --help
-
On macOS:
curl -L https://github.com/Second-Hand-Friends/kleinanzeigen-bot/releases/download/latest/kleinanzeigen-bot-darwin-amd64 -o kleinanzeigen-bot chmod 755 kleinanzeigen-bot ./kleinanzeigen-bot --help
-
- The following components need to be installed:
- Docker
- Bash (on Windows e.g. via Cygwin, MSys2 or git)
- X11 - X Window System display server (on Windows e.g. https://github.com/P-St/Portable-X-Server/releases/latest)
Running the docker image:
-
Ensure the X11 Server is running
-
Run the docker image:
X11_DISPLAY=192.168.50.34:0.0 # replace with IP address of workstation where X11 server is running DATA_DIR=/var/opt/data/kleinanzeigen-bot # path to config # /mnt/data is the container's default working directory docker run --rm --interactive --tty \ --shm-size=256m \ -e DISPLAY=$X11_DISPLAY \ -v $DATA_DIR:/mnt/data \ ghcr.io/second-hand-friends/kleinanzeigen-bot \ --help
-
The following components need to be installed:
- Chromium, Google Chrome, or Chromium based Microsoft Edge browser
- Python 3.10 or newer
- pip
- git client
-
Open a command/terminal window
-
Clone the repo using
git clone https://github.com/Second-Hand-Friends/kleinanzeigen-bot/ -
Change into the directory:
cd kleinanzeigen-bot -
Install the Python dependencies using:
pip install pdm pdm install
-
Run the app:
pdm run app --help
-
The following components need to be installed:
- Docker
- git client
- Bash (on Windows e.g. via Cygwin, MSys2 or git)
- X11 - X Window System display server (on Windows e.g. https://github.com/P-St/Portable-X-Server/releases/latest)
-
Clone the repo using
git clone https://github.com/Second-Hand-Friends/kleinanzeigen-bot/ -
Open the cloned directory in a Bash terminal window and navigate to the docker subdirectory
-
Execute
bash build-image.sh -
Ensure the image is built:
$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE second-hand-friends/kleinanzeigen-bot latest c31fd256eeea 1 minute ago 590MB python 3-slim 2052f0475488 5 days ago 123MB
Running the docker image:
-
Ensure the X11 Server is running
-
Run the docker image:
X11_DISPLAY=192.168.50.34:0.0 # replace with IP address of workstation where X11 server is running DATA_DIR=/var/opt/data/kleinanzeigen-bot # path to config # /mnt/data is the container's default working directory docker run --rm --interactive --tty \ --shm-size=256m \ -e DISPLAY=$X11_DISPLAY \ -v $DATA_DIR:/mnt/data \ second-hand-friends/kleinanzeigen-bot \ --help
Usage: kleinanzeigen-bot COMMAND [OPTIONS]
Commands:
publish - (re-)publishes ads
verify - verifies the configuration files
delete - deletes ads
update - updates published ads
download - downloads one or multiple ads
update-check - checks for available updates
update-content-hash – recalculates each ad's content_hash based on the current ad_defaults;
use this after changing config.yaml/ad_defaults to avoid every ad being marked "changed" and republished
create-config - creates a new default configuration file if one does not exist
diagnose - diagnoses browser connection issues and shows troubleshooting information
--
help - displays this help (default command)
version - displays the application version
Options:
--ads=all|due|new|changed|<id(s)> (publish) - specifies which ads to (re-)publish (DEFAULT: due)
Possible values:
* all: (re-)publish all ads ignoring republication_interval
* due: publish all new ads and republish ads according the republication_interval
* new: only publish new ads (i.e. ads that have no id in the config file)
* changed: only publish ads that have been modified since last publication
* <id(s)>: provide one or several ads by ID to (re-)publish, like e.g. "--ads=1,2,3" ignoring republication_interval
* Combinations: You can combine multiple selectors with commas, e.g. "--ads=changed,due" to publish both changed and due ads
--ads=all|new|<id(s)> (download) - specifies which ads to download (DEFAULT: new)
Possible values:
* all: downloads all ads from your profile
* new: downloads ads from your profile that are not locally saved yet
* <id(s)>: provide one or several ads by ID to download, like e.g. "--ads=1,2,3"
--ads=changed|<id(s)> (update) - specifies which ads to update (DEFAULT: changed)
Possible values:
* changed: only update ads that have been modified since last publication
* <id(s)>: provide one or several ads by ID to update, like e.g. "--ads=1,2,3"
--force - alias for '--ads=all'
--keep-old - don't delete old ads on republication
--config=<PATH> - path to the config YAML or JSON file (DEFAULT: ./config.yaml)
--logfile=<PATH> - path to the logfile (DEFAULT: ./kleinanzeigen-bot.log)
--lang=en|de - display language (STANDARD: system language if supported, otherwise English)
-v, --verbose - enables verbose output - only useful when troubleshooting issues
Note: The output of
kleinanzeigen-bot helpis always the most up-to-date reference for available commands and options.
Limitation of download: It's only possible to extract the cheapest given shipping option.
All configuration files can be in YAML or JSON format.
When executing the app it by default looks for a config.yaml file in the current directory. If it does not exist it will be created automatically.
The configuration file to be used can also be specified using the --config <PATH> command line parameter. It must point to a YAML or JSON file.
Valid file extensions are .json, .yaml and .yml
The following parameters can be configured:
# yaml-language-server: $schema=https://raw.githubusercontent.com/Second-Hand-Friends/kleinanzeigen-bot/refs/heads/main/schemas/config.schema.json
# glob (wildcard) patterns to select ad configuration files
# if relative paths are specified, then they are relative to this configuration file
ad_files:
- "./**/ad_*.{json,yml,yaml}"
# default values for ads, can be overwritten in each ad configuration file
ad_defaults:
active: true
type: OFFER # one of: OFFER, WANTED
description_prefix: ""
description_suffix: ""
price_type: NEGOTIABLE # one of: FIXED, NEGOTIABLE, GIVE_AWAY, NOT_APPLICABLE
shipping_type: SHIPPING # one of: PICKUP, SHIPPING, NOT_APPLICABLE
# NOTE: shipping_costs and shipping_options must be configured per-ad, not as defaults
sell_directly: false # requires shipping_options to take effect
contact:
name: ""
street: ""
zipcode:
phone: "" # IMPORTANT: surround phone number with quotes to prevent removal of leading zeros
republication_interval: 7 # every X days ads should be re-published
# additional name to category ID mappings, see default list at
# https://github.com/Second-Hand-Friends/kleinanzeigen-bot/blob/main/src/kleinanzeigen_bot/resources/categories.yaml
categories:
Verschenken & Tauschen > Tauschen: 272/273
Verschenken & Tauschen > Verleihen: 272/274
Verschenken & Tauschen > Verschenken: 272/192
# timeout tuning (optional)
timeouts:
multiplier: 1.0 # Scale all timeouts (e.g. 2.0 for slower networks)
default: 5.0 # Base timeout for web_find/web_click/etc.
page_load: 15.0 # Timeout for web_open page loads
captcha_detection: 2.0 # Timeout for captcha iframe detection
sms_verification: 4.0 # Timeout for SMS verification banners
gdpr_prompt: 10.0 # Timeout when handling GDPR dialogs
login_detection: 10.0 # Timeout for detecting existing login session via DOM elements
publishing_result: 300.0 # Timeout for publishing status checks
publishing_confirmation: 20.0 # Timeout for publish confirmation redirect
image_upload: 30.0 # Timeout for image upload and server-side processing
pagination_initial: 10.0 # Timeout for first pagination lookup
pagination_follow_up: 5.0 # Timeout for subsequent pagination clicks
quick_dom: 2.0 # Generic short DOM timeout (shipping dialogs, etc.)
update_check: 10.0 # Timeout for GitHub update requests
chrome_remote_probe: 2.0 # Timeout for local remote-debugging probes
chrome_remote_debugging: 5.0 # Timeout for remote debugging API calls
chrome_binary_detection: 10.0 # Timeout for chrome --version subprocess
retry_enabled: true # Enables DOM retry/backoff when timeouts occur
retry_max_attempts: 2
retry_backoff_factor: 1.5
# download configuration
download:
include_all_matching_shipping_options: false # if true, all shipping options matching the package size will be included
excluded_shipping_options: [] # list of shipping options to exclude, e.g. ['DHL_2', 'DHL_5']
folder_name_max_length: 100 # maximum length for folder names when downloading ads (default: 100)
rename_existing_folders: false # if true, rename existing folders without titles to include titles (default: false)
# publishing configuration
publishing:
delete_old_ads: "AFTER_PUBLISH" # one of: AFTER_PUBLISH, BEFORE_PUBLISH, NEVER
delete_old_ads_by_title: true # only works if delete_old_ads is set to BEFORE_PUBLISH
# captcha-Handling (optional)
# To ensure that the bot does not require manual confirmation after a captcha, but instead automatically pauses for a defined period and then restarts, you can enable the captcha section:
captcha:
auto_restart: true # If true, the bot aborts when a Captcha appears and retries publishing later
# If false (default), the Captcha must be solved manually to continue
restart_delay: 1h 30m # Time to wait before retrying after a Captcha was encountered (default: 6h)
# browser configuration
browser:
# https://peter.sh/experiments/chromium-command-line-switches/
arguments:
# https://stackoverflow.com/a/50725918/5116073
- --disable-dev-shm-usage
- --no-sandbox
# --headless
# --start-maximized
binary_location: # path to custom browser executable, if not specified will be looked up on PATH
extensions: [] # a list of .crx extension files to be loaded
use_private_window: true
user_data_dir: "" # see https://github.com/chromium/chromium/blob/main/docs/user_data_dir.md
profile_name: ""
# update check configuration
update_check:
enabled: true # Enable/disable update checks
channel: latest # One of: latest, prerelease
interval: 7d # Check interval (e.g. 7d for 7 days)
# If the interval is invalid, too short (<1d), or too long (>30d),
# the bot logs a warning and uses a default interval for this run:
# - 1d for 'prerelease' channel
# - 7d for 'latest' channel
# The config file is not changed automatically; please fix your config to avoid repeated warnings.
# login credentials
login:
username: ""
password: ""Slow networks or sluggish remote browsers often just need a higher timeouts.multiplier, while truly problematic selectors can get explicit values directly under timeouts. Remember to regenerate the schemas after changing the configuration model so editors stay in sync.
Each ad is described in a separate JSON or YAML file with prefix ad_<filename>. The prefix is configurable in config file.
Parameter values specified in the ad_defaults section of the config.yaml file don't need to be specified again in the ad configuration file.
The following parameters can be configured:
# yaml-language-server: $schema=https://raw.githubusercontent.com/Second-Hand-Friends/kleinanzeigen-bot/refs/heads/main/schemas/ad.schema.json
active: # true or false (default: true)
type: # one of: OFFER, WANTED (default: OFFER)
title:
description: # can be multiline, see syntax here https://yaml-multiline.info/
description_prefix: # optional prefix to be added to the description overriding the default prefix
description_suffix: # optional suffix to be added to the description overriding the default suffix
# built-in category name as specified in https://github.com/Second-Hand-Friends/kleinanzeigen-bot/blob/main/src/kleinanzeigen_bot/resources/categories.yaml
# or custom category name as specified in config.yaml
# or category ID (e.g. 161/278)
category: # e.g. "Elektronik > Notebooks"
price: # price in euros; decimals allowed but will be rounded to nearest whole euro on processing (prefer whole euros for predictability)
price_type: # one of: FIXED, NEGOTIABLE, GIVE_AWAY (default: NEGOTIABLE)
auto_price_reduction:
enabled: # true or false to enable automatic price reduction on reposts (default: false)
strategy: # "PERCENTAGE" or "FIXED" (required when enabled is true)
amount: # reduction amount; interpreted as percent for PERCENTAGE or currency units for FIXED (prefer whole euros for predictability)
min_price: # required when enabled is true; minimum price floor (use 0 for no lower bound, prefer whole euros for predictability)
delay_reposts: # number of reposts to wait before first reduction (default: 0)
delay_days: # number of days to wait after publication before reductions (default: 0)
# NOTE: All prices are rounded to whole euros after each reduction step.
special_attributes:
# haus_mieten.zimmer_d: value # Zimmer
shipping_type: # one of: PICKUP, SHIPPING, NOT_APPLICABLE (default: SHIPPING)
shipping_costs: # e.g. 2.95
# specify shipping options / packages
# it is possible to select multiple packages, but only from one size (S, M, L)!
# possible package types for size S:
# - DHL_2
# - Hermes_Päckchen
# - Hermes_S
# possible package types for size M:
# - DHL_5
# - Hermes_M
# possible package types for size L:
# - DHL_10
# - DHL_20
# - DHL_31,5
# - Hermes_L
shipping_options: []
sell_directly: # true or false, requires shipping_options to take effect (default: false)
# list of wildcard patterns to select images
# if relative paths are specified, then they are relative to this ad configuration file
images:
#- laptop_*.{jpg,png}
contact:
name:
street:
zipcode:
phone: "" # IMPORTANT: surround phone number with quotes to prevent removal of leading zeros
republication_interval: # every X days the ad should be re-published (default: 7)
# The following fields are automatically managed by the bot:
id: # the ID assigned by kleinanzeigen.de
created_on: # ISO timestamp when the ad was first published
updated_on: # ISO timestamp when the ad was last published
content_hash: # hash of the ad content, used to detect changes
repost_count: # how often the ad has been (re)published; used for automatic price reductionsWhen auto_price_reduction.enabled is set to true, the bot lowers the configured price every time the ad is reposted. The starting point for the calculation is always the base price from your ad file (the value of price), ensuring the first publication uses the unchanged amount. For each repost the bot subtracts either a percentage of the previously published price (strategy: PERCENTAGE) or a fixed amount (strategy: FIXED) and clamps the result to min_price.
Important: Price reductions only apply when using the publish command (which deletes the old ad and creates a new one). Using the update command to modify ad content does NOT trigger price reductions or increment repost_count.
repost_count is tracked for every ad (and persisted inside the corresponding ad_*.yaml) so reductions continue across runs.
min_price is required whenever enabled is true and must be less than or equal to price; this makes an explicit floor (including 0) mandatory. If min_price equals the current price, the bot will log a warning and perform no reduction.
Note: repost_count and price reduction counters are only incremented and persisted after a successful publish. Failed publish attempts do not advance the counters.
PERCENTAGE strategy example:
price: 150
price_type: FIXED
auto_price_reduction:
enabled: true
strategy: PERCENTAGE
amount: 10
min_price: 90
delay_reposts: 0
delay_days: 0This posts the ad at 150 € the first time, then 135 € (−10%), 122 € (−10%), 110 € (−10%), 99 € (−10%), and stops decreasing at 90 €.
Note: The bot applies commercial rounding (ROUND_HALF_UP) to full euros after each reduction step. For example, 121.5 rounds to 122, and 109.8 rounds to 110. This step-wise rounding affects the final price progression, especially for percentage-based reductions.
FIXED strategy example:
price: 150
price_type: FIXED
auto_price_reduction:
enabled: true
strategy: FIXED
amount: 15
min_price: 90
delay_reposts: 0
delay_days: 0This posts the ad at 150 € the first time, then 135 € (−15 €), 120 € (−15 €), 105 € (−15 €), and stops decreasing at 90 €.
Note on delay_days behavior: The delay_days parameter counts complete 24-hour periods (whole days) since the ad was published. For example, if delay_days: 7 and the ad was published 6 days and 23 hours ago, the reduction will not yet apply. This ensures predictable behavior and avoids partial-day ambiguity.
Set auto_price_reduction.enabled: false (or omit the entire auto_price_reduction section) to keep the existing behaviour—prices stay fixed and repost_count only acts as tracked metadata for future changes.
You can configure auto_price_reduction once under ad_defaults in config.yaml. The min_price can be set there or overridden per ad file as needed.
You can add prefix and suffix text to your ad descriptions in two ways:
In your config.yaml file you can specify a description_prefix and description_suffix under the ad_defaults section.
ad_defaults:
description_prefix: "Prefix text"
description_suffix: "Suffix text"In your ad configuration file you can specify a description_prefix and description_suffix under the description section.
description:
prefix: "Prefix text"
suffix: "Suffix text"The new format has precedence over the legacy format. If you specify both the new and the legacy format in your config, the new format will be used. We recommend using the new format as it is more flexible and easier to manage.
By default a new browser process will be launched. To reuse a manually launched browser window/process follow these steps:
-
Manually launch your browser from the command line with the
--remote-debugging-port=<NUMBER>flag. You are free to choose an unused port number 1025 and 65535, e.g.:chrome --remote-debugging-port=9222chromium --remote-debugging-port=9222msedge --remote-debugging-port=9222
This runs the browser in debug mode which allows it to be remote controlled by the bot.
⚠️ IMPORTANT: Chrome 136+ Security RequirementStarting with Chrome 136 (March 2025), Google has implemented security changes that require
--user-data-dirto be specified when using--remote-debugging-port. This prevents attackers from accessing the default Chrome profile and stealing cookies/credentials.You must now use:
chrome --remote-debugging-port=9222 --user-data-dir=/path/to/custom/directory
And in your config.yaml:
browser: arguments: - --remote-debugging-port=9222 - --user-data-dir=/path/to/custom/directory user_data_dir: "/path/to/custom/directory"
The bot will automatically detect Chrome 136+ and validate your configuration. If validation fails, you'll see clear error messages with specific instructions on how to fix your configuration.
-
In your config.yaml specify the same flags as browser arguments, e.g.:
browser: arguments: - --remote-debugging-port=9222 - --user-data-dir=/tmp/chrome-debug-profile # Required for Chrome 136+ user_data_dir: "/tmp/chrome-debug-profile" # Must match the argument above
-
When now publishing ads the manually launched browser will be re-used.
NOTE: If an existing browser is used all other settings configured under
browserin your config.yaml file will ignored because they are only used to programmatically configure/launch a dedicated browser instance.
Security Note: This change was implemented by Google to protect users from cookie theft attacks. The custom user data directory uses a different encryption key than the default profile, making it more secure for debugging purposes.
If you encounter browser connection problems, the bot includes a diagnostic command to help identify issues:
For binary users:
kleinanzeigen-bot diagnoseFor source users:
pdm run app diagnoseThis command will check your browser setup and provide troubleshooting information. For detailed solutions to common browser connection issues, see the Browser Connection Troubleshooting Guide.
Please read CONTRIBUTING.md before contributing code. Thank you!
- DanielWTE/ebay-kleinanzeigen-api (Python) API interface to get random listings from kleinanzeigen.de
- f-rolf/ebaykleinanzeiger (Python) Discord bot that watches search results
- r-unruh/kleinanzeigen-filter (JavaScript) Chrome extension that filters out unwanted results from searches on kleinanzeigen.de
- simonsagstetter/Feinanzeigen (JavaScript) Chrome extension that improves search on kleinanzeigen.de
- Superschnizel/Kleinanzeigen-Telegram-Bot (Python) Telegram bot to scrape kleinanzeigen.de
- tillvogt/KleinanzeigenScraper (Python) Webscraper which stores scraped info from kleinanzeigen.de in an SQL database
- TLINDEN/Kleingebäck (Go) kleinanzeigen.de Backup
All files in this repository are released under the GNU Affero General Public License v3.0 or later.
Individual files contain the following tag instead of the full license text:
SPDX-License-Identifier: AGPL-3.0-or-later
This enables machine processing of license information based on the SPDX License Identifiers that are available here: https://spdx.org/licenses/.