pyfunda 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .eggs/
7
+ *.egg
8
+ .pytest_cache/
9
+ .venv/
10
+ venv/
pyfunda-2.0.0/PKG-INFO ADDED
@@ -0,0 +1,445 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyfunda
3
+ Version: 2.0.0
4
+ Summary: Python API for Funda.nl real estate listings
5
+ Project-URL: Homepage, https://github.com/0xMH/pyfunda
6
+ Project-URL: Repository, https://github.com/0xMH/pyfunda
7
+ Project-URL: Issues, https://github.com/0xMH/pyfunda/issues
8
+ Author: 0xMH
9
+ License-Expression: MIT
10
+ Keywords: api,funda,housing,netherlands,real-estate,scraper
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: requests>=2.28.0
20
+ Description-Content-Type: text/markdown
21
+
22
+ # Funda
23
+
24
+ Python API for [Funda.nl](https://www.funda.nl) real estate listings.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install -r requirements.txt
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ```python
35
+ from funda import Funda
36
+
37
+ f = Funda()
38
+
39
+ # Get a listing by ID
40
+ listing = f.get_listing(43117443)
41
+ print(listing['title'], listing['city'])
42
+ # Reehorst 13 Luttenberg
43
+
44
+ # Get a listing by URL
45
+ listing = f.get_listing('https://www.funda.nl/detail/koop/amsterdam/appartement-123/43117443/')
46
+
47
+ # Search listings
48
+ results = f.search_listing('amsterdam', price_max=500000)
49
+ for r in results:
50
+ print(r['title'], r['price'])
51
+ ```
52
+
53
+ ## How It Works
54
+
55
+ This library uses Funda's undocumented mobile app API, which provides clean JSON responses unlike the website that embeds data in Nuxt.js/JavaScript bundles.
56
+
57
+ ### Discovery Process
58
+
59
+ The API was reverse engineered by intercepting and analyzing HTTPS traffic from the official Funda Android app:
60
+
61
+ 1. Configured an Android device to route traffic through an intercepting proxy
62
+ 2. Used the Funda app normally - browsing listings, searching, opening shared URLs
63
+ 3. Identified the `*.funda.io` API infrastructure separate from the `www.funda.nl` website
64
+ 4. Analyzed request/response patterns to understand the query format and available filters
65
+ 5. Discovered how the app resolves URL-based IDs (`tinyId`) to internal IDs (`globalId`)
66
+
67
+ ### API Architecture
68
+
69
+ The mobile app communicates with a separate API at `*.funda.io`:
70
+
71
+ | Endpoint | Method | Purpose |
72
+ |----------|--------|---------|
73
+ | `listing-detail-page.funda.io/api/v4/listing/object/nl/{globalId}` | GET | Fetch listing by internal ID |
74
+ | `listing-detail-page.funda.io/api/v4/listing/object/nl/tinyId/{tinyId}` | GET | Fetch listing by URL ID |
75
+ | `listing-search-wonen.funda.io/_msearch/template` | POST | Search listings |
76
+
77
+ ### ID System
78
+
79
+ Funda uses two ID systems:
80
+ - **globalId**: Internal numeric ID (7 digits), used in the database
81
+ - **tinyId**: Public-facing ID (8-9 digits), appears in URLs like `funda.nl/detail/koop/amsterdam/.../{tinyId}/`
82
+
83
+ The `tinyId` endpoint was key - it allows fetching any listing directly from a Funda URL without needing to know the internal ID.
84
+
85
+ ### Search API
86
+
87
+ Search uses Elasticsearch's [Multi Search Template API](https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-search-template.html) with NDJSON format:
88
+
89
+ ```
90
+ {"index":"listings-wonen-searcher-alias-prod"}
91
+ {"id":"search_result_20250805","params":{...}}
92
+ ```
93
+
94
+ **Search parameters:**
95
+
96
+ | Parameter | Description | Example |
97
+ |-----------|-------------|---------|
98
+ | `selected_area` | Location filter | `["amsterdam"]` |
99
+ | `radius_search` | Radius from location | `{"index": "geo-wonen-alias-prod", "id": "1012AB-0", "path": "area_with_radius.10"}` |
100
+ | `offering_type` | Buy or rent | `"buy"` or `"rent"` |
101
+ | `price.selling_price` | Price range (buy) | `{"from": 200000, "to": 500000}` |
102
+ | `price.rent_price` | Price range (rent) | `{"from": 500, "to": 2000}` |
103
+ | `object_type` | Property types | `["house", "apartment"]` |
104
+ | `floor_area` | Living area m² | `{"from": 50, "to": 150}` |
105
+ | `plot_area` | Plot area m² | `{"from": 100, "to": 500}` |
106
+ | `energy_label` | Energy labels | `["A", "A+", "A++"]` |
107
+ | `sort` | Sort order | `{"field": "publish_date_utc", "order": "desc"}` |
108
+ | `page.from` | Pagination offset | `0`, `15`, `30`... |
109
+
110
+ Results are paginated with 15 listings per page.
111
+
112
+ **Valid radius values:** 1, 2, 5, 10, 15, 30, 50 km (other values are not indexed).
113
+
114
+ ### Required Headers
115
+
116
+ ```
117
+ User-Agent: Dart/3.9 (dart:io)
118
+ X-Funda-App-Platform: android
119
+ Content-Type: application/json
120
+ ```
121
+
122
+ ### Response Data
123
+
124
+ Listing responses include:
125
+ - **Identifiers** - globalId, tinyId
126
+ - **AddressDetails** - title, city, postcode, province, neighbourhood, house number
127
+ - **Price** - numeric and formatted prices (selling or rental), auction flag
128
+ - **FastView** - bedrooms, living area, plot area, energy label
129
+ - **Media** - photos, floorplans, videos, 360° photos, brochure URL (all with CDN base URLs)
130
+ - **KenmerkSections** - detailed property characteristics (70+ fields)
131
+ - **Coordinates** - latitude/longitude
132
+ - **ObjectInsights** - view and save counts
133
+ - **Advertising.TargetingOptions** - boolean features (garden, balcony, solar panels, heat pump, parking, etc.), construction year, room counts
134
+ - **Share** - shareable URL
135
+ - **GoogleMapsObjectUrl** - direct Google Maps link
136
+ - **PublicationDate** - when the listing was published
137
+ - **Tracking.Values.brokers** - broker ID and association
138
+
139
+ ## API Reference
140
+
141
+ ### Funda
142
+
143
+ Main entry point for the API.
144
+
145
+ ```python
146
+ from funda import Funda
147
+
148
+ f = Funda(timeout=30)
149
+ ```
150
+
151
+ #### get_listing(listing_id)
152
+
153
+ Get a single listing by ID or URL.
154
+
155
+ ```python
156
+ # By numeric ID (tinyId or globalId)
157
+ listing = f.get_listing(43117443)
158
+
159
+ # By URL
160
+ listing = f.get_listing('https://www.funda.nl/detail/koop/city/house-name/43117443/')
161
+ ```
162
+
163
+ #### search_listing(location, ...)
164
+
165
+ Search for listings with filters.
166
+
167
+ ```python
168
+ results = f.search_listing(
169
+ location='amsterdam', # City or area name
170
+ offering_type='buy', # 'buy' or 'rent'
171
+ price_min=200000, # Minimum price
172
+ price_max=500000, # Maximum price
173
+ area_min=50, # Minimum living area (m²)
174
+ area_max=150, # Maximum living area (m²)
175
+ plot_min=100, # Minimum plot area (m²)
176
+ plot_max=500, # Maximum plot area (m²)
177
+ object_type=['house'], # Property types (default: house, apartment)
178
+ energy_label=['A', 'A+'], # Energy labels to filter
179
+ sort='newest', # Sort order (see below)
180
+ page=0, # Page number (15 results per page)
181
+ )
182
+ ```
183
+
184
+ **Radius search** - search within a radius from a postcode or city:
185
+
186
+ ```python
187
+ results = f.search_listing(
188
+ location='1012AB', # Postcode or city
189
+ radius_km=10, # Search radius in km
190
+ price_max=750000,
191
+ )
192
+ ```
193
+
194
+ > **Note:** Valid radius values are 1, 2, 5, 10, 15, 30, and 50 km. Other values are automatically mapped to the nearest valid radius.
195
+
196
+ **Sort options:**
197
+
198
+ | Sort Value | Description |
199
+ |------------|-------------|
200
+ | `newest` | Most recently published first |
201
+ | `oldest` | Oldest listings first |
202
+ | `price_asc` | Lowest price first |
203
+ | `price_desc` | Highest price first |
204
+ | `area_asc` | Smallest living area first |
205
+ | `area_desc` | Largest living area first |
206
+ | `plot_desc` | Largest plot area first |
207
+ | `city` | Alphabetically by city |
208
+ | `postcode` | Alphabetically by postcode |
209
+
210
+ **Multiple locations:**
211
+
212
+ ```python
213
+ results = f.search_listing(['amsterdam', 'rotterdam', 'utrecht'])
214
+ ```
215
+
216
+ ### Listing
217
+
218
+ Listing objects support dict-like access with convenient aliases.
219
+
220
+ **Basic info:**
221
+
222
+ ```python
223
+ listing['title'] # Property title/address
224
+ listing['city'] # City name
225
+ listing['postcode'] # Postal code
226
+ listing['province'] # Province
227
+ listing['neighbourhood'] # Neighbourhood name
228
+ listing['municipality'] # Municipality (gemeente)
229
+ listing['house_number'] # House number
230
+ listing['house_number_ext'] # House number extension (e.g., "A", "II")
231
+ ```
232
+
233
+ **Price & Status:**
234
+
235
+ ```python
236
+ listing['price'] # Numeric price
237
+ listing['price_formatted'] # Formatted price string (e.g., "€ 450.000 k.k.")
238
+ listing['price_per_m2'] # Price per m² (from characteristics)
239
+ listing['status'] # "available" or "sold"
240
+ listing['offering_type'] # "Sale" or "Rent"
241
+ ```
242
+
243
+ **Property details:**
244
+
245
+ ```python
246
+ listing['object_type'] # House, Apartment, etc.
247
+ listing['house_type'] # Type of house (e.g., "Tussenwoning")
248
+ listing['construction_type'] # New or existing construction
249
+ listing['construction_year'] # Year built
250
+ listing['bedrooms'] # Number of bedrooms
251
+ listing['rooms'] # Total number of rooms
252
+ listing['living_area'] # Living area in m²
253
+ listing['plot_area'] # Plot area in m²
254
+ listing['energy_label'] # Energy label (A, B, C, etc.)
255
+ listing['description'] # Full description text
256
+ ```
257
+
258
+ **Dates:**
259
+
260
+ ```python
261
+ listing['publication_date'] # When listed on Funda
262
+ listing['offered_since'] # "Offered since" date (from characteristics)
263
+ listing['acceptance'] # Acceptance terms (e.g., "In overleg")
264
+ ```
265
+
266
+ **Location:**
267
+
268
+ ```python
269
+ listing['coordinates'] # (lat, lng) tuple
270
+ listing['latitude'] # Latitude
271
+ listing['longitude'] # Longitude
272
+ listing['google_maps_url'] # Direct Google Maps link
273
+ ```
274
+
275
+ **Media:**
276
+
277
+ ```python
278
+ listing['photos'] # List of photo IDs
279
+ listing['photo_urls'] # List of full CDN URLs for photos
280
+ listing['photo_count'] # Number of photos
281
+ listing['floorplans'] # List of floorplan IDs
282
+ listing['floorplan_urls'] # List of full CDN URLs for floorplans
283
+ listing['videos'] # List of video IDs
284
+ listing['video_urls'] # List of video URLs
285
+ listing['photos_360'] # List of 360° photo dicts with name, id, url
286
+ listing['brochure_url'] # PDF brochure URL (if available)
287
+ ```
288
+
289
+ **Property features (booleans):**
290
+
291
+ ```python
292
+ listing['has_garden'] # Has garden
293
+ listing['has_balcony'] # Has balcony
294
+ listing['has_roof_terrace'] # Has roof terrace
295
+ listing['has_solar_panels'] # Has solar panels
296
+ listing['has_heat_pump'] # Has heat pump
297
+ listing['has_parking_on_site'] # Parking on property
298
+ listing['has_parking_enclosed'] # Enclosed parking
299
+ listing['is_energy_efficient'] # Energy efficient property
300
+ listing['is_monument'] # Listed/protected building
301
+ listing['is_fixer_upper'] # Fixer-upper (kluswoning)
302
+ listing['is_auction'] # Sold via auction
303
+ listing['open_house'] # Has open house scheduled
304
+ ```
305
+
306
+ **Stats & metadata:**
307
+
308
+ ```python
309
+ listing['views'] # Number of views on Funda
310
+ listing['saves'] # Number of times saved
311
+ listing['highlight'] # Highlight text (blikvanger)
312
+ listing['global_id'] # Internal Funda ID
313
+ listing['tiny_id'] # Public ID (used in URLs)
314
+ listing['url'] # Full Funda URL
315
+ listing['share_url'] # Shareable URL
316
+ listing['broker_id'] # Broker ID
317
+ listing['broker_association'] # Broker association (e.g., "NVM")
318
+ listing['characteristics'] # Dict of all detailed characteristics
319
+ ```
320
+
321
+ **Key aliases** - these all work:
322
+
323
+ | Alias | Canonical Key |
324
+ |-------|---------------|
325
+ | `name`, `address` | `title` |
326
+ | `location`, `locality` | `city` |
327
+ | `area`, `size` | `living_area` |
328
+ | `type`, `property_type` | `object_type` |
329
+ | `images`, `pictures`, `media` | `photos` |
330
+ | `agent`, `realtor`, `makelaar` | `broker` |
331
+ | `zip`, `zipcode`, `postal_code` | `postcode` |
332
+
333
+ #### Methods
334
+
335
+ ```python
336
+ listing.summary() # Text summary of the listing
337
+ listing.to_dict() # Convert to plain dictionary
338
+ listing.keys() # List available keys
339
+ listing.get('key') # Get with default (like dict.get)
340
+ listing.getID() # Get listing ID
341
+ ```
342
+
343
+ ## Examples
344
+
345
+ ### Find apartments in Amsterdam under €400k
346
+
347
+ ```python
348
+ from funda import Funda
349
+
350
+ f = Funda()
351
+ results = f.search_listing('amsterdam', price_max=400000)
352
+
353
+ for listing in results:
354
+ print(f"{listing['title']}")
355
+ print(f" Price: €{listing['price']:,}")
356
+ print(f" Area: {listing.get('living_area', 'N/A')}")
357
+ print(f" Bedrooms: {listing.get('bedrooms', 'N/A')}")
358
+ print()
359
+ ```
360
+
361
+ ### Get detailed listing information
362
+
363
+ ```python
364
+ from funda import Funda
365
+
366
+ f = Funda()
367
+ listing = f.get_listing(43117443)
368
+
369
+ print(listing.summary())
370
+
371
+ # Access all characteristics
372
+ for key, value in listing['characteristics'].items():
373
+ print(f"{key}: {value}")
374
+ ```
375
+
376
+ ### Search rentals in multiple cities
377
+
378
+ ```python
379
+ from funda import Funda
380
+
381
+ f = Funda()
382
+ results = f.search_listing(
383
+ location=['amsterdam', 'rotterdam', 'den-haag'],
384
+ offering_type='rent',
385
+ price_max=2000,
386
+ )
387
+
388
+ print(f"Found {len(results)} rentals")
389
+ ```
390
+
391
+ ### Find energy-efficient homes with a garden
392
+
393
+ ```python
394
+ from funda import Funda
395
+
396
+ f = Funda()
397
+ listing = f.get_listing(43117443)
398
+
399
+ # Check property features
400
+ if listing['has_garden'] and listing.get('has_solar_panels'):
401
+ print("Energy efficient with garden!")
402
+
403
+ if listing['is_energy_efficient']:
404
+ print(f"Energy label: {listing['energy_label']}")
405
+ ```
406
+
407
+ ### Download listing photos
408
+
409
+ ```python
410
+ from funda import Funda
411
+ import requests
412
+
413
+ f = Funda()
414
+ listing = f.get_listing(43117443)
415
+
416
+ # Photo URLs are ready to use
417
+ for i, url in enumerate(listing['photo_urls'][:5]):
418
+ response = requests.get(url)
419
+ with open(f"photo_{i}.jpg", "wb") as file:
420
+ file.write(response.content)
421
+
422
+ # Also available: floorplan_urls, video_urls
423
+ ```
424
+
425
+ ### Search by radius from postcode
426
+
427
+ ```python
428
+ from funda import Funda
429
+
430
+ f = Funda()
431
+ results = f.search_listing(
432
+ location='1012AB',
433
+ radius_km=15,
434
+ price_max=600000,
435
+ energy_label=['A', 'A+', 'A++'],
436
+ sort='newest',
437
+ )
438
+
439
+ for r in results:
440
+ print(f"{r['title']} - €{r['price']:,}")
441
+ ```
442
+
443
+ ## License
444
+
445
+ MIT