Kleinanzeigen API: Search German Classifieds as JSON
To get Kleinanzeigen listings as JSON, call GET /v1/kleinanzeigen/search with a query and, optionally, location, category, price_min, price_max and page. Each ad comes back with its id, title, price, location, image and a description excerpt. A successful call costs 1 credit, a failed call costs nothing, and a daily check of one search is 30 credits a month.
Which Kleinanzeigen endpoints can you call?
The Kleinanzeigen API has three endpoints, all GET, all 1 credit per successful call. Search finds ads by keyword, Details returns one ad by its id, and Shipping Options takes no parameters at all. For monitoring, Search does nearly all the work, and you reach for Details only when a result is worth a closer look.
| Method | Path | Returns | Credits | Required | Optional |
|---|---|---|---|---|---|
| GET | /v1/kleinanzeigen/search |
Search | 1 | query |
page, location, category, price_min, price_max |
| GET | /v1/kleinanzeigen/details |
Details | 1 | ad_id |
none |
| GET | /v1/kleinanzeigen/shipping-options |
Shipping Options | 1 | none | none |
The pages for Search, Details and Shipping Options each have a playground where you can run a real call with your key. Kleinanzeigen sits in the Marketplaces area of the catalogue, alongside sources like Vinted and Craigslist.
How do you authenticate and make the first request?
Send your key in the X-API-Key header. Keys start with sk_live_, and you get one by signing up and verifying an email address. There's no credit card, and the free tier gives you 1,000 credits every month. Where a header isn't possible, the api_key query parameter works too. The authentication docs cover both.
Here is a search for iPhones in Berlin, in the category elektronik, priced between 50 and 500:
curl "https://api.perceptlayer.com/v1/kleinanzeigen/search?query=iphone&page=1&location=Berlin&category=elektronik&price_min=50&price_max=500" \
-H "X-API-Key: $API_KEY"
Every successful response carries an X-Credits-Used header (1 here) and X-Credits-Remaining, the balance after the call. Check them once while testing so you know what a run costs before you schedule it.
Which parameters does the search endpoint take?
Search takes one required parameter and five optional ones. query is a string of up to 500 characters. location and category are strings of up to 100 characters. price_min, price_max and page are integers. Nothing else about them is documented, so test unusual values in the playground before you build on them.
| Parameter | Type | Required | Notes |
|---|---|---|---|
query |
string | yes | Up to 500 characters |
page |
integer | no | Request page=2 and onward while has_next_page is true |
location |
string | no | Up to 100 characters; Berlin in the example |
category |
string | no | Up to 100 characters; elektronik in the example |
price_min |
integer | no | Lower price bound |
price_max |
integer | no | Upper price bound |
Invalid input returns a 422 with the code invalid_parameters and costs nothing. See the errors docs for the error codes.
What does a search response look like?
A search returns success, a data array of ads and a meta object that echoes your filters and reports paging. This is a trimmed excerpt of a recorded response for the request above; the ad itself is cut to the fields you'd store.
{
"success": true,
"data": [
{
"id": "3547273212",
"title": "Apple iPhone 13 Pro Max 256GB Silber/Weiß Zustand Sehr gut OVP",
"url": "https://www.kleinanzeigen.de/s-anzeige/apple-iphone-13-pro-max-256gb-silber-weiss-zustand-sehr-gut-ovp/3547273212-173-3473",
"price": "399 €",
"price_numeric": 399,
"location": "13187 Pankow",
"description": "Hallo zusammen, ich verkaufe mein iPhone 13 Pro Max mit 256 GB in der Farbe Silber/Weiß. Das..."
},
…
],
"meta": {
"query": "iphone",
"page": 1,
"location": "Berlin",
"category": "elektronik",
"results_count": 27,
"has_next_page": true,
"duration_ms": 1327.64
}
}
Three fields matter for monitoring. id identifies the ad across runs. price_numeric is a number, so you can sort and compare. has_next_page says whether another page exists. Note that id is a string here, while ad_id on the Details endpoint is an integer, so convert it with int() before you pass it on.
The title and description fields come back in German, as the sellers wrote them. The description is an excerpt that ends in an ellipsis, not the full ad text.
How do you monitor local prices with a script?
A price monitor needs three things: a fixed set of searches, a store of what you've already seen, and a rule for what counts as news. The script below runs one search, walks the pages, compares each ad with a local JSON file and prints new ads and price drops. It uses only the endpoint, parameters and fields shown above.
import json
import os
import time
from pathlib import Path
import requests
BASE = "https://api.perceptlayer.com/v1/kleinanzeigen/search"
HEADERS = {"X-API-Key": os.environ["API_KEY"]}
STATE = Path("seen.json")
PARAMS = {
"query": "iphone",
"location": "Berlin",
"category": "elektronik",
"price_min": 50,
"price_max": 500,
}
MAX_PAGES = 2
seen = json.loads(STATE.read_text()) if STATE.exists() else {}
page = 1
while page <= MAX_PAGES:
resp = requests.get(BASE, headers=HEADERS, params={**PARAMS, "page": page}, timeout=30)
if resp.status_code in (429, 503):
time.sleep(int(resp.headers.get("Retry-After", "5")))
continue
resp.raise_for_status()
body = resp.json()
for ad in body["data"]:
price = ad.get("price_numeric")
if price is None:
continue
old = seen.get(ad["id"])
if old is None:
print(f"NEW {price:>5} EUR {ad['title']} {ad['url']}")
elif price < old["price"]:
print(f"DROP {old['price']} -> {price} EUR {ad['title']} {ad['url']}")
seen[ad["id"]] = {"price": price, "title": ad["title"]}
if not body["meta"]["has_next_page"]:
break
page += 1
STATE.write_text(json.dumps(seen, ensure_ascii=False))
Run it once to build the baseline; every NEW line on that first run is just the current inventory. After that, only changes print. Because seen.json keeps every ad it has met, an ad that disappears from the results simply stops updating. If you want a closer look at a stored ad, the Details endpoint takes its id as ad_id.
The script uses the requests library and waits and retries on 429 and 503 using the Retry-After header, and it raises on other errors. Requests have no idempotency key, so a call that timed out on your side may still have been billed. For proper retry rules see handling web data API errors in Python.
How do you run it every day?
Cron is enough. A line like 0 7 * * * cd /path/to/monitor && python monitor.py runs the check at 07:00 daily. If you want queues or hosted workflows instead, scheduling web data pulls compares the options. To turn the printed lines into an agent that reasons about them, see building a price tracking agent in Python.
What does a daily check cost?
Each page you fetch is one call, and each call is 1 credit, so cost is searches × pages × days. Failed calls return X-Credits-Used: 0 and are never billed. The table works the arithmetic for a 30-day month; credits are the unit, and the pricing page shows what credits cost.
| Setup | Calls a day | Calls × 1 credit, 30 days | Credits a month |
|---|---|---|---|
| 1 search, 1 page | 1 | 30 × 1 | 30 |
| 1 search, 2 pages | 2 | 60 × 1 | 60 |
| 5 searches, 2 pages each | 10 | 300 × 1 | 300 |
| 20 searches, 3 pages each | 60 | 1,800 × 1 | 1,800 |
The first three setups fit inside the 1,000 free credits a month. The fourth exceeds them by 800 credits, which the smallest prepaid pack, the 10,000-credit Starter, covers many times over. Purchased credits last 12 months, and there's no subscription. A Details call per new ad adds 1 credit each, so budget for it separately. The credits docs explain how debits work.
Speed isn't a constraint at this scale. Free accounts get 1 request a second, and a daily job makes a handful of calls. If you ever fan out many searches at once, read the rate limits first.
When is the official site a better fit?
If you only want to browse or watch a handful of searches yourself, the Kleinanzeigen site itself is the simpler tool, with no code to maintain. The API earns its place when you need the data in your own system: price history across weeks, joins with other sources, or an agent that acts on new ads.
Two limits are worth stating. Data is collected from publicly available sources and returned as found at the time of the call, so a listing can change or vanish between your check and a human's click. And Search gives you an excerpt of the description, not the full text. If you combine Kleinanzeigen with other marketplaces, deduplicating listings across sources covers matching ads that have no shared id.
Next step: get a free API key, then paste the cURL call above into your terminal and read X-Credits-Used in the response headers. The Kleinanzeigen Search page lets you try other filters in the playground first.
APIs in this post
Frequently asked questions
Is there an official Kleinanzeigen API?
Check Kleinanzeigen's own site for any official or partner interface; it isn't something we document here. The Perceptlayer Kleinanzeigen API is a separate route to the data and gives you three endpoints (Search, Details and Shipping Options) as JSON with one X-API-Key header. Each successful call costs 1 credit, and the free tier includes 1,000 credits every month with no credit card.
How much does a Kleinanzeigen search cost?
One search call costs 1 credit, and each extra page is another call. Checking one search over 2 pages daily is 60 calls × 1 credit = 60 credits in a month. Failed calls cost nothing and return X-Credits-Used: 0. Credits are prepaid, and the pricing page shows what a pack of credits costs.
How do I get the next page of results?
Read meta.has_next_page in the response. When it is true, repeat the same request with page increased by one. Each page is its own 1-credit call, so cap the number of pages in your script to keep a daily run's cost fixed, as the example does with MAX_PAGES.
What happens if a call fails or I hit the rate limit?
Errors return an HTTP status and a JSON body with code, message and request_id, and nothing is debited. A 429 rate_limited or 503 temporarily_unavailable response includes a Retry-After header with the seconds to wait. A 402 insufficient_credits means the prepaid balance is empty; there is no automatic top-up.
Can an AI agent search Kleinanzeigen?
Yes. The MCP server at https://api.perceptlayer.com/mcp exposes search_endpoints, get_endpoint and call_endpoint. An agent can look up the Search endpoint for free, then call it with a query; only the call itself spends 1 credit on success. The AI agents docs show the setup.