Quick answer: ?page=2 and ?page=3 on Immowelt's search route return the exact same page-1 content, byte for byte — pagination there is a client-side React control, not a URL parameter, and a naive page loop would silently re-scrape row one over and over while reporting success. Separately, the per-listing detail page returns a hard DataDome challenge on every browser fingerprint we tried. Two different ways one target quietly refuses to cooperate, and neither shows up if you only probe the page you land on first.
The pagination that isn't
The obvious way to page a search-results scraper is: fetch page 1, increment a ?page= parameter, fetch again, stop when the results stop changing. We tried exactly that against Immowelt's server-rendered search route before writing the fetch loop.
Page 2 came back identical to page 1. Same 32 cards, same order, same bytes. So did page 3. We didn't trust one sample — this is the kind of thing that's cheap to get wrong by testing once against a page that happens to be short — so we ran it twice more: a narrow postal-code search (32 cards) and a nationwide search (26 cards), three fetches each. Same result every time.
The reason is structural, not a bug on Immowelt's side: the numbered pagination control on the page is a <button>, not an <a href>. Clicking it fires a client-side re-render — React swapping result sets in the DOM — with no corresponding change to the server route. The ?page= parameter the URL bar shows you after clicking is cosmetic; the server-rendered HTML behind it never moves.
If we'd shipped a page-cursor loop anyway — trusting that a numbered pagination control implies a working page parameter, which is true on most sites — it would have looked completely healthy. Every fetch returns 200. Every fetch parses. The Actor would report success, bill for however many "pages" it fetched, and hand back the same 32 rows repeated as many times as the loop ran. That's a strictly worse failure mode than an error: it costs the customer money and looks like it worked.
So this Actor doesn't page at all. Breadth comes from the input: you supply multiple searchUrls — different cities, different districts, different postal codes — and each one gets fetched exactly once. It's a real constraint on how you use the Actor, and it's in the README's Limitations section rather than buried, because the fix for "I need more than one page of Berlin" is "search a narrower area and pass that URL," not a cursor we can't build.
The wall on the other page
Separately, we checked whether the per-listing detail page (/expose/<uuid>) was worth fetching too, since it carries fields the search cards don't — bathrooms, year built, energy label, full address. Some of our other real-estate Actors have an enrich toggle that does exactly this.
The detail page returns a hard DataDome challenge:
var dd={...}
"host":"geo.captcha-delivery.com"
on both Chrome and Firefox TLS impersonation profiles, with and without a referer set. The search page, meanwhile, is completely clean on the same fingerprints — zero anti-bot surface. Two pages on the same domain, two different postures.
That's the part worth generalizing past this one site: reachability is a property of the endpoint, not the domain. "Immowelt is scrapeable" and "Immowelt's detail pages are scrapeable" are different claims, and the first one being true tells you nothing about the second. The only way to know is to actually fetch the page you plan to loop over and the page you plan to enrich from — not just the entry URL you happened to start recon on.
We didn't reach for a browser to clear it. ADR-0002 reserves Camoufox for targets that genuinely need JS execution to render the data at all; going around a confirmed DataDome wall with a browser is a real scope decision — more compute cost, more fragility — not a default response to a 403. So v1 of this Actor is deliberately search-card-only. No detail-page enrichment, no enrich toggle, and the README says so plainly rather than shipping a toggle that would just fail.
What the search cards give you anyway
The upside: the search cards weren't holding out on us for the fields that matter most. Immowelt renders each listing as a single German string —
Wohnung zum Kauf - Treptow-Köpenick - 170.000 € - 2 Zimmer, 45 m²
— and we parse that into typed, numeric fields instead of shipping the blob:
{
"listing_id": "1215b46c-9e03-4081-bc72-67209544ff9f",
"operation": "sale",
"property_type": "Wohnung",
"district": "Treptow-Köpenick",
"price": 170000.0,
"currency": "EUR",
"area_sqm": 45.0,
"rooms": 2,
"price_per_sqm": 3777.78
}
price_per_sqm is computed, not scraped — Immowelt never states it directly, and it's the single most-requested derived field for anyone comparing districts.
A real cloud run against this pipeline pulled 60 rows, 60 unique listing IDs — 31 sale, 29 rent — deliberately exercising both operation branches in one pass.
The takeaway
Both bugs we didn't ship have the same shape: a control that looks like it does the normal thing (a numbered pagination widget, a domain that's clean on one page) and doesn't generalize the way you'd assume. Neither surfaces from a single happy-path probe. Test the page you'll actually loop over, and the page you'll actually enrich from — not just the one you landed on.
🏠 Immowelt Germany Real Estate Scraper turns Immowelt.de search results — kaufen or mieten, any German city or district — into clean, typed rows: numeric price, area, rooms, district, and computed price-per-sqm, instead of one unparsed German string. We rotate curl-cffi browser fingerprints, retry with backoff on 408/429/5xx, and rotate residential proxy sessions pinned to a German exit on every block, with search-URL- and listing-level fault isolation so one bad card never takes down a run. $1.70 per 1,000 results, and you only pay for rows that land.
FAQ
Why doesn't this Actor page through search results with a page number?
Because Immowelt's pagination control is client-side; ?page=2/?page=3 on the server route return byte-identical page-1 content, confirmed against two searches with three fetches each. Breadth comes from supplying multiple search URLs instead.
Why is there no detail-page enrichment (bathrooms, energy label, year built)?
The /expose/<uuid> detail page returns a hard DataDome challenge on every fingerprint we tried, while the search page is completely clean. Rather than ship a browser-automation workaround for a confirmed anti-bot wall, v1 stays scoped to the search-card fields.
Is price_per_sqm scraped from the page?
No — Immowelt never states it. It's computed from the parsed price and area whenever both are known.
Can I search multiple German cities in one run?
Yes. Pass multiple searchUrls and the Actor fetches and dedupes across all of them — that's the mechanism that replaces a page cursor here.
Top comments (0)